Photo by Anita Jankovic on Unsplash
The last piece of the microservice architecture puzzle to cover is
Authentication and
Authorization. As the reader should expect by now, these aspects are handled differently in microservice architectures.
Authorization: Authorization identifies what functions an authenticated user is permitted to perform.
Once authenticated, the application retrieves the session object on each request and verifies the user has sufficient privileges to access the requested feature. If the user has permission the requested resource is returned; otherwise, the application returns an HTTP Client error (usually a 401 Unauthorized status).
In a microservice architecture, each service executes within a separate process and is no longer co-located with the authenticated session. This situation poses a problem as each service must still authenticate and authorized access. We have a couple of approaches we can consider to solve this problem: Session Affinity, Distributed sessions & Client Tokens.
Each token returned from the authentication service contains the user's identity information and claims (analogous to permissions). These are usually encrypted to avoid it being hijacked by a malicious third-party or modified by the caller to escalate their privileges.
As luck would have it, there is a well-known open standard that provides us a token model: the JSON Web Token (JWT).
JWT is an open standard that provides a safe way of transmitting information between parties using a JSON object. The information is digitally signed using a shared secret using the HMAC algorithm or with a public/private key pair (e.g., RSA or ECDSA).
JWTs can be encrypted to provide secrecy between parties and can be signed to verify the integrity of the token's claims. When employing a public/private key pair, the signature certifies that only the party with the private key signed it.
When a client successfully logs in, a JWT is created containing the user's identity and a set of claims about the user. This JWT is then passed with every subsequent request to every secured service. Each service then inspects the JWT, verifies it's signature, decodes its payload to determine if the JWT token's claims match the service's requirements.
The following table contains all the registered claims declared in the RFC:
Here is an example JWT payload:
A simple JWT utility app is available here if you want to try it out. Using the header and payload information we have described previously we get the following JWT:
The receiving service inspects the Authorization header to validates its signature and performs any necessary decryption to read the claims of the token. If the token is valid, the service will verify the claims of the token and grant the appropriate privileges to the caller.
Coming to terms with terms
Authentication: Authentication is the process of validating the identity of a user based on a set of credentials (e.g.username and password).Authorization: Authorization identifies what functions an authenticated user is permitted to perform.
Monolithic Authentication & Authorization
In monolithic applications, the authentication and authorization process commonly employs session-based authentication. This approach usually consists of both a user model containing credentials and a user-role model which maps the user to a set of roles associated with specific application permissions. When a user authenticates via the application's login process, the application creates a session object and populates it with the user's roles. This session object is associated with with a session identifier that is used to find the session on subsequent client requests.Once authenticated, the application retrieves the session object on each request and verifies the user has sufficient privileges to access the requested feature. If the user has permission the requested resource is returned; otherwise, the application returns an HTTP Client error (usually a 401 Unauthorized status).
The perils of Session-based authentication
Monolithic architectures often employ session-based authentication & authorization since the session object is co-located within the same process as the application code.In a microservice architecture, each service executes within a separate process and is no longer co-located with the authenticated session. This situation poses a problem as each service must still authenticate and authorized access. We have a couple of approaches we can consider to solve this problem: Session Affinity, Distributed sessions & Client Tokens.
Session Affinity
One common solution used is Session Affinity. Session affinity is a process (usually achieved through a load-balancer) that routes the caller to the same instance in which they first authenticated to ensure that the user's requests are processed on the same host where the user authenticated. Unfortunately, using session affinity often distorts load across the cluster as every user has a different usage pattern. Additionally, if the instance crashes or becomes unreachable, the load balancer is then forced to redirect subsequent requests to another instance forcing the user to re-authenticate.Distributed Sessions
In the spirit of full disclosure, the description of session-based authentication and authorization described earlier is a simplified version applicable to non-clustered applications. Once a monolithic application is clustered, we begin to run into the same problems as we do in the microservice world. We have seen how session affinity can undermine load-balancing, so instead of routing the user to the authenticated session, we look at two ways to make the session available to distributed services: Replicated Sessions, and Centralized Sessions.Replicated Sessions
One option is replication sessions. In this approach, when a session is created, it is replicated across all the instances in the cluster. This option allows each cluster to service any of the authenticated users as the session data is now visible to each instance. Unfortunately, sessions are often used for more than just authentication. If the session object is large, the performance penalty imposed by replicated sessions can lead to consistency issues when synchronizing across many instances.Centralized Sessions
Another approach would be to centralize all the session objects. When a user authenticates, its session is stored in a highly-available and scalable Session service. When a service is invoked, it retrieves the session object from the Session service and determines if the user has sufficient privileges. While this approach provides a workable solution, it suffers from the added overhead needed to retrieve the centralized session object. This additional overhead is added to every secured request and increases the total response time for every request.Client Tokens
As we have just seen authentication and authorization with a session object can have non-trivial performance ramifications due to the stateful nature of sessions. An alternative approach is to use Client Tokens. With Client Tokens, the caller still authenticates with their credentials, but instead of storing permissions in a session a token is returned to the caller and passed with each subsequent service request to gain access to the desired resource. By moving the authenticated state from the server to the client, we are able to address the limitations of session-based systems.Each token returned from the authentication service contains the user's identity information and claims (analogous to permissions). These are usually encrypted to avoid it being hijacked by a malicious third-party or modified by the caller to escalate their privileges.
As luck would have it, there is a well-known open standard that provides us a token model: the JSON Web Token (JWT).
Json Web Token (JWT)
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.
Internet Engineering Task Force (IETF) - RFC7519
Internet Engineering Task Force (IETF) - RFC7519
JWT is an open standard that provides a safe way of transmitting information between parties using a JSON object. The information is digitally signed using a shared secret using the HMAC algorithm or with a public/private key pair (e.g., RSA or ECDSA).
JWTs can be encrypted to provide secrecy between parties and can be signed to verify the integrity of the token's claims. When employing a public/private key pair, the signature certifies that only the party with the private key signed it.
When a client successfully logs in, a JWT is created containing the user's identity and a set of claims about the user. This JWT is then passed with every subsequent request to every secured service. Each service then inspects the JWT, verifies it's signature, decodes its payload to determine if the JWT token's claims match the service's requirements.
JWT Structure
Each JSON Web Token is comprised of three elements: Header, Payload, and Signature. In its compact form the token is comprised of three parts separated by periods:
xxxxx
.yyyyy
.zzzzz
Header
The header consists of two parts: the token's type, which should be JWT, and the algorithm used to sign it (e.g., HMAC SHA256 or RSA).
{
"alg": "HS256",
"typ": "JWT"
}
The following table includes the authentication elements:
Code | Name | Description |
---|---|---|
alg | Algorithm | The algorithm used to verify the signature of the token. |
cty | Content Type | Usually omitted. This field should be set to JWT if nested signing or encryption is used. |
type | Token Type | When present, it should be set to JWT. |
Payload
The payload portion of the token contains the token's claims. Claims declare statements about an entity as well as additional application-specific information. The payload contains three types of claims : registered, public, and private.Registered Claims
Registered claims are a set of predefined commonly used claims intended to support interoperable claims. They are recommended but not mandatory.The following table contains all the registered claims declared in the RFC:
Code | Name | Description |
---|---|---|
aud | Audience | Identifies the token's intended audience. |
exp | Expiration Time | The value must be a NumericDate representing seconds past 1970-01-01 00:00:00Z. The token is not considered valid when used after the expiration time. |
iat | Issued At | The value must be a NumericDate and represents the time the token was issued. |
iss | Issuer | Identifies the principal that issued the token. |
jti | JWT token id | This identifier uniquely identifies the token across all token issuers. |
nbf | Not Before | A NumericDate that identifies the earliest time which the token is valid for processing. |
sub | Subject | Identifies the subject of the token. |
Public Claims
Public claims can be declared at the discretion of the application developers. However, to avoid collisions with other public claims, it is recommended that the claims are registered with IANA.org. The most current list of registered public claims can be found here.Private Claims
Private claims are used exclusively between parties who agree to share them. They should not collide with registered or public claim types.Here is an example JWT payload:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
NOTE: While signed tokens are protected against tampering their contents are still visible unless they are encrypted.
Signature
To ensure that the token hasn't been compromised in transit, a signature block is used. It is calculated using the algorithm defined in the header applied to the concatenation of the header and the payload (both Base64 encoded) separated with a period:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
This signature is concatenated with the Base64 encoded header and Base64 encoded payload to form the compact JWT.
A simple JWT utility app is available here if you want to try it out. Using the header and payload information we have described previously we get the following JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Using the JWT
When calling a JWT secured service, the token is usually added to the requests Authorization header using the Bearer schema.
Authorization: Bearer <token>
The receiving service inspects the Authorization header to validates its signature and performs any necessary decryption to read the claims of the token. If the token is valid, the service will verify the claims of the token and grant the appropriate privileges to the caller.
Twitter
Facebook
Reddit
LinkedIn
Email