---
title: Legacy Flow
description: Guide for implementing custom JWT authentication between microservices using SuperTokens Core.
sidebar:
  order: 3
---

## Overview

Use the [OAuth2 Client Credentials Flow](/authentication/m2m/client-credentials) when it is available. It gives each
client an identity and uses the standard OAuth2 token and scope model.

This legacy flow is a custom bearer-token scheme for deployments that cannot use client credentials. A service with
access to the SuperTokens Core JWT API can mint a token containing arbitrary claims. Consequently, a `source` or
`sub` claim proves only that a caller with signing access asserted that value; it does not independently prove which
service made the request.

The flow is:

1. **Service M1 requests a short-lived JWT from SuperTokens Core**

2. **M1 sends the JWT to M2 in the Authorization header**

3. **M2 verifies the signature and every required claim before authorizing the request**

:::warning[Security boundary]
Anyone who can call the Core JWT API can mint any service identity or permission accepted by this scheme. Restrict
Core access with network controls and an API key, store the API key in a secrets manager, and monitor issuance. Multiple
Core API keys simplify secret rotation and may help attribute Core requests, but the issued JWT does not identify which
API key was used. Multiple keys therefore do not create cryptographic service identities or authorization boundaries.
:::

For stronger isolation, use client credentials or separate trust domains. Deploying separate Cores can create separate
signing domains, but it adds operational cost and does not turn a shared Core API key into service identity.

## Token policy

For every token:

- Use a short validity appropriate to the request path. The examples below use five minutes.
- Use dynamic signing keys. Dynamic keys rotate every 168 hours (one week) by default unless the Core configuration
  changes `access_token_dynamic_signing_key_update_interval`.
- Require an exact issuer (`iss`), audience (`aud`), subject/service identity (`sub`), source, token type, permissions,
  and expiration (`exp`) at the receiving service.
- Grant only the permissions needed by the target API. Do not treat successful signature verification as authorization.
- Fetch keys from JWKS and support key rotation. Do not embed a public key in the application.

The JWT recipe defaults to a 100-year validity and a static signing key when those arguments are omitted. Those defaults
are unsuitable for bearer credentials. Static keys do not rotate. Always pass a short validity and explicitly select
the dynamic signing key as shown below.

## 1. Initialize the JWT recipe

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import supertokens from "supertokens-node";
import jwt from "supertokens-node/recipe/jwt";

supertokens.init({
  appInfo: {
    apiDomain: "https://auth.example.com",
    appName: "service-auth",
    websiteDomain: "https://example.com",
  },
  supertokens: {
    connectionURI: "...",
    apiKey: "...",
  },
  recipeList: [jwt.init()],
});
```
</Tab>
<Tab title="Go" value="go">
```go
import (
	"github.com/supertokens/supertokens-golang/recipe/jwt"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
	supertokens.Init(supertokens.TypeInput{
		AppInfo: supertokens.AppInfo{
			AppName:      "service-auth",
			WebsiteDomain: "https://example.com",
			APIDomain:     "https://auth.example.com",
		},
		Supertokens: &supertokens.ConnectionInfo{
			ConnectionURI: "...",
			APIKey:        "...",
		},
		RecipeList: []supertokens.Recipe{
			jwt.Init(nil),
		},
	})
}
```
</Tab>
<Tab title="Python" value="python">
```python
from supertokens_python import InputAppInfo, SupertokensConfig, init
from supertokens_python.recipe import jwt

