How to configure, manage and validate access tokens

This guide explains how to configure the properties of access tokens issued by the Connect2id server, how to manage their lifecycle and how resource servers can validate them.

1. Access token properties

This table outlines the available properties of Connect2id server issued access tokens.

Property Variants / values
Lifetime 1 — ∞ seconds
Type Bearer
Bearer with client certificate binding (mTLS)
DPoP
Encoding

Self-contained (JWT)

  • Cryptographic security:
    • Signed (JWS)
    • Signed (JWS) and encrypted (JWE)
  • Profile:
    • c2id-1.1
    • c2id-1.0
    • oauth-1.0 (RFC 9068)
    • custom

Identifier based (opaque)

Subject identifier Public
Pairwise (encrypted)
Custom claims End-user claims
Client data
Authorisation data

1.1 Lifetime

The duration of access token validity. The default lifetime is configured in authzStore.accessToken.defaultLifetime and is set to 600 seconds (10 minutes) out of the box:

authzStore.accessToken.defaultLifetime=600

The default lifetime can be overridden during login by setting the optional access_token.lifetime parameter in the consent object. When set like this it will apply only to the access token(s) that are issued for the current end-user and OAuth 2.0 client. Use this approach to apply an end-user and / or client specific token policy.

Example consent where the access token lifetime is set to 300 seconds (5 minutes):

{
  "scope"        : [ "openid", "email" ],
  "claims"       : [ "email", "email_verified" ],
  "access_token" : { "lifetime" : 300 }
}

For other OAuth 2.0 grants, such as the client credentials grant, the default configured access token lifetime can be overridden via their plugin interface. See the client credentials SPI and the other SPIs to find out exactly how.

When a refresh token is issued together with the access token, the Connect2id server will ensure the access token lifetime does not exceed the refresh token lifetime or maximum idle time, by trimming the access token lifetime when necessary to achieve parity.

A client can find out the access token lifetime from the expires_in parameter of the token response:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token" : "vJkbPNUFaK4kVIMGQlEmyA.-MAquq_5yQqtae62b8i7aw",
  "token_type"   : "Bearer",
  "expires_in"   : 600,
  "scope"        : "app:read app:write"
}

1.2 Type

The type is a crucial security property of the access token.

Bearer

This type is the default and has minimal security. It is widely supported by common OAuth 2.0 client and resource server software.

In order to access a protected resource with a token of type Bearer the client must only present the token and nothing else. This makes for a very simple protocol, but requires the token to be stored and submitted securely (over TLS). A stolen or accidentally leaked bearer token can be used by an attacker to impersonate the end-user.

Bearer token usage is specified in RFC 6750.

Bearer with client certificate binding (mTLS)

Requires the client to submit a client X.509 certificate in its HTTPS request to the token endpoint in order to receive its token(s). The client must present the same certificate in the HTTPS request when accessing a protected resource with the token.

A stolen or leaked token cannot be used by an attacker unless they also have access to the private key associated with the certificate. The client application can store the private key in a HSM or another secure store that prevents extraction of the private key material.

To instruct the Connect2id server to issue certificate bound tokens for a given client it must be registered with the tls_client_certificate_bound_access_tokens parameter set. If the client is non-public it is recommended to use the mutual TLS (mTLS) with the client certificate to also authenticate the client.

The mTLS certificate binding is specified in RFC 8705.

DPoP

The DPoP access token type is intended for browser based clients (SPAs) which require security on par with mTLS, to address the lack of a standard JavaScript API in browsers for making HTTPS calls with a client certificate for mutual TLS.

An OAuth client can request a DPoP access token by including a proof-of-possession (POP) JWT in a DPoP HTTP header for the token request. You can find more information in our blog post that announced support for DPoP in Connect2id server 12.2.

DPoP is specified in a RFC 9449.

1.3 Encoding

OAuth 2.0 doesn't specify the content of the access token. This aspect is determined by the authorisation server, potentially in agreement with the resource servers in its jurisdiction.

An access token can be encoded in two ways:

  • Self-contained -- The authorisation is encoded into the token itself. The resource server validates the token by a cryptographic check of its digital signature using keys published by the Connect2id server.

  • Identifier-based -- The authorisation is stored in the Connect2id server and the token represents a long random key used to access it at the introspection endpoint.

