Skip to main content

JSON Web Tokens (JWT) profile for OAuth2

JSON Web Tokens (JWT) for OAuth Client Authorization Grants is an extension to OAuth2 framework. It allows a client to send a signed JWT token to an OpenID Connect Provider in exchange for an OAuth 2.0 access token.

Usage Scenario

A usage scenario for this feature could be an electric company that needs to receive automatic monthly payments from a customer's online bank account. If the electric company and the online bank have a trusted relationship, the electric company can send a signed JWT token with the required claims to the OpenID Connect Provider configured for the online bank to request an OAuth 2.0 access token each month. The electric company can then use the access token to cash the monthly payments from the online bank.

Ory JWT profile capabilities

Ory supports both methods described in RFC 7523 for using JWTs as authorization grants and client authentication:

  • Using JWTs as Authorization Grants: Allows exchanging a JSON Web Token for an access token using an established trust relationship.
  • Using JWTs for Client Authentication: Allows OAuth 2.0 client authentication using public/private keys via JSON Web Tokens.

Using JWTs as Authorization Grants

To use the urn:ietf:params:oauth:grant-type:jwt-bearer Authorization Grant, the client must make an OAuth 2.0 Access Token Request using the following specific parameter values and encodings:

  • grant_type value must be urn:ietf:params:oauth:grant-type:jwt-bearer.
  • The assertion parameter must contain a single JWT.

The scope parameter may also be included to indicate the requested scope:

POST /oauth2/token HTTP/1.1
Host: {project.slug}.projects.oryapis.com
Content-Type: application/x-www-form-urlencoded

scope=foo+bar
&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0.
eyJpc3Mi[...omitted for brevity...].
J9l-ZhwP[...omitted for brevity...]

Establishing a trust relationship

Before using this grant type, a trust relationship must be established in Ory OAuth2 and OpenID Connect. Trust relationships come in two flavors:

  • Trust relationships restricted to a single subject. This means that the issuer is only allowed to sign JWTs for the trusted subject.
  • Trust relationships that allow issuing tokens for any subject. This may be useful for some cases (like implementing a Secure Token Service), but gives the issuer the ability to impersonate any user, so this should only be done if the issuer is as trusted as your own Ory OAuth2 and OpenID Connect instance.

Restricted trust relationships require registering the issuer, subject, and the public key using the Ory SDK:


import { Configuration, OAuth2Api } from "@ory/client"

const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)

export async function createTrustRelation(accessToken: string) {
const { data } = await ory.trustOAuth2JwtGrantIssuer({
trustOAuth2JwtGrantIssuer: {
// The "allow_any_subject" indicates that the issuer is allowed to have any principal as the subject of the JWT.
allow_any_subject: true,

// The "subject" identifies the principal that is the subject of the JWT. It must be unset if `allow_any_subject` is true.
subject: "some-subject-id",

// The "expires_at" indicates, when grant will expire, so we will reject assertion from "issuer" targeting "subject".
expires_at: "2021-04-23T18:25:43.511Z",

// The "issuer" identifies the principal that issued the JWT assertion (same as "iss" claim in JWT).
issuer: "https://example.org",

// The public key with which the JWT's signature can be verified (example)
jwk: {
alg: "RS256",
use: "sig",
kty: "RSA",
e: "AQAB",
kid: "d8e91f55-67e0-4e56-a066-6a5f0c2efdf7",
n: "nzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA-kzeVOVpVWwkWdVha4s38XM_pa_yr47av7-z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr_Mrm_YtjCZVWgaOYIhwrXwKLqPr_11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e-lf4s4OxQawWD79J9_5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa-GSYOD2QU68Mb59oSk2OB-BtOLpJofmbGEGgvmwyCI9Mw",
},

// The "scope" contains list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749])
scope: ["read"],
},
})
}

You can also delete, get, and list trust relationships.

Exchanging JWT assertion for access token

To exchange a JWT for an access token at the OAuth2 token endpoint, you need to make a POST request to the OAuth2 token endpoint with the following parameters:

  • grant_type: set to urn:ietf:params:oauth:grant-type:jwt-bearer
  • assertion: the JWT, which should be passed in the request body or in the assertion parameter of the request body.