init(
    app_info=InputAppInfo(
        app_name="service-auth",
        api_domain="https://auth.example.com",
        website_domain="https://example.com",
    ),
    supertokens_config=SupertokensConfig(
        connection_uri="...",
        api_key="...",
    ),
    framework="django",
    recipe_list=[jwt.init()],
)
```
</Tab>
</CodeGroup>

The `apiDomain`/`api_domain`/`APIDomain` value becomes the JWT issuer domain and must be the domain that serves the JWKS
endpoint. If this process initializes no other recipe, `appName` and `websiteDomain` do not affect this flow.

## 2. Create a short-lived JWT

Use a fixed schema rather than accepting arbitrary claims from request input. This example identifies `M1`, limits the
token to `M2`, and grants one permission.

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import jwt from "supertokens-node/recipe/jwt";

const response = await jwt.createJWT(
  {
    iss: "https://auth.example.com",
    aud: "service-m2",
    sub: "service-m1",
    source: "microservice",
    token_type: "service_access",
    permissions: ["comments:write"],
  },
  300,
  false,
);

if (response.status !== "OK") {
  throw new Error("JWT creation failed");
}

const accessToken = response.jwt;
```
</Tab>
<Tab title="Go" value="go">
```go
validitySeconds := uint64(300)
useStaticSigningKey := false

response, err := jwt.CreateJWT(map[string]interface{}{
	"iss":         "https://auth.example.com",
	"aud":         "service-m2",
	"sub":         "service-m1",
	"source":      "microservice",
	"token_type":  "service_access",
	"permissions": []string{"comments:write"},
}, &validitySeconds, &useStaticSigningKey)
if err != nil {
	return err
}

accessToken := response.OK.Jwt
```
</Tab>
<Tab title="Python" value="python">
<DependentContent group="python-io-style" label="I/O style">
<ContentOption title="Asyncio" value="asyncio">
```python
from supertokens_python.recipe.jwt import asyncio
from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult

response = await asyncio.create_jwt(
    {
        "iss": "https://auth.example.com",
        "aud": "service-m2",
        "sub": "service-m1",
        "source": "microservice",
        "token_type": "service_access",
        "permissions": ["comments:write"],
    },
    validity_seconds=300,
    use_static_signing_key=False,
)
if not isinstance(response, CreateJwtOkResult):
    raise RuntimeError("JWT creation failed")

access_token = response.jwt
```
</ContentOption>
<ContentOption title="Syncio" value="syncio">
```python
from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult
from supertokens_python.recipe.jwt.syncio import create_jwt

response = create_jwt(
    {
        "iss": "https://auth.example.com",
        "aud": "service-m2",
        "sub": "service-m1",
        "source": "microservice",
        "token_type": "service_access",
        "permissions": ["comments:write"],
    },
    validity_seconds=300,
    use_static_signing_key=False,
)
if not isinstance(response, CreateJwtOkResult):
    raise RuntimeError("JWT creation failed")

access_token = response.jwt
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

Prefer the backend SDK. It avoids manually constructing the Core request and keeps the API key out of command-line
arguments. If operational tooling must call the released Core API directly, provide the URL and headers through an
owner-readable curl config file (`0600`) populated by your secret tooling. Provide the request body over standard input:

```bash
curl --config /run/secrets/supertokens-curl.conf --data-binary @- <<'JSON'
{
  "payload": {
    "iss": "https://auth.example.com",
    "aud": "service-m2",
    "sub": "service-m1",
    "source": "microservice",
    "token_type": "service_access",
    "permissions": ["comments:write"]
  },
  "useStaticSigningKey": false,
  "algorithm": "RS256",
  "jwksDomain": "https://auth.example.com",
  "validity": 300
}
JSON
```

Configure that file with the `/recipe/jwt` URL, `POST` method, `rid: jwt`, `Content-Type: application/json`, and
`api-key` header. Do not put the API key in command-line arguments, shell history, environment dumps, or generated logs.
Disable shell tracing such as `set -x` around secret handling, restrict access to the config file, and remove temporary
copies immediately after use.

Keep the token in memory only as long as needed. Send it as `Authorization: Bearer <token>` over TLS. Never log the
token or place it in a URL, source file, or long-lived configuration value.

## 3. Verify and authorize the JWT

The JWKS endpoint is:

```text
<YOUR_API_DOMAIN><API_BASE_PATH>/jwt/jwks.json
```

With the default API base path, this is `https://auth.example.com/auth/jwt/jwks.json`. Configure a maintained JWT
library to fetch and cache this JWKS, honor its cache behavior, and refetch when it encounters an unknown `kid`. Dynamic
keys rotate every week by default. Static keys may also appear in JWKS, but they do not rotate and must not be selected
or hardcoded for this flow.

Do not trust decoded data until the verification library reports success. Verification must:

1. Allow only `RS256`; reject an unexpected or missing `alg` or `kid`.
2. Verify the signature with the JWKS key selected by `kid`.
3. Reject every library error before reading claims. This includes an invalid signature, expired token, malformed token,
   unknown key, and issuer or audience mismatch.
4. Require `exp` and reject expired tokens. Do not disable expiry verification or add an unbounded clock tolerance.
5. Require exact expected values for `iss`, `aud`, `source`, and `token_type`.
6. Require an approved `sub` service identity and every permission needed by the endpoint.

For the example above, `M2` must require:

| Claim | Required value |
| --- | --- |
| `iss` | `https://auth.example.com` |
| `aud` | `service-m2` |
| `sub` | An approved calling service, such as `service-m1` |
| `source` | `microservice` |
| `token_type` | `service_access` |
| `permissions` | Includes the endpoint's required permission |
| `exp` | Present and in the future |

Return `401 Unauthorized` when authentication fails. Return `403 Forbidden` when the token is valid but its service or
permissions do not authorize the operation. Do not reveal signature, key, or claim-validation details to the caller.

### Idempotent writes and replay

A valid bearer token can be replayed until it expires. For non-idempotent writes, require a caller-generated
`Idempotency-Key` scoped to the authenticated service and operation. Atomically reserve the key in shared durable storage
before the side effect, and return the stored result for an identical retry. Reject reuse with different request data,
and retain the record for a bounded period covering the retry window.

If a token must be accepted only once, add a server-generated, unpredictable, unique `jti` claim when creating it. Before
the side effect, atomically insert `(iss, sub, jti)` into replay storage shared by every service instance; reject the
request if it already exists. Keep the entry until at least `exp` plus the permitted clock skew. Couple replay reservation
and the write transaction, or use a transactional outbox, so a crash cannot consume the token without a defined result.
An `Idempotency-Key` or `jti` is not a substitute for signature, claim, identity, and permission verification.

### APIs that accept frontend sessions and service tokens

Prefer separate endpoints or an explicit authentication policy for frontend sessions and service tokens. If one endpoint
must accept both, verify each credential only with its intended verifier and apply a separate authorization policy. Never
fall back to trusting decoded JWT claims after either verifier returns an error. A malformed, expired, or invalid token
must not be downgraded into another authentication path.

Use the backend SDK's `getSession` function for frontend session verification. Use the bounded JWKS procedure above for
legacy service tokens. Accept the request only after one verifier succeeds and the corresponding identity and permission
checks pass.

## Compromise response

- **Core API key compromised:** revoke and replace it, stop token issuance while investigating, and wait at least the
  maximum token lifetime before considering previously minted tokens expired. Review issuance and service logs.
- **Dynamic signing key compromised:** rotate the signing material, prevent further issuance, and reject affected keys.
  Network restrictions can reduce exposure but do not make forged tokens safe.
- **Bearer token compromised:** revoke or disable the caller where possible and let the short expiry bound exposure. If
  immediate revocation is required, use an introspected or stateful design rather than this self-contained legacy flow.