Self-contained is the default encoding of access tokens minted by the Connect2id server.

During authorisation the optional access_token.encoding parameter in the consent object can be set to IDENTIFIER to switch the encoding. Use this method to switch the token encoding for all clients or only for ones that require it.

Example consent where the access token encoding is switched to identifier-based:

{
  "scope"        : [ "openid", "email" ],
  "claims"       : [ "email", "email_verified" ],
  "access_token" : { "encoding" : "IDENTIFIER" }
}

The self-contained access tokens are encoded as a signed and optionally encrypted JSON Web Token (JWT).

The signing algorithm is configured by authzStore.accessToken.jwsAlgorithm and is set to RS256 out of the box:

authzStore.accessToken.jwsAlgorithm=RS256

To make the JWT claims (payload) confidential, i.e. protected from inspection by the client and the end-user, encrypt them after the signing. To do this set access_token.encrypt in the consent object. Example:

{
  "scope"        : [ "openid", "email" ],
  "claims"       : [ "email", "email_verified" ],
  "access_token" : { "encrypt" : true }
}

The key management and content encryption algorithms are configured by authzStore.accessToken.jweAlgorithm, respectively authzStore.accessToken.jweMethod, and have these values out of the box:

authzStore.accessToken.jweAlgorithm=dir
authzStore.accessToken.jweMethod=A128GCM

Note, identifier-based access tokens are an alternative to signed-then-encrypted JWTs for keeping the underlying token authorisation confidential from clients and end-users.

The structure of the claims within the JWT is determined by the profile configured in authzStore.accessToken.codec.jwt.profile, set to c2id-1.1 out of the box.

authzStore.accessToken.codec.jwt.profile=c2id-1.1

To reduce the overall size of the JWT the supplied token profiles will compress the standard names of granted OpenID claims for release at the UserInfo endpoint. This compression can be configured here.

If the available profiles don't suit you use a self-contained access token codec plugin to define your own structure for the JWT-encoded access tokens.

1.4 Subject identifier

Access tokens have an associated subject, which for OAuth grants authorised by a person (all standard OAuth 2.0 grants save for the client credentials grant intended for services acting on their own behalf) designates an identifier for the end-user.

  • Public subject identifier -- The default behaviour of the Connect2id server is to simply set the access token subject to the local identifier of the end-user, thus making it public to all resource servers. If the token is encoded as a JWT that is signed only (and not additionally encrypted), the subject identifier will also be visible to the OAuth client. If the token is a signed-and-encrypted JWT, or an opaque identifier, the token data which includes the subject will not be visible to the client.

  • Pairwise subject identifier -- The access token subject is encrypted deterministically for the token audience, so that for a given end-user and resource server the identifier will be stable across all token issues. The encryption makes the local end-user identifier confidential, and if the Connect2id server is used to secure access to APIs belonging to multiple third party APIs, also technically impossible to correlate if the parties collude.

The setup and use of access tokens with pairwise subjects is described in a dedicated guide.

1.5 Custom claims

The Connect2id server supports three sources that can feed additional (custom) claims into the access tokens.

Feeding end-user claims

The Connect2id server claims source that feeds consented end-user claims into UserInfo responses and ID tokens can also be used with access tokens.

For tokens issued in the browser-based flows (code, implicit and hybrid) this is done in the consent, by specifying the name of the claim to include with an appropriate prefix:

  • The access_token: prefix will make the claim appear as top-level claim in the access token, e.g. access_token:email.

  • The access_token:uip: prefix will make the claim appear as member within the optional uip (preset userinfo) claim, e.g. access_token:uip:email.

  • The access_token:dat: prefix will make the claim appear as member within the optional dat (data) claim, e.g. access_token:dat:email.

Use this source when you need to include user-specific claims (attributes) into the tokens.

Example access token claims composed with access_token:email:

{
  "sub"       : "449d693f-c0b8-4088-8ed6-6607d3c95853",
  "client_id" : "ieJ0iefo",
  "scope"     : "https://api.example.com/read",
  "email"     : "[email protected]",
  ...
}

Feeding client custom data

The client registration "data" field, which offers a generic JSON object container for custom client-related parameters, can also act as source for access token claims.