Here is an example HTTP request to exchange the JWT

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL215LWlzc3Vlci5jb20iLCJzdWIiOiJhbGljZUBleGFtcGxlLm9yZyIsImF1ZCI6IjcxNDZkZDBiLWYyNDMtNDNiYS04MTVjLTdhMDAyMTZiNDgyMyIsIm5iZiI6MTMwMDgxNTc4MCwiZXhwIjoxMzAwODE5MzgwfQ.Dpn7zYEhaWxi7CLxr1c8Db2zxOJDzpu5QTZgeM6me68aGt7jgpKujunfx2FBhhuKY2oJmIAhXJWXplGH2NnbCGxNzx17Y4CPGJE9jLC2ZxprvV_5Cdmx5GkGcFjpOXsgBSonhmsyKkxYhS3C-mq4u2Tx9Zi494G2EbDH0L2BSuWYi411qm4LrIHQRdiFP9v34VH-5hU005bvrlGJBA9W-Eom4krFYtC4_Zgc7XY2mcChBw0AYz3A1B0_7ui95iDR-33D5tBAGRn6iGgnVBeR1GmZX5y4jz7Nht2lbPQkrCyLsoPxn2ZQPqvbOUKxdgsrhkcs0UGND8GsDwDzISuuAw

which has the claims

{
iss: "https://my-issuer.com",
sub: "7146dd0b-f243-43ba-815c-7a00216b4823",
aud: "https://{project.slug}.projects.oryapis.com/oauth2/token",
nbf: 1300815780,
exp: 1300819380,
}

for an OAuth2 Access Token (the scope parameter is optional):

POST /oauth2/token HTTP/1.1
Host: {project.slug}.projects.oryapis.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&scope=read
&assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL215LWlzc3Vlci5jb20iLCJzdWIiOiJhbGljZUBleGFtcGxlLm9yZyIsImF1ZCI6IjcxNDZkZDBiLWYyNDMtNDNiYS04MTVjLTdhMDAyMTZiNDgyMyIsIm5iZiI6MTMwMDgxNTc4MCwiZXhwIjoxMzAwODE5MzgwfQ.Dpn7zYEhaWxi7CLxr1c8Db2zxOJDzpu5QTZgeM6me68aGt7jgpKujunfx2FBhhuKY2oJmIAhXJWXplGH2NnbCGxNzx17Y4CPGJE9jLC2ZxprvV_5Cdmx5GkGcFjpOXsgBSonhmsyKkxYhS3C-mq4u2Tx9Zi494G2EbDH0L2BSuWYi411qm4LrIHQRdiFP9v34VH-5hU005bvrlGJBA9W-Eom4krFYtC4_Zgc7XY2mcChBw0AYz3A1B0_7ui95iDR-33D5tBAGRn6iGgnVBeR1GmZX5y4jz7Nht2lbPQkrCyLsoPxn2ZQPqvbOUKxdgsrhkcs0UGND8GsDwDzISuuAw

In the example above, the JWT is passed in the assertion parameter of the request body. The grant_type parameter is set to urn:ietf:params:oauth:grant-type:jwt-bearer to indicate that we are exchanging a JWT for an access token.

The OAuth2 token endpoint will then verify the signature of the JWT and check the claims to ensure that it is valid. If the JWT is valid, the OAuth2 token endpoint will issue an access token that can be used to access protected resources:

Example claims of JSON Web Token
{
iss: "https://{project.slug}.projects.oryapis.com/",
sub: "7146dd0b-f243-43ba-815c-7a00216b4823",
scp: ["read"],
// ...
}

JWT assertion validation requirements

When using the urn:ietf:params:oauth:grant-type:jwt-bearer Authorization Grant, the assertion parameter's JWT is validated through the following steps:

  • The JWT must contain an iss (issuer) claim that has a unique identifier for the entity that issued the JWT. This value should match the issuer value of the trust relationship.
  • The JWT must contain a sub (subject) claim that identifies the principal as the subject of the JWT (e.g., the user ID). This value should match the subject value of the trust relationship unless allow_any_subject is true.
  • The JWT must contain an aud (audience) claim with a value that identifies the authorization server as an intended audience. The value should be the OAuth2 Token URL.
  • The JWT must contain an exp (expiration time) claim that restricts the time window during which the JWT can be used. This can be controlled through the /oauth2/grant/jwt/max_ttl setting.
  • The JWT may contain an nbf (not before) claim that identifies the time before which the token must not be accepted for processing by Ory.
  • The JWT may contain an iat (issued at) claim that identifies the time at which the JWT was issued. This can be controlled through the /oauth2/grant/jwt/iat_optional (default false) setting. If iat is not provided, the current time (when Ory receives the assertion) will be considered the issued date.
  • The JWT may contain a jti (JWT ID) claim that provides a unique identifier for the token. This can be controlled through the /oauth2/grant/jwt/jti_optional (default false) setting. Note that if jti is required, Ory will reject all assertions with the same jti if jti was already used by some assertion, and this assertion is not expired yet (see exp claim).
  • The JWT signature is verified using the JSON Web Key registered in the trust relationship.
  • If a scope was included in the OAuth2 access token request, it will be validated against the allowed scope in the trust relationship.

Configuring JWT validation parameters

