Modern applications more and more consist of microservices. And these applications need some sort of authentication and authorization mechanism. Let’s imagine a client component that communicates with a group of microservices, but every interaction needs to be authenticated and authorized.
Intro
Typically authentication and authorization logic utilizes tokens. That token is generated by some AuthService. AuthService can authenticate a user, let’s say by his username and password. Conceivable this service is able to authorize the particular users by matching rights & roles data.
Well knows protocols utilizing token by reference validation are kerberos, CAS, and OAuth. Not going into protocol details here, we can summarize, that basically there is Authorization Service that gives you some Ticket (or several) in exchange for provided Credential. In the case of OAuth2 you would get Access Token (and Refresh Token).
As shown in the diagram, once acquired, access token is used in all following service calls. The Receiving service need to check the validity of the token and maybe even query some details to the token (Identity, Authorization level).
‘By reference’ tokens
When modeled as reference token, every service needs to contact Authorization Service again on every interaction, because tokens are typically anonymous. This also means that a token is only a temporal reference to the Identity Data (User Data). The must be some routine that would allow service to fetch everything it needs using that token reference.
In the age of monoliths1 and stateful backend workers this approach had probably more advantages than disadvantages.
It might be mentioned here, that in microservices styled architectures, the services tending to be stateless (the state is inhibiting). At least having services that do not share a common state is often the desired goal. Also if services are tending to be small and simple, the complexity is not completely gone, it just moved out of services to the level of interactions and communication between the services and we need to be very careful here. Imagine 10 and more services as the backend of one Single Page Applications that need to talk to one authoritative service just to verify tokens on every request… Let me summarize why the utilization of by reference tokens protocols become less favorably:
- Authorization Service becomes a single point of failure, need to be always online (Even several seconds of downtime means complete outage.
- Every Microservice has to know where to find the AuthService and how to talk to it (adds code + complexity).
- Increases communication complexity
- By that increases complexity if infrastructure code (firewalls, properties with service names, more environment-dependent parts)
- Makes deployment complexer
- Makes development and tests complex (more mocking needed potentially)
- Don’t scale well with an increasing number of services
JSON Web tokens (by value)
So no surprize, that also other concepts existing in that field.:
token by value - tokens that contain and not reference all needed information* (at least for authentication & authorization)
Suck token can be validated in place, by the the microservice itself. Just add some cryptography on top.
This is exactly what JSON Web Tokens (JWT) are for!
JSON Web Token RFC standardized this approach. So how it works?
Utilizing cryptography you can take a portion of information including its validity period and authenticate this information still having relative short result to be suitable use as a token. This is how JWT works in one sentence.
Since the JWT could be validated in place, it enables us to build architectures that are far less coupled to the Authorization Service and potentially influencing the design of Authorization Service itself, by reducing its complexity drastically.
However, some complexity remains. There are two cryptographical ways to authenticate and both require cryptographical keys:
JWT crypto details discussion
With the shared key, you get the complexity and risks of shared key distribution (See detail on Symmetric Key Cryptography.
- First you need to distribute new keys relatively synchronously to all services or make services robust by checking several keys. Both add complexity to your micro or macro architecture.
- If you have to share key in an environment you can’t trust to 100% (technically or organizationally) it becomes a security problem. Everyone who has a key can sign tokens.
This is where asymmetric algorithms can help. Asymmetric algorithms maintain key-pair: public and private, the public key is designed to be not a secret, so you can share it with all services that need validate your tokens even with services, you can’t trust at all. You just need to secure your private key and the issuing service.
The main difference between RSA and ECDSA lies in the speed and key size. ECDSA requires smaller keys to achieve the same level of security as RSA. This makes it a great choice for small JWTs.
Keep in mind JWT does not encrypt your data, so do not put anything sensitive into it.
But it’s well suited being a token or hold userId (you should not have sensitive ids at all). Hover, nothing prevents you from taking this to the next level: You could also encrypt your JWT token. I will not cover it here. It’s just and idea.JWT code example
Maybe code example will undermine the previous advisement. Here an example of HMAC based shared key JWT use case. I this example i relay on jwtk/jjwt java library. Let’s start with maven dependencies.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
and here is code sample that generates JWT token
public String createToken(long userId, String userEmail, long ttlMillis) {
//Get current timestamp
long nowMillis = System.currentTimeMillis();
// Let's set the JWT Claims
JwtBuilder builder = Jwts.builder().setIssuedAt(new Date(nowMillis)).setSubject(String.valueOf(userId))
.setIssuer("MyAuthoritativeService")
.claim("email", userEmail)
.signWith(SignatureAlgorithm.HS256, jwtKey)
.setExpiration(getExpDate(nowMillis, ttlMillis));
// Builds the JWT and serializes it to a compact, URL-safe string
return builder.compact();
}
Here token is generated for userId
and userEmail
that are “backed in” as two claims. The getExpDate(nowMillis, ttlMillis)
method produces Expiration Date. Here jwtKey
is used as a pre-shared key. The resulting token may look like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZW1haWwiOiJteXVzZXJuYW1lQGdtYWlsLmNvbSIsInJvbGUiOiJtYXN0ZXIifQ.Le-mu-DstBdPhSoaeaN9Rl_TlIe05NLpsy2vs_q8c74
Let’s look at the validation code part:
Jws<Claims> claims = Jwts.parser().setSigningKey(jwtKey).parseClaimsJws(token);
String userId = claims.getBody().getSubject();//Subject is user ID in this example.
Object email = claims.getBody().get("email");
Here token is parsed by use of the Key (jwtKey
) and two backends in claims are extracted from the body. Of course, different exceptions may occur on parsing.
A token could be just invalid and signature violated or token can be already expired of course you should react to that exceptions.
JWT details
JWT is standardized by RFC7519. As you may be recognized, by example token, JWT consist of 3 parts:
- JOSE Header: JSON object containing the parameters describing the cryptographic operations and parameters employed. The JOSE (JSON Object Signing and Encryption)
- JWS Payload: The sequence of octets to be secured – a.k.a. the message. The payload can contain an arbitrary sequence of octets
- JWS Signature: Digital signature or MAC over the JWS Protected Header and the JWS Payload
I hope you like it. I will propose two token solutions with JWT next time.
-
Mentioning “Monoliths age” I mean the past. Maybe that term is too harsh. ↩︎
-
See also Symmetric Key Cryptography ↩︎