Example client registration with custom data:

{
  "client_id"   : "ieJ0iefo",
  "grant_types" : [ "client_credentials" ],
  "data"        : {
     "org_id"      : "org-14738",
     "org_name"    : "Acme Inc",
     "org_contact" : "[email protected]"
  },
  ...
}

To feed selected members from the client "data" field into the access tokens issued to the client use the following configuration:

authzStore.accessToken.codec.jwt.copyClientData

Example configuration to feed the data.org_id member:

authzStore.accessToken.codec.jwt.copyClientData=org_id

Example resulting access token claims:

{
  "sub"       : "449d693f-c0b8-4088-8ed6-6607d3c95853",
  "client_id" : "ieJ0iefo",
  "scope"     : "https://api.example.com/read",
  "org_id"    : "org-14783",
  ...
}

This configuration works with all OAuth grants, but only for access tokens that are self-contained (JWT-encoded). It has no effect on identifier based access tokens.

Feeding authorisation custom data

Whenever a client is granted access to a resource or user identity the Connect2id server creates an internal authorisation object to represent the grant and the associated token properties.

Similar to client registrations, this object supports an optional "dat" (data) member, a generic JSON object container intended for storing custom authorisation parameters. This data object is automatically copied into the minted access tokens, making it avaiable to resource servers.

The custom authorisation data can be set for all OAuth grants. For the browser-based via consent object, for the rest via the grant handler's plugin SPI.

Example access token claims with a custom dat claim:

{
  "sub"       : "449d693f-c0b8-4088-8ed6-6607d3c95853",
  "client_id" : "ieJ0iefo",
  "scope"     : "https://api.example.com/read",
  "dat"       : {
     "enforce_single_use" : true,
     "app_ctx"            : "ext"
  }
  ...
}

If required by the access token profile, selected "dat" members can be moved to become top-level claims with this configuration property:

authzStore.accessToken.codec.jwt.moveAuthzData

To make enforce_single_use a top-level access token claim:

authzStore.accessToken.codec.jwt.moveAuthzData=enforce_single_use

The resulting access token claims:

{
  "sub"                : "449d693f-c0b8-4088-8ed6-6607d3c95853",
  "client_id"          : "ieJ0iefo",
  "scope"              : "https://api.example.com/read",
  "enforce_single_use" : true,
  "dat"                : {
     "app_ctx" : "ext"
  }
  ...
}

2. Access token lifecycle

2.1 Expiration and refresh

The Connect2id server indicates the lifetime of an access token issued to a client in the token response expires_in parameter.

Example token response indicating an access token lifetime of 300 seconds (5 minutes):

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token"  : "eyJraWQiOiJDWHVwIiwidHlwIjoiYXQrand0IiwiYWxnIjoiUlMyN...",
  "token_type"    : "Bearer",
  "expires_in"    : 300,
  "scope"         : "https://scopes.example.com/track-item",
  "refresh_token" : "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..yi2k..."
}

After the access token expires the client will no longer be able to use it. Trying to use an invalid or expired access token will normally result in a 401 Unauthorized error at the protected resource.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token" error_description="Invalid / expired access token"

The Connect2id server will issue a refresh token in the browser based OAuth flows (code and implicit), to let the client obtain a new access token before (or after) the current one expires.

To override this behaviour and issue only an access token to the client, set the optional refresh_token.issue parameter in the consent object to false. This can be useful in authorisations for one-time access or a transaction. Example:

{
  "scope"         : [ "openid", "email" ],
  "claims"        : [ "email", "email_verified" ],
  "refresh_token" : { issue : false }
}

Example token response with an access token only:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token" : "eyJraWQiOiJDWHVwIiwidHlwIjoiYXQrand0IiwiYWxnIjoiUlMyN...",
  "token_type"   : "Bearer",
  "expires_in"   : 300,
  "scope"        : "https://scopes.example.com/track-item"
}

