Background
OAuth 2.0 spec provides guidance (RFC 6750) on how to procure access tokens and use them to access resources but doesn’t specify the format of the access tokens issued. The client should treat it as an opaque string and not make any assumptions or depend on the format of the token to be a certain way.
The OAuth 2.0 spec also provides guidance (RFC 7662) on how a resource server can introspect token metadata for an opaque token using an endpoint provided for this purpose by the authorization server. While this suggests the attributes that can be introspected and known about the token, it doesn’t require the token to be formatted any particular way or even be readable at all.
If the access token format (claims in it) is exposed to the clients in a readable way, they form a part of the API contract between the client and auth+service provider. There may/will be clients relying on the format and any change to the format would be a breaking change.
Note the distinction between ID token which is meant to be read by clients and therefore must be a JWT and an access token which is meant to be introspected by the resource server and not by the client. Therefore the access token (exposed externally to clients) should not be a JWT.
API client perspective
From client perspective we should NOT provide any guarantees about access token format and require the clients to treat tokens as opaque strings and not make any assumptions about their format or contents. The reason for this is straightforward:
-
By doing so we are not deviating from the OAuth 2.0 standard in any way.
-
The service retains flexibility to make changes to format and contents if need arises without worrying about breaking clients depending on the format.
-
Avoid the additional work of publishing token format publicly as part of API docs and keeping it up to date.
Additional guidelines for tokens handed over to clients
-
Access tokens which can be introspected by clients (if jwt is used for some reason) must not contain any fields considered secret or secure. Any fields added to such a token must undergo security review and approval.
-
Token size must not exceed 8K. While respective RFC have no constraints on the size of tokens and headers, there are practical limits imposed by intermediary network components and application servers.
Resource server perspective
It’s recommended to have a standard token format across all services belonging to a provider for the following reasons:
-
Makes it easy for a central authorization service to mint the token since all services follow same format.
-
Easy to build reusable libraries for token generation and validation when standard format is used.
-
No dependencies (network calls) to services for minting tokens which improves resiliency and performance. If each service follows a different token format then the authorization service will need to delegate token generation to the respective service which will necessitate the extra network call.
The above approach is feasible when all service start development after such a standard token format is defined. But reality could be different when services do development in silos and no standard format is initially defined or when services get added over time due to M&A activity. In such cases:
-
There is work entailed on each existing service to start understanding the new (standard) token format. For ex. the claims in the new token may be different from the one in the old token or the method of extracting the claims from a token may be different.
-
The service needs to continue processing the legacy token format till all its existing client flows migrate and all legacy tokens are invalidated. For ex. clients may still be using a token endpoint specific to the service which gives out legacy tokens. Such clients need to start communicating with the new token endpoint which gives out the standardized tokens.
Options for token format
Phantom token - Opaque string externally and JWT internally
Pros:
-
Secure because (external) token can’t be introspected to learn anything about it.
-
Additional level of indirection from opaque token to internal token format allows services to continue using their existing token format in the implementation.
-
Token revocation/invalidation is possible by deleting opaque token and its association from the map.
-
Centralized auth service can mint the opaque token string while the corresponding service can mint the actual token.
-
API gateway can be leveraged to implicitly validate, exchange the opaque token for the internal token without burdening services with this responsibility.
-
Flexible and decoupled approach because internal token formats can evolve without affecting other services, auth service and clients.
-
Follows the API Security Best Practices for: Use API Gateway, Opaque token externally JWT internally & Use a Central OAuth Server.
Cons:
-
We need to have an endpoint for validating the opaque tokens and looking up the corresponding internal tokens.
-
Additional processing to validate and lookup tokens will add some latency to each request.
-
Relies on services to mint the internal token and return to auth service creating a runtime dependency on them for the case when service has a legacy token format in use.
JWT with common (denominator) claims
Pros:
-
If common claims can be agreed to across services, the auth service can mint and issue JWT with common (denominator) claims.
-
It’s possible to implement common libraries for generating and validating such tokens.
-
Auth service can generate tokens in a decoupled way (without runtime dependency on the services).
-
Services/APIGW can introspect tokens without calling external endpoints because tokens are self-contained and tamper-proof.
-
Follows the API Security Best Practices for: Common/Reusable JWT validation library
Cons:
-
Less secure because tokens can be introspected unless we encrypt the payload.
-
If there are many services existing with multiple differences in the existing token formats, it may need lot of effort to drive this type of unification. If the service depends on unique claims (specific to the service) then this approach may not be feasible.
-
When new services are added through merger/acquisition, the common denominator needs to be re-evaluated. Adding the service to the fold will require it to accept a totally new format.
-
Care to be to taken that no claims have PII.
-
Clients may create dependencies on claims in the token resulting in coupling.
JWE tokens
Pros:
-
Secure because the tokens are encrypted and not readable by clients.
-
There is no need for token translation, which saves time and resources at the intermediate layers (API gateway, auth server).
-
Token format and contents are not exposed to clients allowing us flexibility to evolve them and also not causing confidential info leaks.
-
The token has all the relevant information needed by the resource server for doing business logic including PII.
Cons:
-
Clients having possession of these tokens gives them an opportunity to try and break the encryption.
-
It prevents trusted components like API Gateways from doing validation checks and common processing based on token introspection because the tokens are encrypted. The alternative is to provide private keys for all services to the API gateway which is not secure and scalable option. The other option is to use common private keys in all services and API gateway which is also not very secure.
-
Existing services which are not using JWE have to start decrypting tokens, understand and use the format.
In Conclusion
If you are starting afresh and have an API gateway in the architecture, then the phantom token approach gives good amount of flexibility and security. However if there are existing services with different token formats in use and there is no API gateway or only some services using API gateway then it requires more detailed understanding of the constraints and tradeoffs to arrive at a decision.