Skip to content

Signed and encrypted UserInfo

Receive UserInfo as a JWT signed by the realm or encrypted to the client's own key, and configure the app to read it.

The UserInfo endpoint answers JSON by default. A client can ask instead for a signed JWT (the app proves the answer came from Authio) or a JWT signed and then encrypted to the client's own key (only the app reads it). The form is chosen per client and applies to every UserInfo call it makes.

Which form to use

JSONSigned JWTEncrypted to the client's key
Content-Typeapplication/jsonapplication/jwtapplication/jwt
What the app gainsNothing to configureProof of origin and audience (iss, aud)Proof of origin, and only the app reads the claims
What the app configuresNothingSignature validation against the realm's JWKSThe client's private key and signature validation

Before you start

  • The client's tokens must be signed with RSA or ECDsa. A UserInfo signed with HMAC could not be verified by the app, and Authio refuses that combination.
  • For the encrypted form, the client needs its own encryption key (generated or imported). Token encryption can stay off: the two are independent.
  • The private key lives on the app's server. A SPA or a mobile app has nowhere to keep it: use the signed JWT at most for those clients.

Step by step in Authio

  1. Generate the client's key (encrypted form only)

    In Clients, open the client and go to Edit → Security → Token Encryption → Generate key pair. Use Download JWK before closing the dialog: the private key is shown once. If the client already holds a key for its tokens, it is the same one; do not generate another.

  2. Check the encryption algorithms

    UserInfo is encrypted with the algorithms of the Token Encryption group, even with token encryption off. Leave JWE Algorithm on RSA, Key Encryption Algorithm on RSA-OAEP and Content Encryption Algorithm on a CBC-HMAC, such as A256CBC-HS512.

  3. Choose the response form

    In Security → UserInfo response, turn on Encrypted to the client's key (Signed JWT turns on with it, since an encrypted answer is always signed first) or only Signed JWT. Save.

  4. Configure the app and deploy

    An app configured for JSON refuses a JWT, and the other way around. Update the app (see below) and deploy it together with the change in Authio.

What the endpoint answers

  • Signed: a JWT with the UserInfo claims, signed with the same key and algorithm as the client's ID token, with iss the realm's issuer, aud the client_id and a short exp.
  • Encrypted: that same JWT inside a JWE (five parts) to the client's key, with the alg and enc configured on the client. Once opened, it is validated like the signed one.
  • The sub is the same as the ID token's. Check it matches before using the other claims.
Test with curlBash
curl -i https://<your-authio>/realms/<realm>/protocol/openid-connect/userinfo \
  -H "Authorization: Bearer <access token>"

# HTTP/1.1 200 OK
# Content-Type: application/jwt
#
# eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIi...  (five parts: encrypted)

Configure your app

.NET (Authio.Client)

IAuthioUserInfoClient (Authio.Client 0.6.0 or later) reads all three forms. A signed JWT is verified against the realm's JWKS (iss the issuer, aud the client); an encrypted one is opened with Authio:ClientEncryptionKey (the private JWK or the PEM) and then verified. An answer that does not verify, or that no key opens, throws AuthioProtocolException with invalid_userinfo.

appsettings.jsonJSON
{
  "Authio": {
    "Authority": "https://<your-authio>",
    "Realm": "<realm>",
    "ClientId": "<client>",
    "ClientEncryptionKey": "<the private JWK or the PEM, from a secret>"
  }
}

BFF and ASP.NET Core's OpenID Connect handler

ASP.NET Core's handler reads a signed UserInfo but does not open an encrypted one. In Authio's BFF, or with the handler directly, keep the answer signed only, or turn GetClaimsFromUserInfoEndpoint off and call UserInfo through IAuthioUserInfoClient.

Next.js

authio-provider-nextauth signs in with the ID token and never calls UserInfo, so turning UserInfo into a JWT does not change sign-in. If your app calls UserInfo itself, open the answer with decryptAuthioToken (which uses AUTH_AUTHIO_ENCRYPTION_KEY) and validate the signature with jose:

lib/userinfo.tsTypeScript
import { decryptAuthioToken } from 'authio-provider-nextauth/v5'
import { createRemoteJWKSet, jwtVerify } from 'jose'

const issuer = process.env.AUTH_AUTHIO_ISSUER!
const jwks = createRemoteJWKSet(
  new URL(`${issuer}/protocol/openid-connect/certs`),
)

export async function readUserInfo(accessToken: string) {
  const response = await fetch(`${issuer}/protocol/openid-connect/userinfo`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  })

  if (!response.headers.get('content-type')?.includes('application/jwt')) {
    return response.json()
  }

  // Opens the JWE; a JWT that is only signed comes back as it is
  const signed = await decryptAuthioToken(await response.text())
  const { payload } = await jwtVerify(signed, jwks, {
    issuer,
    audience: process.env.AUTH_AUTHIO_ID,
  })

  return payload
}

openid-client (Node)

openid-client reads whichever form the client metadata names: userinfo_signed_response_alg for the signed one and, for the encrypted one, also userinfo_encrypted_response_alg/enc and the private key in jwks, with alg added to the JWK (it only tries a key whose alg matches the JWE's).

userinfo.tsTypeScript
const key = JSON.parse(process.env.AUTHIO_ENCRYPTION_KEY!)

const client = new issuer.Client(
  {
    client_id: '<client>',
    client_secret: '<secret>',
    userinfo_signed_response_alg: 'RS256', // the client's signing algorithm
    userinfo_encrypted_response_alg: 'RSA-OAEP',
    userinfo_encrypted_response_enc: 'A256CBC-HS512',
  },
  { keys: [{ ...key, alg: 'RSA-OAEP' }] },
)

const claims = await client.userinfo(accessToken)

Any OIDC client

  1. Call UserInfo with the access token and look at the Content-Type: application/jwt means the JWT form.
  2. With five parts, decrypt it with the client's private key, accepting the alg and enc configured on the client.
  3. Validate the signed JWT against the realm's JWKS: signature, iss (the realm's issuer), aud (your client_id) and exp.
  4. Check the sub matches the ID token's before using the other claims.

Troubleshooting

SymptomCause and fix
The app fails to read UserInfo after the changeThe library expects JSON and got a JWT (or the other way around). Configure the same form on both sides.
invalid_userinfo (.NET)The signature did not verify or no key opened the JWE. Check Authio:ClientEncryptionKey and that the key is the client's current one.
The Encrypted to the client's key switch is lockedThe client has no key of its own yet. Generate or import one under Token Encryption.
Authio refuses to save the signed UserInfoThe client's tokens are signed with HMAC. Switch to RSA or ECDsa.
GCM content algorithm unavailableAuthio only encrypts with CBC-HMAC (A128CBC-HS256, A192CBC-HS384, A256CBC-HS512).