In the absence of a refresh token the client can obtain a new access token only by making a new, repeated authorisation request to the Connect2id server. The end-user will be (re)authenticated (if they don't have an active user session with the server). If the previous consent wasn't persisted (by setting its long_lived parameter to false) the end-user will also be asked again for their consent to the requested scope (and any OpenID claims).

2.2 Revocation

The issued access and refresh tokens can be revoked at any one time. After a revocation event the OAuth client must repeat the authorisation request and receive the user's consent anew to continue accessing the protected resource.

Triggered by the client

An OAuth 2.0 client can ask the Connect2id server to revoke an obtained access or refresh token if it's no longer needed. All other tokens (access and refresh) issued to the client for the same end-user will also be revoked. If the token is linked to a persisted authorisation record it will be deleted as well.

Example revocation request by a client:

POST /token/revoke HTTP/1.1
Host: c2id.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

token=eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..kC0eQSyYpkdZ1a2Yr5iPUg

Triggered by the end-user or an IdP policy

The tokens and any underlying persisted authorisation record can also be invalidated in response to the end-user choosing to withdraw access to the client application or in response to some policy action, such as the termination of a user account.

The Connect2id server provides a special revocation API, to facilitate the implementation of a access management UI for users and administrators, or to respond to policy actions and events.

The API supports revocation by multiple criteria, such as revoking all client tokens and persisted authorisations for a user. Example:

POST /authz-store/rest/v3/revocation HTTP/1.1
Host: c2id.com
Authorization: Bearer ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6
Content-Type: application/x-www-form-urlencoded

subject=alice

3. Access token validation

Resource servers, such as API gateways, must validate the received access tokens according to the agreed encoding.

3.1 Self-contained (JWT)

  1. Parse the access token as a signed JWT.
  2. Ensure the JWT "typ" (type) header matches the expected for the access token profile, e.g. at+jwt.
  3. Ensure the JWT "alg" (algorithm) matches the expected, e.g. RS256.
  4. Verify the JWT signature with the matching public key, identified by the JWT "kid" (key ID) header and made available by the Connect2id server at its public JWK set endpoint. The resource server can cache the server JWK set, and refresh it when it sees a new JWT "kid" header.
  5. Ensure the JWT claims set contains all claims that must be present according to the profile, e.g. "iss", "sub", "cid", "scp", "iat", "exp" and "jti".
  6. Ensure the "iss" (issuer) claim matches the Connect2id server issuer URL, e.g. "https://c2id.com".
  7. Ensure the "exp" (expiration time) is in the future.
  8. Ensure the "aud" (audience), if set, contains the resource server.
  9. If the token must be client certificate bound, or of type DPoP, check the binding by examining the "cnf" (confirmation) claim.
  10. Finally, get the token scope and any other necessary parameters to process the request.

If any one of the checks fails the token must be rejected and the resource server must return an HTTP 401 error with an appropriate code to the client.

Example error for an invalid Bearer token:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token" error_description="Invalid / expired access token"

If the token scope doesn't match the scope of the HTTP request (as an API call or otherwise) return aninsufficient_scope error code to the client.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_scope" error_description="Scope not sufficient for the requested operation"

If the token is signed and then encrypted with a symmetric Connect2id server key, it must be first parsed as JWE and decrypted. After that the resource server can process the JWE payload as a signed JWT.

If the web server or API gateway has a built-in facility for validating access tokens or JWTs, use that instead of trying to roll your own validation code. The Nimbus JOSE+JWT library also has a facility for validating JWT-encoded access tokens.

3.2 Identifier-based (opaque)

  1. Inspect the access token at the token introspection endpoint. Note that in order to inspect a token at the Connect2id server endpoint the resource server must authenticate itself, or include a valid token authorisation.
  2. Ensure the "aud" (audience), if set, contains the resource server.
  3. If the token must be client certificate bound, or of type DPoP, check the binding by examining the "cnf" (confirmation) claim.
  4. Finally, get the token scope and any other necessary parameters to process the request.

Example introspection request:

POST /token/introspect HTTP/1.1
Host: c2id.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

token=giuLtTTnya5XpHVKNopT9w.gepM14CKpHcWloJ3XqMtvA

If required by the application the resource server can enforce single use of the access token (for an identifier-based access token) by including the optional revoke=true form parameter. The Connect2id server will invalidate the access token after completing its introspection.

If the token is invalid or has an insufficient scope, return an error as explained above in the section for self-contained tokens.

The token introspection response can be customised with a plugin.