In order to make policy decisions we commonly need to know the identity of the caller. Traditionally this has often been done by providing a user or client identifier along with the request, and using that identifier to look up further information like user details or permissions from a remote data source. While this model works fairly well for many applications, it scales poorly in distributed systems such as microservice environments. If multiple services are involved serving a single request from a user, having them all ask an external service or database for information about the user would effectively mean doubling the number of total requests sent, and all in order to retrieve the same data for each service!
JSON web tokens
To deal with this, OAuth2 established a basic protocol for obtaining access tokens, which would be issued by the authorization server and passed around between services in order to identify and authorize the caller. Using opaque tokens however poses many of the same problems as those faced when passing around user identifiers - rather than asking a database for more information the authorization server would instead have to be consulted whenever additional data was needed. While the OAuth2 specification leaves it to the implementation to decide on the format of the tokens, JSON Web Tokens (JWTs) have emerged as the most common one. Using JWTs as tokens include a number of benefits.
- Self-contained - JWTs contain most, or all, of the details needed to identify the caller.
- Signed - JWTs are signed by the issuer, meaning their authenticity can be verified by the receiver.
- Immutable - a JWT can't be changed anywhere in the request chain without invalidating the signature.
The OpenID Connect standard took it one step further, and made JWTs the format for tokens, encoding user identity in a new token type called ID tokens. This, along with many other enhancements added a much welcome identity layer on top of the quite minimal OAuth2 standard. For the purpose of this blog, we will deal with both access tokens and ID tokens as JWTs.
Token issuers, discovery and metadata
Any JWT issued by an OAuth2 or OpenID Connect authorization server will carry the URL of the issuer as part of the iss claim in the token payload. This value must be matched against the expected issuer(s) when verifying the token. OpenID Connect standardized on requiring the issuer to be a valid HTTPS URL pointing to the address of the issuer. This URL can be used for discovery purposes, obtaining information from the issuer, like available endpoints, that would otherwise need to be kept in applications and policies. The metadata endpoint of any compliant OpenID Connect server is found by appending the /.well-known/openid-configuration endpoint to the issuer URL. In order to reduce the number of configuration options coded into our policies, we may query this URL and use the endpoints provided in the response for further interactions with the authorization server.
While not originally part of the OAuth2 specification, the metadata specification from OpenID Connect has later been "backported" to OAuth2. Since all endpoints are practically the same, it is however common to only use the OpenID Connect metadata endpoint where available.
JWKS endpoint and token verification
A JSON Web Key Set is a collection of cryptographic keys in JSON format. The public key components of these are used to verify the authenticity of JWTs and can be provided as is to the JWT verification functions of Open Policy Agent (OPA). While these could be pulled into OPA as part of data bundles or by other means provided to a policy, the preferred way is to query the authorization server directly. Standards compliant token issuers issuing tokens as JWTs will publish the public key component of any signature keys it uses at its JWKS endpoint (jwks_uri from the metadata endpoint). Any key published there will have a key ID (kid), which will also be present in the header of any JWT signed by the issuer. Using OPA's http.send function we can turn directly to the JWKS endpoint of the issuer to retrieve the public keys needed for token verification.
Many authorization servers employ key rotation schemes for their token issuers. The OpenID Connect specification describes what such a rotation scheme would normally look like:
“Rotation of signing keys can be accomplished with the following approach. The signer publishes its keys in a JWK Set at its jwks_uri location and includes the kid of the signing key in the JOSE Header of each message to indicate to the verifier which key is to be used to validate the signature. Keys can be rolled over by periodically adding new keys to the JWK Set at the jwks_uri location. The signer can begin using a new key at its discretion and signals the change to the verifier using the kid value. The verifier knows to go back to the jwks_uri location to re-retrieve the keys when it sees an unfamiliar kid value. The JWK Set document at the jwks_uri SHOULD retain recently decommissioned signing keys for a reasonable period of time to facilitate a smooth transition.”
Depending on the time frame in which the old keys are still present in the JWKS - and the lifetime of issued tokens - we might not need to do anything for the rotation to work with the simple policy above. Just tweak the force_cache_duration_seconds value to something lower and it should pick up both the old and the new keys for as long as there are tokens issued signed with one of the keys about to be decommissioned. For more aggressive rotation schemes, or those where the time frame is variable or unknown, we might however want to add measures in place to cache both a copy of the old JWKS as well as the new one rotated in, and consult both of them for token verification:
In the above example, presence of the kid from the token header is checked for within the cached JWKS, and only if not found there we assume that the keys have been rotated and make a new request which we also cache. This handles key rotation while putting minimal load on the authorization server.
While a token is often provided as part of the input to OPA, certain scenarios require the token to be retrieved programmatically. An example of this is when two services need to communicate without a user being involved. For those cases we may use the OAuth2 client credentials flow to authenticate a client at the token endpoint in order to obtain an access token. We can then use the token obtained for subsequent requests to OAuth2 protected resource servers.
In order to avoid retrieving a token for each request sent, make sure to use the caching options of http.send appropriately. Note that the access token lifetime will be provided in the expires_in attribute of the response from the authorization server, so ideally you'd set force_cache_duration_seconds to that minus a few seconds.
Though less commonly used, the resource owner password credentials flow looks almost identical to the client credentials example provided above. Simply change the grant_type value in the body to "password" and add username and password parameters.