This blog post discusses tokens and their implementation considerations used by OAuth 2.0 and OpenID Connect protocols. The specifications are flexible and leave a lot of decision power to system developers. What kind of aspects should I consider when working with tokens?
OAuth 2.0 is an industry-standard authorization protocol enabling third-party applications to obtain access to HTTP services. OAuth 2.0 is targeted for wide range of devices and applications like web, desktop and mobile apps as well as IoT devices. OpenID Connect (OIDC) 1.0 is an identity layer on top of the OAuth 2.0 protocol. In addition to obtaining authorization it enables clients to verify the identity of an end user.
These protocols rely heavily on tokens, digital representations of information traded between different parties of the system. For example, tokens are used for transferring and storing information about authorization of users and applications, user identities and logout events. In its simplest form token is a short unique sequence of characters:
To make such token useful, typically some other information related to it is stored to the system. That can be for example the subject of the token and expiration time stored to a database. Another approach to tokens is JSON Web Token
(JWT). It embeds payload into a token and enables signing it in a secure way so that both the creator and receiver can verify the authenticity of the data. Example of a JWT:
Most people who have worked on implementing authentication are likely familiar with JWT. What might surprise many is that JWT is not required in most of the use cases where tokens are utilized. Access tokens and refresh tokens can be non-JWT equally well. OAuth 2.0 or OIDC specification doesn't specify the format of them - that is left as an implementation detail for developers.
JWT is not as simple as it looks
Fundamentally the concept of JWT is simple. It consists of three Base64 encoded parts separated by dots: header, payload and signature. However, the way JWT can be utilized in applications using OAuth 2.0 or OIDC is not that straightforward.
Header contains general parameters of the token, typically 'typ' describing the type of the token and 'alg' describing the signing algorithm used for signing the token.
Payload consists of name-value pairs called claims formatted as a JSON object. As a baseline, JWT specification lists seven claims, none of them being mandatory. Regarding ID Tokens, OIDC specification lists ten claims, five being mandatory. What might be surprising, JWT ID ('jti') is not in the list of those ten claims. However, the specification allows including any unspecified claims to ID Tokens. Someone implementing authentication with OIDC can’t rely just on the spec: implementations between OIDC providers vary. The following table describes claims specified by JWT and OIDC ID Token specifications:
Signature is calculated from the header, payload and a secret. Signature is used for validating that the token is not manipulated. Token validation is not only mandatory in the authorization server but also in the client application. That is the only way to securely verify that the payload can be trusted – in case of ID Token, the identity of the end user.
There are two primary methods and few algorithms to sign tokens, specified by JSON Web Algorithms
- Asymmetric algorithms using public/private key pair (RS256, ES256, PS256 etc.). OIDC provider signs tokens with the private key and delivers the corresponding public key to client applications with some method, for example implementing the OIDC metadata endpoint. The default method to sign ID Tokens is RS256.
- HMAC based symmetric algorithms (HS256, HS384 or HS512), which use a single secret that is shared with the client. In the context of ID Token that must be the OAuth 2.0 Client Secret value. If the client secret can’t be stored securely to a client app, for example in case of clients using the implicit flow, the second signing method is not an option. Also, because clients are able to create and sign valid tokens with the secret, never apply this method using a shared secret to access tokens or refresh tokens.
Visibility of payload
Developers must be mindful about what kind of data gets included to a token. By default, JWT token payload is just Base64 encoded. It means that anyone getting an access to a token can decode and see its payload. If a developer decides to implement access tokens as JWT, the payload will fall all the way to the end user’s user agent and is eventually readable by the user. Typically access tokens only contain information needed by the authorization server itself. As a rule of thumb, include just the necessary information to an access token, nothing else.
To prevent the problem of leaking secure information to wrong hands, OIDC specifies an option to encrypt JWT payload. With that method the actual payload can be made visible only to the party having the decryption key: typically the client application but it can be the authorization server only.
Complexity and performance
After all the choice between plain text tokens and JWT is a trade-off between complexity of the implementation and system’s performance. JWT can contain all the information authorization server or even client application needs for authorizing a user, preventing additional network requests or database lookups. However, it’s typical that authorization servers support token revocation. If a token is revoked, there is no method to include that information to an already assigned token. In that case a network request and/or database lookup is required anyway.
The complexity of how tokens are managed grows if certain OIDC specifications are supported. Logout specification requires authorization server to keep track of ID Tokens assigned to specific clients, if logout session is supported. If refresh tokens are supported, authorization server should revoke them upon logout, unless they were issued with the offline scope. The idea about independent all-inclusive tokens suffers when a system has relations to tokens and needs to keep track of them.
Considering performance, JWT does not necessarily improve it always. As demonstrated in the beginning of this blog post, the size of a JWT compared to a plain text token is considerably larger – and that example had just three claims in the payload. Think about an IoT device sending small amounts of measurement data in a short interval. A large token included to every single network request can increase the amount of network traffic significantly.
However tokens are coded and managed, attention needs to be paid to their lifetime. A long living token is easy to use as it needs to get renewed rarely. On the other hand, longer lifetime is a bigger security risk since the probability of token getting to wrong hands increases. In some use cases, like devices in a private network, that risk might be smaller making a long lifetime a justifiable option.
Giving tokens a shorter lifetime introduces new problems. Token expiring at the wrong moment might appear as undesirable behavior in the user experience or even lead to errors. OAuth 2.0 specifies the concept of refresh tokens which is a solution to that problem. With refresh tokens access tokens can be renewed seamlessly behind the scenes when needed. A downside is the complexity of implementation. It is one extra step of logic to trade refresh tokens to access tokens. In some cases added complexity might not be worth it.