JWT + Okta Auth Integration: A Short Practical Guide for Frontend and Backend Developers

A compact, skimmable explainer on JWT (RFC 7519), OAuth 2.0/OpenID Connect, and Okta — and how a web-app (frontend) developer and a REST API (backend) developer should integrate JWT and Okta auth into their app.

Research date: Aug 16, 2026. Every factual claim is cited inline to an authoritative source (IETF RFCs or official Okta docs). This is a practical “one screen per concept” explainer for the two people who actually wire this up: the frontend/web-app developer and the backend REST API developer.


TL;DR

  • JWT (RFC 7519) is a signed, not encrypted token. Anyone can decode the payload; only the signature proves who issued it. Verify the signature and never put secrets in it.
  • OAuth 2.0 / OIDC is how you obtain tokens. The access token authorizes API calls; the ID token proves who the user is. They are not interchangeable.
  • Okta is the identity provider / authorization server. It runs the OIDC endpoints, signs the tokens with RS256, and publishes its public keys in a JWKS so your backend can verify tokens locally.
  • Frontend job: do the login redirect dance (Authorization Code + PKCE), keep tokens in memory, send Authorization: Bearer <access_token>.
  • Backend job: verify every access token against Okta’s JWKS (signature + iss/aud/exp/iat), treat it as stateless, and never ship a refresh token to the API.

1. JWT in one screen

