# Move an app that uses Okta's SDKs (https://docs.idra.zone/docs/migrate-okta-sdk)

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](#sign-out) |
| `verifyIdToken` | Works | | Change the issuer. See [jwt-verifier](#jwt-verifier-configuration) for `http` |
| `signInWithCredentials` | Refused | `AuthApiError: Failed to fetch`, with a CORS error in the console | [Sign in by redirect](#sign-in-by-redirect) |
| `token.getWithoutPrompt`, or `tokenManager.renew` with no refresh token | Refused | `AuthSdkError: OAuth flow timed out` | [Renew with a refresh token](#renew-with-a-refresh-token) |
| `session.exists` | Always `false`, with no error, even with a live IdRa session | `false` | [Replace session.exists](#replace-sessionexists) |
| `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](#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:

```js title="before.js"
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:

```js title="after.js"
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](https://docs.idra.zone/docs/admin/oidc).

## 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:

1. Create the API under **Security → API**. Its identifier is the audience you verify against.
2. Add your API scope to its allowed scopes. **Also add `openid` and `offline_access`.** A browser sign-in loses any scope the API doesn't allow: without `openid`, the token response has no ID token, and `handleLoginRedirect` fails with `AuthSdkError: Unable to parse OAuth flow response: response type "id_token" was requested but "id_token" was not returned.`
3. 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`:

```js title="with-api.js"
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:

```js title="verify.mjs"
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);
}
```

```js
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](https://docs.idra.zone/docs/migrate-from-okta#okta-sdk-compatibility). `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.