Securing a web application or api requires actually validating the access token that is being used. When using JSON web tokens (JWTs), there are two mechanisms for doing this. But the core of the solution requires inspecting that JWT, understanding who the authority is, and using that authority for verification.
The properties or fields in a JWT are called claims. JWTs contain an ISS claim. This is the Issuer. The issuer is the authorization server (AS) which is marked by the issuer. As such the AS provides a full document about how JWTs are constructed and how to verify them. This document must always be found at https://${Issuer}/.well-known/openid-configuration (according to RFC 8414). Here’s a real life example.
What’s more is that the openid configuration may inform you of an introspection endpoint. By passing the token there the AS will tell you if the token is a valid one. However, not only is this optional, it is expensive since the results can not be cached. If you are verifying 1000s of tokens per second, it is far too prohibitive to verify them like that.
JWTs are signed, that means they have a signature which allows them to be verified. With the signature, they also contain a kid which specifies which public key was used to sign the token and create the signature.
A better alternative is to use the issuer, kid, and signature to verify the token. To do this get the relevant JSON Web Keys (JWK). Use the issuer to get the keys, find the right key using the kid, and then verify the signature using the key. Therefore keys allow you to self verify the token much faster and the keys themselves are cacheable, that means that you can avoid frequent API calls by using the JWKs. This still has a similar problem as the introspect endpoint; that is you are implicitly trusting the issuer. So step one becomes:
- Trust the Issuer - To verify a JWT the first step is to list the issuers that the web application will trust. If you trust all issuers it is trivial for an attacker to create a verifiable token and call your api. Specifying a self-created token which will pass your checks.
After that, verify to token, just break open the token grab associate JWK and verify the signature
Note: This is a generic authorizer. For Authress specific verifiers, see the verifying JWTs
const axios = require('axios');
const { jwtVerify } = require('jose');
const jwkConverter = require('jwk-to-pem');
const ISSUER = 'https://login.authress.io';
const PUBLIC_KEY_URL = `${ISSUER}/.well-known/openid-configuration/jwks`;
class Authorizer {
async getUser(request) {
const authorization = Object.keys(request.headers).find(key => {
return key.match(/^Authorization$/i);
});
const token = request.headers[authorization] ? request.headers[authorization].split(' ')[1] : null;
if (!token) {
throw Error('Unauthorized');
}
const unverifiedToken = jwtManager.decode(token, { complete: true });
const kid = unverifiedToken && unverifiedToken.header && unverifiedToken.header.kid;
if (!kid) {
throw Error('Unauthorized');
}
const issuer = unverifiedToken && unverifiedToken.payload && unverifiedToken.payload.iss;
if (!issuer) {
throw Error('Unauthorized');
}
const key = await this.getPublicKey(PUBLIC_KEY_URL, kid);
try {
const pemKey = await importJWK(key);
const options = { algorithms: ['EdDSA'], issuer };
const verifiedToken = await jwtVerify(token, pemKey, options);
return identity.sub;
} catch (exception) {
throw Error('Unauthorized');
}
}
async getPublicKey(jwkKeyListUrl, kid) {
if (!this.publicKeysPromises[jwkKeyListUrl]) {
this.publicKeysPromises[jwkKeyListUrl] = axios.get(jwkKeyListUrl);
}
try {
const result = await this.publicKeysPromises[jwkKeyListUrl];
const jwk = result.data.keys.find(key => key.kid === kid);
if (jwk) {
return jwkConverter(jwk);
}
// If the public key isn't found it could be because this token was signed with a new public key, so try fetching a new version
const retryResult = await axios.get(jwkKeyListUrl);
const newJwk = retryResult.data.keys.find(key => key.kid === kid);
if (newJwk) {
this.publicKeysPromises[jwkKeyListUrl] = retryResult;
return jwkConverter(newJwk);
}
// Otherwise this is an old jwk, so fall through and throw Unauthorized Error
} catch (error) {
// If there is a problem looking up the keys, we have no choice but to return a 401 to the caller.
// * It's possible that there was a problem connecting to the jwks endpoint. In those cases, adding automatic retries to the HTTP calls here, is recommended
// * If the retries don't work, and there is still a problem, return a 401 to the caller.
}
throw Error('Unauthorized');
}
}
module.exports = Authorizer;