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 beurn: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 tourn:ietf:params:oauth:grant-type:jwt-bearer
assertion
: the JWT, which should be passed in the request body or in theassertion
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:
{
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 theissuer
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 thesubject
value of the trust relationship unlessallow_any_subject
istrue
. - 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
(defaultfalse
) 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
(defaultfalse
) setting. Note that if jti is required, Ory will reject all assertions with the samejti
ifjti
was already used by some assertion, and this assertion is not expired yet (seeexp
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. Theaud
(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:
- 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.
- Register the public key with the OAuth2 client. This can be done using the
jwks_uri
orjwks
fields of the client. Thejwks_uri
is a URL that points to a JSON Web Key Set (JWKS) that contains the public key. Thejwks
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.