A JWT is “a compact, URL-safe means of representing claims to be transferred between two parties,” where claims are a signed/encrypted JSON object (https://www.rfc-editor.org/rfc/rfc7519.html). It looks like header.payload.signature — three base64url segments joined by dots (RFC 7519 §3.1):

eyJhbGciOiJSUzI1NiIsImtpZCI6InNrLTEifQ.eyJpc3MiOiJodHRwczovL3lv...
PartContentsExample
Headersigning algorithm + key id{"alg":"RS256","kid":"sk-1"}
Payloadthe claims (JSON){"iss":"...","sub":"user-123","aud":"my-api","exp":...}
Signaturecrypto over header.payloadBase64Url(RS256(header.payload, privKey))

HS256 vs RS256 (RFC 7518 §3.2, §3.3):

HS256 (HMAC-SHA256)RS256 (RSA-SHA256)
Key typeone shared symmetric secretasymmetric public/private key pair
Who holds the secretboth signer and verifiersigner keeps private; verifiers get public
Use casesame trusted service signs and verifiesidentity provider signs, many resource servers verify
Okta’s choice—RS256 (https://developer.okta.com/docs/guides/validate-access-tokens/main/)

Claims that matter (RFC 7519 §4.1):

ClaimMeaningCheck
ississuer — who signed thismust match your identity provider
subsubject — the useruse as the user id
audaudience — who it’s formust contain your API’s audience
expexpiration (NumericDate)must be in the future
iatissued-at timesanity check
nbfnot-beforereject if before this (rarely used)
jtiunique token idreplay/revocation bookkeeping

Why stateless: the token self-contains the authorization info in a verifiable form, so a resource server can validate it without contacting the issuer on every request (RFC 6749 §1.4: a token “may self-contain the authorization information in a verifiable manner (i.e., a token string consisting of some data and a signature)”). That’s the whole point — local verification, no DB or network lookup per call.

The caveats (read these twice):

  • Signed ≠ encrypted. The payload is just base64url JSON — anyone can decode it. RFC 7519 §12: if a JWT may carry privacy-sensitive info, measures MUST be taken to prevent disclosure; the simplest is to leave sensitive info out. Never put secrets (passwords, API keys, tokens) in the payload.
  • Verify the signature before trusting anything. RFC 7519 §11.1: contents “cannot be relied upon in a trust decision unless its contents have been cryptographically secured.” RFC 8725 §3.10 (“Do Not Trust Received Claims”) — a JWT is a claim by the issuer, not a fact.
  • Algorithm confusion is a real attack. Attackers have swapped RS256 → HS256 and tricked libraries into verifying an RSA-signed token as an HMAC with the public key as the HMAC secret, and flipped alg to none (RFC 8725 §2.1, https://www.rfc-editor.org/rfc/rfc8725.html). Mitigation: pin the allowed algorithms (RFC 8725 §3.1) and reject anything else.

2. OAuth 2.0 / OIDC in one screen

OAuth 2.0 defines four roles (RFC 6749 §1.1):

RoleWho it is in your stack
Resource ownerthe end user
Clientyour web app / SPA
Authorization serverthe identity provider — Okta
Resource serveryour backend REST API

OIDC (OpenID Connect) extends OAuth 2.0 with authentication — it standardizes user sign-in and adds the ID token, a JWT about the authentication event and the user (https://developer.okta.com/docs/concepts/oauth-openid/).

ID token vs access token — never confuse them:

ID tokenAccess token
Purposeprove who the user is (authentication)authorize access to an API (authorization)
audyour app’s client IDyour API’s audience
Who uses itthe client app (frontend)the resource server (backend)
Okta guidance“use only the access token to grant access, and not the ID token” (https://developer.okta.com/docs/guides/validate-access-tokens/main/)backend authorizes requests from this

The flow you want for a browser app: Authorization Code + PKCE.

  1. Frontend redirects the browser to Okta’s /authorize endpoint with response_type=code + a PKCE code_challenge.
  2. User authenticates on Okta-hosted pages.
  3. Okta redirects back with an authorization code.
  4. Frontend (or SDK) exchanges the code + code_verifier at /token for access/ID/refresh tokens.

Why this flow: the implicit grant (tokens issued straight into the browser redirect) is deprecated by current best practice — RFC 9700 §2.1.2 says clients “SHOULD NOT use the implicit grant” because access tokens leak into URLs, browser history, and Referer headers; use response_type=code instead (https://www.rfc-editor.org/rfc/rfc9700.html). Public clients MUST use PKCE (RFC 9700 §2.1.1, and RFC 7636 defines PKCE). PKCE + a fresh state/nonce also covers the login-flow CSRF risk (RFC 9700 §2.1). Okta recommends Authorization Code + PKCE for SPAs, web apps, and native apps (https://developer.okta.com/docs/concepts/oauth-openid/).


3. Okta in one screen

What Okta is: the identity provider — an OAuth 2.0 authorization server and OIDC provider (“Okta is your authorization server”). Each authorization server has a unique issuer URI and its own signing key, keeping security domains apart (https://developer.okta.com/docs/concepts/oauth-openid/).

What Okta issues:

Endpoints (paths are relative to your issuer, e.g. https://{yourOktaDomain}/oauth2/default):

EndpointPurpose
/.well-known/oauth-authorization-serverOIDC discovery document — lists everything below incl. jwks_uri (https://developer.okta.com/docs/guides/validate-access-tokens/main/)
/v1/authorizestarts login, returns the code
/v1/tokencode → tokens; refresh tokens; revocation
/v1/userinfoget the user’s profile with the access token
/v1/keysJWKS — Okta’s public signing keys for signature verification (https://developer.okta.com/docs/reference/api/oidc/)

Setup in the Okta dashboard (5 minutes):

  1. Admin Console → Applications → Create App Integration → OIDC - OpenID Connect.
  2. Pick Single-Page Application (or Web). Note: “If you choose an inappropriate app type, it can break the sign-in or sign-out flows” — public clients have no client secret (https://developer.okta.com/docs/guides/sign-into-spa-redirect/angular/main/).
  3. Grant types: Authorization Code and Refresh Token (this enables Authorization Code + PKCE and token refresh). Refresh-token rotation is the default for SPAs.
  4. Register Sign-in redirect URIs (e.g. http://localhost:4200/login/callback) and sign-out redirect URIs.
  5. Copy Client ID (General tab) and Issuer (Security → API → Authorization Servers → Issuer URI).
  6. Add your app origin under Security → API → Trusted Origins to allow CORS/Okta API access (https://developer.okta.com/docs/guides/sign-into-spa-redirect/angular/main/).

Token lifetimes to know: on the org authorization server, access & ID tokens are hard-coded to 60 minutes and refresh tokens to 90 days; on a custom authorization server, access tokens are configurable between 5 minutes and 24 hours (https://developer.okta.com/docs/reference/api/oidc/).


4. Web app developer (frontend): integrate Okta login

Use an SDK. Okta ships @okta/okta-auth-js plus framework wrappers (@okta/okta-angular, etc.) and the hosted Sign-In Widget. The redirect model — frontend delegates the whole sign-in UI to Okta-hosted pages — is the recommended, stronger default (https://developer.okta.com/docs/concepts/oauth-openid/).

Configure with the three values from Step 3:

const oktaAuth = new OktaAuth({
  issuer: 'https://{yourOktaDomain}/oauth2/default',
  clientId: '{yourClientId}',
  redirectUri: window.location.origin + '/login/callback',
  scopes: ['openid', 'profile', 'offline_access']   // offline_access → refresh token
});

The flow you implement:

  1. User clicks sign in → await oktaAuth.signInWithRedirect() → browser goes to Okta’s /authorize. The SDK handles Authorization Code + PKCE by default (pkce: true is the default in @okta/okta-auth-js) and generates a fresh state for CSRF protection (https://github.com/okta/okta-auth-js#pkce-oauth-20-flow).
  2. Okta redirects back to your /login/callback route → SDK exchanges the code for tokens.
  3. Attach the access token to API calls: Authorization: Bearer <access_token>. Okta’s own sample interceptor only adds the header for your allowed origins — good hygiene, since your SPA will also load third-party assets (https://developer.okta.com/docs/guides/sign-into-spa-redirect/angular/main/).
// minimal interceptor sketch (Angular/React analogous)
fetch('/api/me', {
  headers: { 'Authorization': `Bearer ${oktaAuth.getAccessToken()}` }
});

Where to keep tokens — this is a security decision:

  • A browser cannot keep a true secret. Okta’s guidance is blunt: “Long-lived refresh tokens aren’t suitable for clients such as single-page apps (SPAs)… there isn’t a way to safely store persistent refresh tokens in a browser” (https://developer.okta.com/docs/guides/refresh-tokens/main/).
  • So for a SPA: keep the access token in memory (not localStorage). @okta/okta-auth-js supports storage types memory, sessionStorage, localStorage, and cookie — memory survives no page reload but exposes nothing to XSS, whereas anything in localStorage is readable by any script running on your origin (https://github.com/okta/okta-auth-js#storagetypes).
  • Combine short-lived access tokens with refresh token rotation (the default for Okta SPAs) instead of a persistent refresh token: each refresh returns a new refresh token and Okta detects reuse of an old one, revoking everything issued since authentication (https://developer.okta.com/docs/guides/refresh-tokens/main/).
  • Don’t console.log tokens, don’t put them in query strings, and clear them on sign-out (oktaAuth.signOut()).

5. Backend REST API developer: verify the JWT on every request

Your API is the resource server. You never see the user’s password, never run the OAuth dance, and never handle refresh tokens. You verify the access token in the Authorization: Bearer header.

The verification checklist (per Okta’s “Validate Access Tokens” guide, https://developer.okta.com/docs/guides/validate-access-tokens/main/):

  1. Fetch and cache the JWKS from Okta’s discovery document (issuer + "/.well-known/oauth-authorization-server", then jwks_uri). Cache it and re-fetch on a schedule — Okta rotates signing keys regularly (https://developer.okta.com/docs/reference/api/oidc/).
  2. Verify the signature against the JWK selected by the token’s kid. Okta signs with RS256.
  3. Verify iss equals your Okta issuer URL.
  4. Verify aud equals the audience you configured for this API in the Okta authorization server (for a custom AS this is e.g. api://default).
  5. Verify exp (and iat/nbf) — reject expired tokens; allow small clock skew (Okta recommends ≤ 2 minutes).
  6. Pin alg = RS256 and reject anything else (RFC 8725 §3.1).
  7. Use the verified claims (sub, scp) for identity and authorization — don’t trust unverified token bytes.

Middleware pattern (pseudocode, language-agnostic):

// Runs before every protected route — never on a per-request AS call.
async function authMiddleware(req, res, next) {
  const jwt = req.headers.authorization?.replace(/^Bearer /, '');
  const keys = await jwksCache.get();            // cached JWKS from Okta
  const payload = verifyJwt(jwt, { keys });      // alg pin + signature
  if (payload && payload.iss === ISSUER          // Okta issuer URL
             && payload.aud.includes(API_AUD)    // your API audience
             && payload.exp > now) {             // lifetime
    req.user = { sub: payload.sub, scopes: payload.scp };
    return next();
  }
  return res.status(401).json({ error: 'invalid token' });
}

Okta publishes JWT-verification middleware for popular frameworks (e.g. the Okta.AspNetCore package configures your API’s authentication against Okta and maps verified claims into the request principal — https://developer.okta.com/docs/guides/protect-your-api/main/). Use the official middleware for your stack rather than hand-rolling crypto.

Remote validation (optional): if you must guarantee a token hasn’t been revoked, Okta also offers the /introspect endpoint — it’s a network call, “slower,” but confirms the token is still active (https://developer.okta.com/docs/guides/validate-access-tokens/main/). Use local JWT verification by default; introspection only where you need instant revocation.

Refresh tokens — your API’s rules:

  • The refresh token goes only to Okta’s /token endpoint, never to your API. RFC 6749 §1.5: refresh tokens “are never sent to resource servers.” If your API ever receives one, that’s a client bug — reject it.
  • Your API just returns 401 on an expired access token; the client refreshes via Okta. Okta’s rotation default makes stolen refresh tokens self-destructing via reuse detection (https://developer.okta.com/docs/guides/refresh-tokens/main/).
  • For logout / revocation: revoke the client’s tokens at Okta (the OAuth 2.0 revocation endpoint, RFC 7009, is part of Okta’s token endpoints) and end the Okta session so the ID/access/refresh set can’t be re-minted. Okta handles single logout across your OIDC app when the user signs out.

CORS/CSRF notes:

  • CORS: enable CORS on your API for the exact origins of your SPA, not *. Okta’s API guide ships an “AllowAll” sample but warns to restrict it in production (https://developer.okta.com/docs/guides/protect-your-api/main/). Okta likewise wants Trusted Origins listed, not open CORS (https://developer.okta.com/docs/guides/sign-into-spa-redirect/angular/main/).
  • CSRF: a bearer token in an Authorization header is not sent automatically by the browser, so a cross-site POST can’t piggyback it — the classic CSRF risk lives in the login flow, which PKCE/state/nonce covers (RFC 9700 §2.1). Keep tokens in headers, never in cookies you’re auto-sending, and you stay out of CSRF territory.
  • Always use TLS/HTTPS everywhere — the token is only as safe as the channel (RFC 9700 §2.6).

6. Putting it together — checklist

Both roles, once:

Frontend developer:

  • Use @okta/okta-auth-js (+ framework wrapper) or the hosted Sign-In Widget.
  • Authorization Code + PKCE flow (SDK default), fresh state per request.
  • Keep access tokens in memory, not localStorage; rely on refresh token rotation, not persistent refresh tokens (https://developer.okta.com/docs/guides/refresh-tokens/main/).
  • Send Authorization: Bearer <access_token> only to your allowed API origins.
  • Sign-out clears tokens and ends the Okta session.

Backend developer:

  • Verify every protected request: signature vs cached JWKS, alg=RS256, iss, aud, exp (https://developer.okta.com/docs/guides/validate-access-tokens/main/).
  • Use Okta/official JWT middleware for your framework; don’t hand-roll crypto.
  • Authorize from verified claims (sub, scp) only; reject refresh tokens at the API.
  • Return 401 on invalid/expired tokens; let the client refresh via Okta.
  • CORS restricted to your known SPA origins; HTTPS everywhere.

Sources