To configure the /oauth2/grant/jwt/jti_optional, /oauth2/grant/jwt/iat_optional, and /oauth2/grant/jwt/max_ttl settings using the Ory CLI, follow the example below:

ory patch oauth2-config \
--replace "/oauth2/grant/jwt/jti_optional=true" \
--replace "/oauth2/grant/jwt/iat_optional=true" \
--replace "/oauth2/grant/jwt/max_ttl=1h"

JWTs for client authentication

Ory provides support for OAuth 2.0 Client Authentication with RSA and ECDSA private/public key pairs and supports signing algorithms such as RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, and EdDSA. This method of authentication replaces the classic HTTP Basic Authorization and HTTP POST Authorization schemes. Instead of sending the client_id and client_secret, you authenticate the client with a signed JSON Web Token.

To use this feature for a specific OAuth 2.0 Client, set the token_endpoint_auth_method to private_key_jwt and register the public key of the RSA/ECDSA signing key either using the jwks_uri or jwks fields of the client.

When authenticating the client at the token endpoint, generate and sign (with the RSA/ECDSA private key) a JSON Web Token with the following claims:

  • iss: REQUIRED. Issuer. This claim MUST contain the client_id of the OAuth Client.
  • sub: REQUIRED. Subject. This claim MUST contain the client_id of the OAuth Client.
  • aud: REQUIRED. Audience. The aud (audience) Claim is a value that identifies the Authorization Server (Ory) as an intended audience. The Authorization Server MUST verify that it's an intended audience for the token. The Audience SHOULD be the URL of the Authorization Server's Token Endpoint.
  • jti: REQUIRED. JWT ID. This is a unique identifier for the token, which can be used to prevent reuse of the token. These tokens MUST only be used once, unless conditions for reuse were negotiated between the parties; any such negotiation is beyond the scope of this specification.
  • exp: REQUIRED. Expiration time on or after which the ID Token MUST NOT be accepted for processing.
  • iat: OPTIONAL. Time at which the JWT was issued.

To make a request to the /oauth2/token endpoint, include

client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer

and

client_assertion=<signed-jwt>

in the request body:

POST /oauth2/token HTTP/1.1
Host: {project.slug}.projects.oryapis.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=i1WsRn1uB1&
client_id=s6BhdRkqt3&
client_assertion_type=
urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&
client_assertion=PHNhbWxwOl ... ZT

Registering the client's public key

To configure the public key for an OAuth2 client in order to verify the signature of the authentication JWT, you need to do the following:

  1. Generate an RSA or ECDSA key pair. The private key will be used by the client to sign JWTs, and the public key will be used by the authorization server to verify the signatures.
  2. Register the public key with the OAuth2 client. This can be done using the jwks_uri or jwks fields of the client. The jwks_uri is a URL that points to a JSON Web Key Set (JWKS) that contains the public key. The jwks field is a direct JSON object representation of the JWKS.

Here is an example of how to register an RSA public key for an OAuth2 client:


import { Configuration, OAuth2Api } from "@ory/client"

const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)

export async function createOAuth2Client() {
await ory.createOAuth2Client({
oAuth2Client: {
token_endpoint_auth_method: "private_key_jwt",
token_endpoint_auth_signing_alg: "RS256", // or ES256, EdDSA; ...
// ...

// define the public key directly:
jwks: {
keys: [
{
kty: "RSA",
n: "jL7h5wc-yeMUsHGJHc0xe9SbTdaLKXMHvcIHQck20Ji7SvrHPdTDQTvZtTDS_wJYbeShcCrliHvbJRSZhtEe0mPJpyWg3O_HkKy6_SyHepLK-_BR7HfcXYB6pVJCG3BW-lVMY7gl5sULFA74kNZH50h8hdmyWC9JgOHn0n3YLdaxSWlhctuwNPSwqwzY4qtN7_CZub81SXWpKiwj4UpyB10b8rM8qn35FS1hfsaFCVi0gQpd4vFDgFyqqpmiwq8oMr8RZ2mf0NMKCP3RXnMhy9Yq8O7lgG2t6g1g9noWbzZDUZNc54tv4WGFJ_rJZRz0jE_GR6v5sdqsDTdjFquPlQ",
e: "AQAB",
use: "sig",
kid: "some-key-id",
},
],
},

// or alternatively tell Ory to fetch it from an URL:
jwks_uri: "https://path-to-my-public/keys.json",
},
})
}

Note that the kid field (key ID) should be a unique identifier for the public key. This is used by the authorization server to identify which key to use to verify the signature of the JWT. If you have multiple keys, you should use a different kid for each key.

Once the public key has been registered, the OAuth2 client can use its private key to sign JWTs, and the authorization server can use the public key to verify the signatures.