Skip to content

Token signing

Choose how a client signs its ID token and its access token, when HS256 with the client secret makes sense, and how apps, APIs and MCP servers verify each one.

Every token Authio issues is a signed JWT. A client has two signing settings: one for what only your app reads (the ID token, the signed UserInfo response and the back-channel logout token) and one for the access token your APIs read. The refresh token follows neither: Authio always signs it with its own key. For tokens nobody outside the app should read, see Encrypted tokens.

The two settings

In Clients, open the client and go to Edit → Security → Token Configuration. Each setting takes RS, PS or ES (asymmetric, verified with the realm’s public keys) or HS (HMAC, keyed with the client secret), in 256, 384 or 512 bits.

ID token & userinfo signatureAccess token signature
SignsThe ID token, the signed UserInfo response and the back-channel logout tokenThe access token
Read byYour app onlyYour APIs: the client’s own API, resource servers, MCP servers
With RS, PS or ESThe app verifies it against the realm’s JWKSAny API verifies it against the realm’s JWKS
With HSThe app verifies it with its own client secretOnly who holds the client secret validates it locally; every other API validates it by introspection
Left out when the client is createdThe family’s default: PS256 for RSA, ES256 for ECDsa, HS256 for HMACThe ID token’s algorithm when it is asymmetric, otherwise PS256

The refresh token is always signed by Authio

Only Authio reads refresh tokens, so they are always signed with the key Authio holds for the client, whatever the two settings say. Rotating the client secret never ends a session: refresh tokens keep working, and HS-signed tokens issued before the rotation are simply replaced on the next refresh.

When to use HS256

HMAC suits a confidential client whose tokens are read only by its own app and its own API, both of which already hold the client secret: they verify tokens without fetching a JWKS or caching keys. In every other case prefer an asymmetric algorithm: anyone verifies with the public key, nothing secret is shared, and whoever can verify a token cannot also sign one.

SituationChoose
Only your app reads the ID token, and it holds the secretHS for the ID token is fine
Only your own API reads the access token, and it holds the secretHS for the access token is fine
Third-party APIs, resource servers or MCP servers read the access tokenRS, PS or ES for the access token
A public client: SPA, mobile app, CLIRS, PS or ES for both: a public client has no secret
Several services would have to share the secretRS, PS or ES

Whoever verifies an HS token can also sign one

With HMAC the key that verifies is the key that signs. Keep the client secret on the client’s own servers: every service you hand it to could mint tokens Authio never issued.

What Authio accepts

RuleWhy
A public client cannot use HS in either setting.HS is keyed with the client secret, and a public client has none.
For HS, the client secret must be at least 32, 48 or 64 bytes for HS256, HS384 or HS512.RFC 7518 §3.2 asks for a key at least as long as the hash. Rotate the secret, or choose a shorter hash.
When the ID token and the access token are both asymmetric, they use the same algorithm.A client holds a single signing key, which signs both tokens and every refresh token.
HS for the ID token with an asymmetric access token is accepted, and so is the reverse.The HS token uses the secret; the other token and the refresh token use the client’s key.

A refusal names the field at fault in notifications: clientConfiguration.clientTokenConfiguration.idTokenSigningAlgorithmType for a public client asking for HS on the ID token, clientConfiguration.clientTokenConfiguration.idTokenSigningAlgorithm for a secret too short for it, and clientConfiguration.clientTokenConfiguration.accessTokenSigningAlgorithm for anything about the access token, two different asymmetric algorithms included.

In the Admin API

The settings live in clientConfiguration.clientTokenConfiguration. algorithmJwsType is now idTokenSigningAlgorithmType and digitalSubscriptionsAlgorithm is now idTokenSigningAlgorithm, with no alias; accessTokenSigningAlgorithm is new and takes the same values. On create it is optional (left out, the default above applies); on update, leaving it out keeps the current algorithm.

Request bodyJSON
{
  "clientConfiguration": {
    "clientTokenConfiguration": {
      "idTokenSigningAlgorithmType": "HMAC",
      "idTokenSigningAlgorithm": "HmacSha256",
      "accessTokenSigningAlgorithm": "RsaSsaPssSha256"
    }
  }
}
AlgorithmFamily (idTokenSigningAlgorithmType)Value
RS256, RS384, RS512RSARsaSha256, RsaSha384, RsaSha512
PS256, PS384, PS512RSARsaSsaPssSha256, RsaSsaPssSha384, RsaSsaPssSha512
ES256, ES384, ES512ECDsa, with jsonWebKeyECTypesEcdsaSha256, EcdsaSha384, EcdsaSha512
HS256, HS384, HS512HMACHmacSha256, HmacSha384, HmacSha512

APIs, resource servers and MCP servers

An access token signed with HS has no key in the realm’s JWKS. The client’s own API holds the client secret and validates it locally; any other API (a third party’s, a resource server of the realm, an MCP server) does not have the secret and must validate the token by introspection, as the resource server’s introspection client.

`accessTokenRequiresIntrospection`

The client’s response carries accessTokenRequiresIntrospection: true when its access token is signed with HS and some resource server of the realm accepts the client, an MCP server included. The console shows it as a warning and saves the client anyway. A change to the resource servers can take up to 5 minutes to show in it.

