TL;DR:
When signing your JWTs it is better to use an asymmetric signing algorithm. Doing so will no longer require sharing a private key across many applications. Using an algorithm like RS256 and the JWKS endpoint allows your applications to trust the JWTs signed by Auth0.
The code snippets below have been adapted from Auth0's node-jwks-rsa and express-jwt.
Check out the sample repository.
Auth0 offers a generous free tier to get started with modern authentication.
RS256 vs HS256
When creating clients and resources servers (APIs) in Auth0, two algorithms are supported for signing JSON Web Tokens (JWTs): RS256 and HS256. HS256 is the default for clients and RS256 is the default for APIs. When building applications, it is important to understand the differences between these two algorithms. To begin, HS256 generates a symmetric MAC and RS256 generates an asymmetric signature. Simply put HS256 must share a secret with any client or API that wants to verify the JWT. Like any other symmetric algorithm, the same secret is used for both signing and verifying the JWT. This means there is no way to fully guarantee Auth0 generated the JWT as any client or API with the secret could generate a validly signed JWT. On the other hand, RS256 generates an asymmetric signature, which means a private key must be used to sign the JWT and a different public key must be used to verify the signature. Unlike symmetric algorithms, using RS256 offers assurances that Auth0 is the signer of a JWT since Auth0 is the only party with the private key.
Verifying RS256
Due to the symmetric nature of HS256, we favor the use of RS256 for signing your JWTs, especially for APIs with 3rd party clients. However, this decision comes with some extra steps for verifying the signature of your JWTs. Auth0 uses the JWK specification to represent the cryptographic keys used for signing or verifying tokens. This spec defines two high level data structures: JWKS and JWK. Here are the definitions directly from the specification:
JSON Web Key (JWK)
A JSON object that represents a cryptographic key. The members of the object represent properties of the key, including its value.
JWK Set
A JSON object that represents a set of JWKs. The JSON object MUST have a "keys" member, which is an array of JWKs.
At the most basic level, the JWKS is a set of keys containing the public keys that should be used to verify any JWT issued by the authorization server. Auth0 exposes a JWKS endpoint for each tenant, which is found at https://your-tenant.auth0.com/.well-known/jwks.json. This endpoint will contain the JWK used to sign all Auth0 issued JWTs for this tenant. Here is an example of the JWKS used by a demo tenant.
{ "keys": [ { "alg": "RS256", "kty": "RSA", "use": "sig", "x5c": [ "MIIC+DCCAeCgAwIBAgIJBIGjYW6hFpn2MA0GCSqGSIb3DQEBBQUAMCMxITAfBgNVBAMTGGN1c3RvbWVyLWRlbW9zLmF1dGgwLmNvbTAeFw0xNjExMjIyMjIyMDVaFw0zMDA4MDEyMjIyMDVaMCMxITAfBgNVBAMTGGN1c3RvbWVyLWRlbW9zLmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMnjZc5bm/eGIHq09N9HKHahM7Y31P0ul+A2wwP4lSpIwFrWHzxw88/7Dwk9QMc+orGXX95R6av4GF+Es/nG3uK45ooMVMa/hYCh0Mtx3gnSuoTavQEkLzCvSwTqVwzZ+5noukWVqJuMKNwjL77GNcPLY7Xy2/skMCT5bR8UoWaufooQvYq6SyPcRAU4BtdquZRiBT4U5f+4pwNTxSvey7ki50yc1tG49Per/0zA4O6Tlpv8x7Red6m1bCNHt7+Z5nSl3RX/QYyAEUX1a28VcYmR41Osy+o2OUCXYdUAphDaHo4/8rbKTJhlu8jEcc1KoMXAKjgaVZtG/v5ltx6AXY0CAwEAAaMvMC0wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUQxFG602h1cG+pnyvJoy9pGJJoCswDQYJKoZIhvcNAQEFBQADggEBAGvtCbzGNBUJPLICth3mLsX0Z4z8T8iu4tyoiuAshP/Ry/ZBnFnXmhD8vwgMZ2lTgUWwlrvlgN+fAtYKnwFO2G3BOCFw96Nm8So9sjTda9CCZ3dhoH57F/hVMBB0K6xhklAc0b5ZxUpCIN92v/w+xZoz1XQBHe8ZbRHaP1HpRM4M7DJk2G5cgUCyu3UBvYS41sHvzrxQ3z7vIePRA4WF4bEkfX12gvny0RsPkrbVMXX1Rj9t6V7QXrbPYBAO+43JvDGYawxYVvLhz+BJ45x50GFQmHszfY3BR9TPK8xmMmQwtIvLu1PMttNCs7niCYkSiUv2sc2mlq1i3IashGkkgmo=" ], "n": "yeNlzlub94YgerT030codqEztjfU_S6X4DbDA_iVKkjAWtYfPHDzz_sPCT1Axz6isZdf3lHpq_gYX4Sz-cbe4rjmigxUxr-FgKHQy3HeCdK6hNq9ASQvMK9LBOpXDNn7mei6RZWom4wo3CMvvsY1w8tjtfLb-yQwJPltHxShZq5-ihC9irpLI9xEBTgG12q5lGIFPhTl_7inA1PFK97LuSLnTJzW0bj096v_TMDg7pOWm_zHtF53qbVsI0e3v5nmdKXdFf9BjIARRfVrbxVxiZHjU6zL6jY5QJdh1QCmENoejj_ytspMmGW7yMRxzUqgxcAqOBpVm0b-_mW3HoBdjQ", "e": "AQAB", "kid": "NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg", "x5t": "NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg" } ]}
Note: At the time of writing, Auth0 only supports a single JWK for token verification, however it is important to assume this endpoint could contain multiple JWKs. As an example, multiple keys can be found in the JWKS when rotating signing certificates.
The JWKS above contains a single key. Each property in the key is defined by the JWK specification RFC 7517 Section 4. We will use these properties to determine which key was used to sign the JWT. Here is a quick breakdown of what each property represents:
- alg: is the algorithm for the key
- kty: is the key type
- use: is how the key was meant to be used. For the example above,
represents signature verification.sig
- x5c: is the x509 certificate chain
- e: is the exponent for a standard pem
- n: is the moduluos for a standard pem
- kid: is the unique identifier for the key
- x5t: is the thumbprint of the x.509 cert (SHA-1 thumbprint)
Verifying a JWT using the JWKS endpoint
Now that we understand JWKS and the specific properties of each JWK let's put this together and verify a JWT signed by Auth0 with RS256. In our example we are going to build a first party API for our ficticious company c0der.io. The API will have a single endpoint returning metadata about our API, which requires a valid JWT. Ideally your resource server would also validate necessary scopes, however, that is beyond this topic. We will assume an API and client have been created and we will focus on what happens once our API recieves a request.
Here are the steps for validating the JWT:
- Retrieve the JWKS and filter for potential signature verification keys.
- Extract the JWT from the request's authorization header.
- Decode the JWT and grab the
property from the header.kid
- Find the signature verification key in the filtered JWKS with a matching
property.kid
- Using the
property build a certificate which will be used to verify the JWT signature.x5c
- Ensure the JWT contains the expected audience, issuer, expiration, etc.
Note: There are many good libraries for verifying a JWT. You can use the curated list to find one for your language at JWT.io.
Let's jump into some code and see this in action.
Retrieving the JWK
The first thing we are going to do is grab the key set from the Auth0 JWKS endpoint. Using
request
we can simply perform a GET
to retrieve the JSON blob./** * https://github.com/sgmeyer/auth0-node-jwks-rs256/blob/master/src/lib/JwksClient.js#L7-L28 **/ export class JwksClient { constructor(options) { this.options = { strictSsl: true, ...options }; } getJwks(cb) { request({ uri: this.options.jwksUri, strictSsl: this.options.strictSsl, json: true }, (err, res) => { if (err || res.statusCode < 200 || res.statusCode >= 300) { if (res) { return cb(new JwksError(res.body && (res.body.message || res.body) || res.statusMessage || `Http Error ${res.statusCode}`)); } return cb(err); } var jwks = res.body.keys; return cb(null, jwks); }); } }
After grabbing the JWKS we will filter out all the keys that are not intended for verifying a JWT. This step may seem unneccessary as the Auth0 JWKS endpoint typically contains a single signature verification key, however it is good practice to assume multiple keys could be present (i.e. key rotation).
export class JwksClient { constructor(options) { ... } getJwks(cb) { ... } /** * https://github.com/sgmeyer/auth0-node-jwks-rs256/blob/master/src/lib/JwksClient.js#L30-L58 **/ getSigningKeys(cb) { const callback = (err, keys) => { if (err) { return cb(err); } if (!keys || !keys.length) { return cb(new JwksError('The JWKS endpoint did not contain any keys')); } const signingKeys = keys .filter(key => key.use === 'sig' // JWK property `use` determines the JWK is for signature verification && key.kty === 'RSA' // We are only supporting RSA (RS256) && key.kid // The `kid` must be present to be useful for later && ((key.x5c && key.x5c.length) || (key.n && key.e)) // Has useful public keys ).map(key => { return { kid: key.kid, nbf: key.nbf, publicKey: certToPEM(key.x5c[0]) }; }); // If at least one signing key doesn't exist we have a problem... Kaboom. if (!signingKeys.length) { return cb(new JwksError('The JWKS endpoint did not contain any signature verification keys')); } // Returns all of the available signing keys. return cb(null, signingKeys); }; this.getJwks(callback); } }
Quick recap, we have retrieved the set of keys (JWKS) from Auth0 and we have filtered out all keys that are not intended for verifying a JWT with the keytype of RSA. As an additional measure, we filtered out any key missing a public key and a
kid
property. This step is important as later we will need to use the kid
property to find the exact key necessary to verify the JWT.Finding the exact signature verification key
Continuing with our example, we will add an additional method to
JwksClient
called getSigningKey
taking a key identifier (kid
) as an argument and return the expected key (JWK) used to sign the JWT. If a JWK is not found, then an error must be thrown. This means the JWT supplied with the request was signed with a key that is not supported by Auth0. This should ultimately be treated as a 401 Unauthorized
. If a matching key is found, then that key will be passed as an argument to the callback.export class JwksClient { constructor(options) { this.options = { strictSsl: true, ...options }; } getJwks(cb) { ... } getSigningKeys(cb) { ... } /** * https://github.com/sgmeyer/auth0-node-jwks-rs256/blob/master/src/lib/JwksClient.js#L67-L84 **/ getSigningKey = (kid, cb) => { const callback = (err, keys) => { if (err) { return cb(err); } const signingKey = keys.find(key => key.kid === kid); if (!signingKey) { var error = new SigningKeyNotFoundError(`Unable to find a signing key that matches '${kid}'`); return cb(error); } return cb(null, signingKey) }; this.getSigningKeys(callback); }
So far we have built a client that can be used to retrieve the JWKS and find the signature verification key using the value of the
kid
property. At this point, we have neither verified the JWT nor extracted it from the request. Note: As an optimization for production it would be wise to implement a caching mechanism for the JWKS or keys so that each request does not require a call to the JWKS endpoint.
Creating a Signing Secret Wrapper
Before we move on to processing the JWT we want to create a quick wrapper for the
JwksClient
that returns a method that will eventually hand off the key we need to verify the JWT. This class is a bit specific to the node async model, however it is necessary for the final step when we tie together the final middleware./** * https://github.com/sgmeyer/auth0-node-jwks-rs256/blob/master/src/lib/expressJwtSecret.js#L4-L38 **/ const handleSigningKeyError = (err, cb) => { // If we didn't find a match, can't provide a key. if (err && err.name === 'SigningKeyNotFoundError') { return cb(null); } // Any other error we will bubble up. if (err) { return cb(err); } }; export default (options) => { if (options === null || options === undefined) { throw new ArgumentError('An options object must be provided when initializing expressJwtSecret'); } const client = new JwksClient(options); const onError = handleSigningKeyError; return function secretProvider(req, header, payload, cb) { // Only RS256 is supported. if (!header || header.alg !== 'RS256') { return cb(null, null); } client.getSigningKey(header.kid, (err, key) => { if (err) { return onError(err, (newError) => cb(newError, null)); } // Provide the key. return cb(null, key.publicKey || key.rsaPublicKey); }); }; };
Grabbing the JWT from the Request
Now we need to move on to extract the JWT from the request. Most APIs expect the JWT is sent as a Bearer Token in the authorization header or as a URL parameter. Now we are going to start building an Express middleware that will extract the JWT, create a signing secret, and verify the token using the
jsonwebtoken
module. We will start by creating a file called expressJWt.js
and extract the JWT from the request.export default (options) => { var middleware = (req, res, next) => { // https://github.com/sgmeyer/auth0-node-jwks-rs256/blob/master/src/middleware/expressJwt.js#L12-L23 var authHeader = req.headers.authorization; var parts = authHeader.split(' '); if (parts.length != 2) { throw new UnauthorizedError('credentials_required', { message: 'No authorization token was found' }); } var scheme = parts[0]; if(!/^Bearer$/i.test(scheme)) { throw new UnauthorizedError('credentials_bad_scheme', { message: 'Format is Authorization: Bearer [token]' }); } } return middleware; }
Note: It is important to differentiate between verifying and decoding a token. Decoding simply means to base64url decode the header and payload. This step is not concerned with validating the signature, audience, expiration, etc. Verifying the token is the process of ensuring the token's signature, but also the audience, expiration, etc are valid.
At this point the middleware is pretty boring. It simply parses the authorization header for the format
Authorization: Bearer [token]
. If the authorization header doesn't exist, is empty, or does not fit the format the we will throw an UnauthorizedError
which later is treated as a 401 Unauthorized
response. If the header does meet the criteria the JWT is pulled from the authorization header.Decoding the JWT
Now that we have our hands on the JWT we need to decode the token. This will be important later when we want to grab the
kid
property to find the key we need to verify the signature. Recall we can decode the JWT as it is simply base 64 url encoded. To do this we will use the jsonwebtoken
node module. If you are interested in learning more about how a JWT is constructed check out our blog post.export default (options) => { var middleware = (req, res, next) => { ... // https://github.com/sgmeyer/auth0-node-jwks-rs256/blob/master/src/middleware/expressJwt.js#L25-L28 var token = parts[1]; // This could fail. If it does handle as 401 as the token is invalid. var decodedToken = jwt.decode(token, {complete: true}); } return middleware; }
Retrieving the Secret and Verifying the JWT
Now that we have the decoded token we have all the pieces we need to call get the JWKS and find the signature verification key. First, we will use the
options.secret
to wrap our calls to JwksClient
and assign it to getSecret
. Next, using the jsonwebtoken
node module, we will verify the token. Lastly, we will use async.waterfall
to call getSecret
and invoke verifyToken
with the results of getSecret
. This will ultimately be used to verify the JWT.export default (options) => { var secretCallback = options.secret; var middleware = (req, res, next) => { ... // https://github.com/sgmeyer/auth0-node-jwks-rs256/blob/master/src/middleware/expressJwt.js#L30-L58 if (decodedToken.header.alg !== 'RS256') { // we are only supporting RS256 so fail if this happens. return cb(null, null); } var tasks = [ function getSecret(callback) { secretCallback(req, decodedToken.header, decodedToken.payload, callback); }, function verifyToken(secret, callback) { jwt.verify(token, secret, options, function(err, decoded) { if (err) { callback(new UnauthorizedError('invalid_token', err)); } else { callback(null, decoded); } }); } ]; async.waterfall(tasks, (err, result) => { if (err) { return next(err); } set(req, _requestProperty, result); next(); }); } return middleware; }
Calling our middleware
Now that we have all the pieces in place to validate an RS256 token, we need to configure the middleware and apply it to the route.
/** * https://github.com/sgmeyer/auth0-node-jwks-rs256/blob/master/src/middleware/jwtCheck.js#L8-L17 **/ import expressJwt from './expressJwt'; import expressJwtSecret from '../lib/expressJwtSecret'; export const jwtCheck = expressJwt({ secret: expressJwtSecret({ jwksUri: `https://your-tenant.auth0.com/.well-known/jwks.json` }), // Validate the audience and the issuer. audience: 'https://api.c0der.io/v1/', issuer: `https://your-tenant.auth0.com/`, algorithms: ['RS256'] }); app.get('/meta', jwtCheck, api());
As you can see anytime we call the api's
/meta
endpoint a token will be verified. This is where all of the pieces are pulled together. Each time a request is made to /meta
the JWT will be verified against the appropriate key in the JWKS. Also, if the JWT passes signature verification the audience and issuer will be checked using jsonwebtoken
module.Auth0 provides the simplest and easiest to use user interface tools to help administrators manage user identities including password resets, creating and provisioning, blocking and deleting users.
Aside: Delegating JWT Implementation to the Experts
JWTs are an integral part of the OpenID Connect standard, an identity layer that sits on top of the OAuth2 framework. Auth0 is an OpenID Connect certified identity platform. This means that if you pick Auth0 you can be sure it is 100% interoperable with any third party system that also follows the specification.
The OpenID Connect specification requires the use of the JWT format for ID tokens, which contain user profile information (such as the user's name and email) represented in the form of claims. These claims are statements about the user, which can be trusted if the consumer of the token can verify its signature.
While the OAuth2 specification doesn't mandate a format for access tokens, used to grant applications access to APIs on behalf of users, the industry has widely embraced the use of JWTs for these as well.
As a developer, you shouldn't have to worry about directly validating, verifying, or decoding authentication-related JWTs in your services. You can use modern SDKs from Auth0 to handle the correct implementation and usage of JWTs, knowing that they follow the latest industry best practices and are regularly updated to address known security risks.
For example, the Auth0 SDK for Single Page Applications provides a method for extracting user information from an ID Token,
.auth0.getUser
If you want to try out the Auth0 platform, sign up for a free account and get started! With your free account, you will have access to the following features:
- Universal Login for Web, iOS & Android
- Up to 2 social identity providers (like Twitter and Facebook)
- Unlimited Serverless Rules
To learn more about JWTs, their internal structure, the different types of algorithms that can be used with them, and other common uses for them, check out the JWT Handbook.
Happy Coding
At this point we have seen an end to end sample for verifying a RS256 signed JWT. This process not overly difficult, however it does add significant complexity as opposed to a symmetric algorithm like HS256. Despite the added complexity it offers significant security benefits over HS256 and will surely pay off in the long run. We highly recommend using RS256 as a means to sign your JWTs.
You can find the full sample on GitHub.