---
title: "Issue JWT access tokens"
url: "https://effect-auth.itsbroly.com/recipes/issue-jwt-access-tokens/"
description: "Mint short-lived bearer tokens from an authenticated application session."
---



## Goal [#goal]

Exchange an **already authenticated** application context for a short-lived JWT that another API can verify. The session cookie and JWT are different credentials:

| Credential     | Job                                                                                                                   |
| -------------- | --------------------------------------------------------------------------------------------------------------------- |
| Session cookie | Authenticates the browser to your application; opaque, `HttpOnly`, and backed by server state.                        |
| Access token   | Authorizes a caller to a specific API; sent explicitly as `Authorization: Bearer …` and valid until its short expiry. |

Issuing a JWT does not sign a user in. Read [Sessions](/concepts/sessions/) first.

## Flow [#flow]

```text
browser + session cookie
        |
        v
authenticated application endpoint
        |  userId + current app permissions
        v
     JwtIssuer ---- RS256 private key
        |
        +---- short-lived access token ----> API
                                             |
                              JwtVerifier + public key
                                             |
                                      app authorization
```

## Prerequisites [#prerequisites]

* A handler that has already validated the session and produced a trusted `userId`.
* An RS256 key pair loaded from validated secret/configuration input. Never place the private JWK in source control.
* Stable issuer and audience identifiers. Effect Auth does not read JWT environment variables or supply these values; see [Configuration](/reference/configuration/).

## Minimal implementation [#minimal-implementation]

Build issuer and verifier services from one key set. In a split deployment, give the issuer both JWKs and give verifiers only the public JWK.

```ts title="access-tokens.ts"
import { Duration, Effect, Layer, Redacted } from "effect";
import {
  JwtIssuer,
  JwtKeyId,
  JwtKeysMemoryLive,
  JwtVerifier,
  JwtWebCryptoRs256SignatureLive,
  JwtFromSignatureLive,
  type JwtPrivateJwk,
  type JwtPublicJwk,
} from "@effect-auth/core/Jwt";

export const AccessTokenLive = (keys: {
  readonly privateJwk: JwtPrivateJwk;
  readonly publicJwk: JwtPublicJwk;
}) =>
  JwtFromSignatureLive.pipe(
    Layer.provide(JwtWebCryptoRs256SignatureLive()),
    Layer.provide(
      JwtKeysMemoryLive([
        {
          id: JwtKeyId("access-token-rs256-2026-07"),
          alg: "RS256",
          status: "active",
          ...keys,
        },
      ])
    )
  );

// Call only after session authentication. `userId` is trusted server context.
export const issueAccessToken = (userId: string) =>
  JwtIssuer.use((jwt) =>
    jwt.issue({
      alg: "RS256",
      issuer: "https://auth.example.com",
      subject: userId,
      audience: "projects-api",
      expiresIn: Duration.minutes(10),
      claims: { scope: "projects:read" },
    })
  );

export const verifyAccessToken = (rawToken: string) =>
  JwtVerifier.use((jwt) =>
    jwt.verify({
      token: Redacted.make(rawToken),
      issuer: "https://auth.example.com",
      audience: "projects-api",
      clockTolerance: Duration.seconds(30),
    })
  );
```

Return `Redacted.value(issued.token)` to the authenticated caller. Do not put it in a cookie or log it. At the API, reject `valid: false`; for `valid: true`, treat `sub` and `scope` only as signed input to **application-owned authorization**. Your application decides what scopes mean and whether the user still has access. Follow [Protect an API endpoint](/recipes/protect-api-endpoint/) for bearer extraction and endpoint integration.

## Test it [#test-it]

Provide `AccessTokenLive(testKeys)` to a test program, issue a token, then verify it:

```ts
const program = Effect.gen(function* () {
  const issued = yield* issueAccessToken("user-123");
  const result = yield* verifyAccessToken(Redacted.value(issued.token));

  if (!result.valid) throw new Error(result.reason);
  return result.claims;
}).pipe(Effect.provide(AccessTokenLive(testKeys)));
```

Also test a changed signature, wrong issuer, wrong audience, and verification after expiry. Assert authorization separately; a valid signature is not permission by itself.

## Production notes [#production-notes]

* Keep TTLs short and always verify signature, `iss`, `aud`, and time claims. Add `jti` only when you need token identity.
* Rotate keys with unique `kid` values. Keep retired public keys available until every token they signed has expired; disable compromised keys.
* `Jwks.document()` exposes only public JWK material. Publish it with the standard `JwtDiscoveryHttpOperations.jwks` operation so remote verifiers can discover rotation keys.
* Stateless verification does not consult revocation. If immediate invalidation is required, issue `jti`, persist revocations, and use `JwtRevocation.introspect` (or the standard JWT introspection operation) on the authorization path. That adds storage and availability costs; short TTLs are usually simpler.
* The standard JWT HTTP operations are `introspect` and `revoke`; they do not mint tokens. See the [JWT operations reference](/reference/http-operations/#tokens-and-federation).