Either sign the client’s access token with RS, PS or ES, or configure those servers to validate by introspection alone:

Node.js (Express)TypeScript
import { authioBearer } from '@hyper.authio/express'

app.use(
  '/api',
  authioBearer({
    issuer: process.env.AUTHIO_ISSUER!,
    audiences: 'https://orders.example.com',
    introspection: 'introspectionOnly',
    client: { clientId: 'orders-api', clientSecret: process.env.ORDERS_API_SECRET! },
  }),
)

The client in each of these is the server’s own confidential client, chosen as the resource server’s Introspection client: Authio reports a token addressed to a resource server as active only to that client.

Configure your app and your API

The Authio libraries mirror the client: each setting is an option, and an option left out is off. Without a signing option a library accepts only the asymmetric algorithms the realm publishes, and refuses an HS token with invalid_algorithm, naming the option or introspection. With an HS algorithm named, it verifies with the client secret, which must meet the same length as above.

Node.js

In @hyper.authio/client and @hyper.authio/bff 0.8.0, idTokenSignedResponseAlg covers the ID token and the logout token, and userinfoSignedResponseAlg the signed UserInfo, which follows idTokenSignedResponseAlg when left out. In @hyper.authio/server and @hyper.authio/express, accessTokenSignedResponseAlg (one algorithm or several) verifies HS with the clientSecret of client: { clientId, clientSecret }.

Sign-in (client)TypeScript
import { createAuthioClient } from '@hyper.authio/client'

export const authio = createAuthioClient({
  issuer: process.env.AUTHIO_ISSUER!,
  clientId: 'storefront',
  clientSecret: process.env.AUTHIO_CLIENT_SECRET!,
  checks: ['pkce', 'state', 'nonce'],
  codeChallengeMethod: 'S256',
  idTokenSignedResponseAlg: 'HS256',
  redirectUri: 'https://shop.example.com/callback',
})

Without resource, the access token’s aud is the audience configured on the client (Authio unless you changed it). Any other API validates by introspection, as shown above.

.NET

Authio.Client, Authio.AspNetCore and Authio.AspNetCore.Bff 0.8.0 read IdTokenSignedResponseAlg, UserinfoSignedResponseAlg (left out, the ID token’s) and AccessTokenSignedResponseAlg (one value or a list) from the Authio section. HS is verified with ClientSecret, as UTF-8 bytes.

appsettings.json (BFF)JSON
{
  "Authio": {
    "Authority": "https://authio.api.hyperio.tech",
    "Realm": "acme",
    "ClientId": "acme-web",
    "ClientSecret": "YOUR_CLIENT_SECRET",
    "IdTokenSignedResponseAlg": "HS256",
    "AccessTokenSignedResponseAlg": "HS256"
  }
}

Next.js (NextAuth v4 and Auth.js v5)

@hyper.authio/provider-nextauth 4.0.0 takes NextAuth’s own client metadata: client.id_token_signed_response_alg for the ID token and client.userinfo_signed_response_alg for the signed UserInfo. HS is verified with clientSecret (AUTH_AUTHIO_SECRET on v5). The provider has no access token setting: it reads the access token only for the claims of session.authio, and your API verifies it.

auth.tsTypeScript
import NextAuth from 'next-auth'
import { createAuthio } from '@hyper.authio/provider-nextauth/v5'

export const authio = createAuthio({
  checks: ['pkce', 'state', 'nonce'],
  client: { id_token_signed_response_alg: 'HS256' },
  authorization: { params: { scope: 'openid profile email roles offline_access' } },
})

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [authio.provider],
  callbacks: authio.callbacks,
  events: authio.events,
})

PKCE with `plain` only does not work with NextAuth

NextAuth and Auth.js only send S256 challenges, and codeChallengeMethod: 'plain' is refused when the integration is created. A client with PKCE on that allows only plain refuses every sign-in from them: allow S256 on the client.

Troubleshooting

SymptomCause and fix
invalid_algorithm in the app or the APIThe client signs with HS and the library was not told. Set idTokenSignedResponseAlg, accessTokenSignedResponseAlg or client.id_token_signed_response_alg, or validate by introspection.
invalid_signature on HS tokensThe library holds a different secret from the client’s current one, after a rotation for instance. Update it.
A resource server or MCP server refuses every tokenThe client’s access token is signed with HS. Switch it to RS, PS or ES, or validate by introspection (introspectionOnly).
The save refuses HS: a public clientGive the client a secret (client authentication on) or choose RS, PS or ES.
The save refuses HS: the secret is too shortRotate the secret, or choose HS256, which needs 32 bytes.
The save refuses two asymmetric algorithmsChoose the same algorithm for both, or sign the ID token with HS.
The library fails at start-up asking for a longer secretAn HS algorithm is named and the client secret it was given is shorter than 32, 48 or 64 bytes.

Analytics cookies

We would like to measure how the console is used, with Google Analytics, so we can see which screens people get stuck on. It writes a cookie that recognises this browser on your next visit.

Refusing changes nothing else. The cookies that keep you signed in and remember your language and theme are needed to run the console, and stay either way. Read the Privacy Policy