Move an app that uses Okta's SDKs
Check whether an app using okta-auth-js or jwt-verifier works with only the issuer changed, and what to change if it doesn't.
Search your app for the calls in the table. If it uses only the first group, change the issuer and client ID and you're done. Otherwise, follow the section linked from the row.
Does your app work unchanged?
| Your app calls | On IdRa | What you see | What to do |
|---|---|---|---|
signInWithRedirect, token.getWithRedirect, handleLoginRedirect | Works | Change the issuer and client ID | |
token.getUserInfo, token.verify, token.revoke, exchangeCodeForTokens | Works | Change the issuer and client ID | |
signOut | Works | Change the issuer and client ID. A JWT access token stays valid until it expires: see Sign out | |
verifyIdToken | Works | Change the issuer. See jwt-verifier for http | |
signInWithCredentials | Refused | AuthApiError: Failed to fetch, with a CORS error in the console | Sign in by redirect |
token.getWithoutPrompt, or tokenManager.renew with no refresh token | Refused | AuthSdkError: OAuth flow timed out | Renew with a refresh token |
session.exists | Always false, with no error, even with a live IdRa session | false | Replace session.exists |
signOut, with no ID token | Refused | AuthApiError: Failed to fetch | Keep the ID token in tokenManager, or pass idToken |
verifyAccessToken, token from a sign-in with no API resource | Refused | Jwt cannot be parsed | Request an API resource |
token.introspect, client with no secret | Refused | OAuthError: invalid_client: invalid authorization header format | Call it from a server, with a client that has a secret |
signInWithCredentials, session.exists and getWithoutPrompt stay this way on purpose. Each would need a second way to sign in, outside your MFA and sign-on policies, or a session cookie that's sent across sites.
Before you start, create a Single-Page Application under Applications, add your callback to its sign-in redirect URIs and your signed-out page to its sign-out redirect URIs, and assign it. The issuer is https://<your-org>.idra.team/api/auth.
Sign in by redirect
Before, the app collects the password, renews from a hidden iframe, and asks the session cookie whether the person is signed in:
import { OktaAuth } from "@okta/okta-auth-js";
import { issuer, clientId } from "./env.js";
const oktaAuth = new OktaAuth({
issuer,
clientId,
redirectUri: `${location.origin}/login/callback`,
scopes: ["openid", "profile", "email"],
});
const status = document.querySelector("#status");
const report = (e) => (status.textContent = `${e.name}: ${e.message}`);
// The app's own sign-in form.
document.querySelector("#login").addEventListener("submit", async (e) => {
e.preventDefault();
const { username, password } = Object.fromEntries(new FormData(e.target));
try {
const tx = await oktaAuth.signInWithCredentials({ username, password });
await oktaAuth.token.getWithRedirect({ sessionToken: tx.sessionToken });
} catch (err) {
report(err);
}
});
// Silent renew from the session cookie, in a hidden iframe.
document.querySelector("#renew").addEventListener("click", async () => {
try {
const { tokens } = await oktaAuth.token.getWithoutPrompt();
oktaAuth.tokenManager.setTokens(tokens);
status.textContent = "renewed";
} catch (err) {
report(err);
}
});
// Is the person signed in?
document.querySelector("#check").addEventListener("click", async () => {
status.textContent = `session.exists: ${await oktaAuth.session.exists()}`;
});After, the form becomes a button, and IdRa's login page handles the password, MFA and policies. The whole app:
import { OktaAuth } from "@okta/okta-auth-js";
import { issuer, clientId } from "./env.js";
const oktaAuth = new OktaAuth({
issuer,
clientId,
redirectUri: `${location.origin}/login/callback`,
postLogoutRedirectUri: `${location.origin}/after`,
// offline_access asks for the refresh token that replaces the hidden iframe.
scopes: ["openid", "profile", "email", "offline_access"],
restoreOriginalUri: (_, originalUri) => location.replace(originalUri ?? "/after"),
});
const status = document.querySelector("#status");
// The login page belongs to the identity provider now: the form becomes a button.
document.querySelector("#signin").addEventListener("click", () => {
oktaAuth.signInWithRedirect({ originalUri: location.pathname });
});
// Runs on the redirect URI: exchanges the code, then calls restoreOriginalUri.
if (oktaAuth.isLoginRedirect()) await oktaAuth.handleLoginRedirect();
// Keeps tokens fresh: when one expires, the SDK uses the refresh token.
await oktaAuth.start();
// Renew on demand. Also uses the refresh token.
document.querySelector("#renew").addEventListener("click", async () => {
await oktaAuth.tokenManager.renew("accessToken");
status.textContent = "renewed";
});
// Is the person signed in to this app? Answered from its own tokens.
if (await oktaAuth.isAuthenticated()) {
const { email } = await oktaAuth.token.getUserInfo();
status.textContent = `signed in: ${email}`;
} else {
status.textContent = "signed out";
}
document.querySelector("#signout").addEventListener("click", () => {
oktaAuth.signOut();
});Both files import issuer and clientId from an env.js of your own.
Renew with a refresh token
Add offline_access to scopes, as above. Nothing else changes: when a refresh token is stored, tokenManager.renew and the SDK's automatic renewal after start() post it to the token endpoint instead of opening an iframe. Each renewal returns a new refresh token.
Refreshing re-checks the app's policies, so someone who has lost access is refused. See OIDC apps.
Replace session.exists
session.exists asked whether the person has a session at the identity provider. Choose by what you actually need to know:
- Is this person signed in to my app? Use
oktaAuth.isAuthenticated(), as above. It reads the app's own tokens. - Can I sign them in without showing a login page? Call
signInWithRedirect. With a live IdRa session the person comes straight back, signed in. Without one, they see the login page.
Don't read false from session.exists as "signed out".
Sign out
signOut() works with its defaults: the SDK revokes the refresh token and then the access token, then goes to the logout redirect. The refresh token is refused from then on, and so is an opaque access token.
A JWT access token stays valid until it expires. Its revoke call answers 200, but nothing can end it, so keep its lifetime short.
Request an API resource
Access tokens are opaque unless the sign-in asks for an API resource, and verifyAccessToken can only read a JWT. In the console:
- Create the API under Security → API. Its identifier is the audience you verify against.
- Add your API scope to its allowed scopes. Also add
openidandoffline_access. A browser sign-in loses any scope the API doesn't allow: withoutopenid, the token response has no ID token, andhandleLoginRedirectfails withAuthSdkError: Unable to parse OAuth flow response: response type "id_token" was requested but "id_token" was not returned. - Link the client, and add an access rule.
Then pass resource when you sign in. Setting extraParams in the OktaAuth constructor doesn't send it; pass it to signInWithRedirect:
import { OktaAuth } from "@okta/okta-auth-js";
import { issuer, clientId, apiIdentifier, apiScope } from "./env.js";
const oktaAuth = new OktaAuth({
issuer,
clientId,
redirectUri: `${location.origin}/api-app/callback`,
postLogoutRedirectUri: `${location.origin}/after`,
scopes: ["openid", apiScope, "offline_access"],
restoreOriginalUri: (_, originalUri) => location.replace(originalUri ?? "/api-app"),
});
document.querySelector("#signin").addEventListener("click", () => {
oktaAuth.signInWithRedirect({
originalUri: location.pathname,
// Without a resource the access token is opaque. With one it's a JWT, whose audience is the identifier.
extraParams: { resource: apiIdentifier },
});
});
if (oktaAuth.isLoginRedirect()) await oktaAuth.handleLoginRedirect();
document.querySelector("#renew").addEventListener("click", async () => {
await oktaAuth.tokenManager.renew("accessToken");
document.querySelector("#status").textContent = "renewed";
});
// A JWT access token can't be revoked: it stays valid until it expires. Keep its lifetime short.
document.querySelector("#signout").addEventListener("click", () => {
oktaAuth.signOut();
});The renewed token is a JWT for the same API too.
jwt-verifier configuration
On your API's server, set the issuer, and pass the API's identifier as the audience:
import OktaJwtVerifier from "@okta/jwt-verifier";
/** On your API's server. `http` issuers are refused, so a local run passes `testing`; leave it out in production. */
export function verifier(issuer, testing) {
const jwt = new OktaJwtVerifier({ issuer, ...(testing && { testing }) });
return (accessToken, audience) => jwt.verifyAccessToken(accessToken, audience);
}const verify = verifier("https://<your-org>.idra.team/api/auth");
const { claims } = await verify(accessToken, "https://api.example.com");A token for another audience is refused. With openid in the scopes, aud is an array of the identifier and the UserInfo URL. To run against a local http issuer, pass { disableHttpsCheck: true } as testing.
What this was tested on
We ran the code above on 2026-10-31 with @okta/okta-auth-js 8.0.1 and @okta/jwt-verifier 4.0.2, bundled with bun build, in one headless Chromium driven by Playwright 1.64.1. The app ran on 127.0.0.1 against a seeded in-memory IdRa on localhost, so the two were on different sites. The person signed in with a password and TOTP. Automatic renewal was triggered by moving the page's clock past the token's expiry, not by waiting.
Not run: https, the hosted idra.team service, a real Okta tenant (before.js has never signed in anywhere), other browsers, custom domains, and frameworks such as @okta/okta-react. An app and IdRa on one registrable domain weren't tried. The reasons given for the refusals come from IdRa's cookie and frame settings; each error above was observed.
The rows for token.verify, exchangeCodeForTokens, token.revoke, signOut with no ID token and token.introspect come from the earlier SDK run. tokenManager.renew with no refresh token falls back to getWithoutPrompt in the SDK's source; we ran getWithoutPrompt directly. The test fails if the code blocks on this page differ from the code it runs.