If you already have an idea on stateless authentication and JWT then proceed with this implementation blog otherwise just go through the previous blog Stateless Authentication to get an idea.
As i mentioned in my previous blog JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA.
Client can access the the resources from different applications. So to validate the token at applications, we require the secret or a public/private key.
Problems of validating the token in every application
- We have to maintain the secret key in all the applications and have to write or inject the token validation logic in every application. The validation logic may include more than token validation like fingerprint mismatch, session idle time out and many more based on the requirement.
- If the applications are developed in different languages then we have to implement the token validation logic based on application technology stack and maintenance is very difficult.
Solution
Instead of maintaining the validation logic in every application, we can write our validation logic at one common place so that every request can make use of that logic irrespective of application (Note: Here Applications could be developed in any language). I have chosen reverse proxy server (Nginx) to maintain the validation logic with the help of Lua.
Advantages
- We don’t need to maintain the secret or private/public key in every application. Just maintain at authentication server side to generate a token and at proxy server (Nginx) to validate the token.
- Maintenance of the validation logic easy.
Before jumping in to the flow and implementation let’s see why we have chosen this technology stack.
Why JWT ?
To achieve the stateless authentication we have chosen JWT (JSON Web Token). We can easily, securely transmitting information between parties as a JSON object. If we want to put some sensitive information in JWT token, we can encrypt the JWT payload itself using the JSON Web Encryption (JWE) specification.
Why Nginx + Lua ?
Nginx+Lua is a self-contained web server embedding the scripting language Lua. Powerful applications can be written directly inside Nginx without using cgi, fastcgi, or uwsgi. By adding a little Lua code to an existing Nginx configuration file, it is easy to add small features.
One of the core benefits of Nginx+Lua is that it is fully asynchronous. Nginx+Lua inherits the same event loop model that has made Nginx a popular choice of webserver. “Asynchronous” simply means that Nginx can interrupt your code when it is waiting on a blocking operation, such as an outgoing connection or reading a file, and run the code of another incoming HTTP Request.
Why Memcached ?
To keep the application more secured, along with the token validation we are doing the fingerprint check and handling idle time out as well. Means, if the user is idle for some time and not doing any action then user has to be logged out from the application. To do the fingerprint check and idle time out check, some information needs to be shared across the applications. To share the information across the applications we have chosen Memcached (Distributed Cache).
Note: If you don’t want to do fingerprint mismatch check and idle time out check, then you can simply ignore the Memcached component from the flow.
Flow
Step 1
Client try to access the resource from the application with out JWT token or invalid token. As shown in the flow, request goes to the proxy server (Nginx).
Step 2
Nginx looks for the auth header (X-AUTH-TOKEN) and validates the token with the help of Lua.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
— Get the token from header
local token = ngx.req.get_headers()[“X-AUTH-TOKEN”];
if token == nil then
status = “NO_TOKEN”;
else
— Verifying the token with secret key
local jwt_obj = jwt:verify(“SampleSecretKey”,token,0)
if not jwt_obj[“verified”] then
status = “INVALID_TOKEN”;
end
end
— Building json response from Nginx using Lua
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.header.content_type = “application/json; charset=utf-8”
ngx.say(cjson.encode({ status = status }))
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
|
Step 3
As token is not present or invalid, nginx sends below response to the client.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
If JWT token is not present in the header
Status code : 401
{
“status” : “NO_TOKEN”
}
If JWT token is invalid
Status code : 401
{
“status” : “INVALID_TOKEN”
}
|
Step 4
Now user has to login in to the system, So client will load the login page.
Step 5
Client will send a request to the authenticate server to authenticate the user. Along with username and password client sends the fingerprint also. Here we are considering fingerprint to make sure that all the requests are initiating from the same device where user logged in to the system.
Sample authenticate request body
1
2
3
4
5
|
{
“username” : “sample_user”,
“password” : “**********”,
“fingerprint” : “97c73b6a8687c0579b81d7c67d50a87d”
}
|
Step 6
Authenticate server validates the credentials and create a JWT token with TokenId (random generated UUID) as a claim and this tokenId is useful to uniquely identify the user. And set the JWT token in response header (X-AUTH-TOKEN).
Create JWT Token
Add this dependency to your pom.xml to work on JWT
1
2
3
4
5
|
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.4</version>
</dependency>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClaims;
import io.jsonwebtoken.impl.TextCodec;
public class TokenHandler {
/**
* here custom claim is a java object and it can contain any type of data
* here tokenId is a randomly generated UUID
*/
public String createToken(CustomClaim customClaim, String tokenId) {
// Create a claim and put the data in to a claim.
Claims claims = new DefaultClaims();
claims.put(“account”, customClaim);
claims.put(“tokenId”, tokenId);
// Here secretKey could be any thing.
return Jwts.builder()
.setClaims(claims)
.setSubject(customClaim.getUserName())
.signWith(SignatureAlgorithm.HS256, TextCodec.BASE64.encode(secretKey))
.compact();
}
}
|
While creating the token you can set any number of claims.
CustomClaim.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
public class CustomClaim {
private String userName;
private String firstName;
private String lastName;
private String emailId;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmailId() {
return emailId;
}
public void setEmailId(String emailId) {
this.emailId = emailId;
}
}
|
Generated JWT token looks like below
1
|
eyJhbGciOiJIUzI1NiJ9.eyJBY2NvdW50Ijp7InVzZXJOYW1lIjoic2FtcGxlX3VzZXIiLCJmaXJzdE5hbWUiOiJTYW1wbGUiLCJsYXN0TmFtZSI6IlVzZXIiLCJlbWFpbElkIjoic2FtcGxlLnVzZXJAaW1hZ2luZWEuY29tIn0sIlRva2VuSWQiOiI3NDg2MGUxMS04ODI1LTQxMGItYTA5OC00ZDczNTliMzI2MmQiLCJzdWIiOiJzYW1wbGVfdXNlciJ9.xztiJD4nhZqmczaodMXSLu1–daH5faRtkpXlcO4xcMY
|
And the JWT token payload looks like below. You can put what ever data you want like roles & permissions associated to him and so on…
1
2
3
4
5
6
7
8
9
10
|
{
“Account”: {
“userName” : “sample_user”,
“firstName” : “Sample”,
“lastName” : “User”,
“emailId” : “sample.user@imaginea.com”
},
“TokenId” : “74860e11-8825-410b-a098-4d7359b3262d”,
“sub” : “sample_user”
}
|
Step 7
Put TokenId as a key and user meta information like fingerprint, last access time etc… as a value in memcached which is useful to verify the fingerprint and session idle time out at nginx side using Lua.
Sample Memcached content
1
2
3
4
5
6
7
|
<key> : <value>
“74860e11-8825-410b-a098-4d7359b3262d” : {
“fingerprint” : “97c73b6a8687c0579b81d7c67d50a87d”,
“sessionIdleTimeOut” : 60,
“lastAccessTime” : “17-JUN-2016 10:42:55”
}
|
Put Content in Memcached
Add this dependency to your pom.xml to work on Memcached
1
2
3
4
5
|
<dependency>
<groupId>net.spy</groupId>
<artifactId>spymemcached</artifactId>
<version>2.12.0</version>
</dependency>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import net.spy.memcached.MemcachedClient;
public class MemcachedHandler {
MemcachedClient memcachedClient = new MemcachedClient(new InetSocketAddress(“localhost”, 11211));
/**
* tokenId is randomly generated UUID which is same as what we put it in a JWT token in * step 6.
* fingerPrint value is what we get in authenticate request body.
*/
public void setContentInMemcached(String tokenId, String fingerPrint, int idleTimeOut){
MemcachedObject memcachedObject = new MemcachedObject();
memcachedObject.setFingerPrint(fingerPrint);
memcachedObject.setIdleTimeOut(idleTimeOut);
SimpleDateFormat ft = new SimpleDateFormat(“dd-MM-yyyy HH:mm:ss”);
memcachedObject.setLastAccessTime(ft.format(new Date()));
String value = objectMapper.writeValueAsString(memcachedObject);
putContentInMemCache(tokenId, value);
}
public void putContentInMemCache(String key, String value) {
memcachedClient.set(key, 60*60*24*30, value);
}
}
|
Step 8
Send back response to the client from authentication server with response header X-AUTH-TOKEN
1
|
X–AUTH–TOKEN : eyJhbGciOiJIUzI1NiJ9.eyJBY2NvdW50Ijp7InVzZXJOYW1lIjoic2FtcGxlX3VzZXIiLCJmaXJzdE5hbWUiOiJTYW1wbGUiLCJsYXN0TmFtZSI6IlVzZXIiLCJlbWFpbElkIjoic2FtcGxlLnVzZXJAaW1hZ2luZWEuY29tIn0sIlRva2VuSWQiOiI3NDg2MGUxMS04ODI1LTQxMGItYTA5OC00ZDczNTliMzI2MmQiLCJzdWIiOiJzYW1wbGVfdXNlciJ9.xztiJD4nhZqmczaodMXSLu1–daH5faRtkpXlcO4xcMY
|
Step 9
Fetch the token from response header and store it in local storage at client side. So that we can send this token in request header from next request onwards.
Step 10
Now client access the resource from application with valid JWT token. As shown in the flow request goes to the proxy server (Nginx). With every request client will send a fingerprint in some header and consider header name as “FINGER-PRINT”.
Step 11
Nginx validates the token. As token is valid, extract the TokenId from the JWT token to fetch the user meta information from memcached.
If there is no entry in the memcached with “TokenId” then Nginx simply senda a response as “LOGGED_OUT” to the client.
1
2
3
4
5
6
|
If there is no entry in the Memcached with TokenId, means user logged out
Status code : 401
{
“status” : “LOGGED_OUT”
}
|
But in our case user is logged in into the system, So there will be an entry in memcached with TokenId. So fetch that user meta information to do the following checks.
Fingerprint mismatch : While sending the authenticate request, client is sending fingerprint along with username and password. We are storing that fingerprint value in memcached and we use this value to compare with the fingerprint which is coming in every request. If fingerprint matches, then it’s proceed further. Otherwise nginx will send a response to client saying that fingerprint is mismatched.
1
2
3
4
5
6
|
If finger print mismatch happens
Status code : 401
{
“status” : “FINGERPRINT_MISMATCH”
}
|
Session idle time out : While successful authentication of a user at authentication server side, we are putting configured session_idle_timeout of a user in memcached. If it’s configured as “-1”, then we simply skip the session idle time out check. Otherwise for every request just we check whether session is idle or not. If session is not idle, we update the last_access_time value to current system time in memcached. If session is idle then Nginx send below response to the client.
1
2
3
4
5
6
|
If session is idle
Status code : 401
{
“status” : “SESSION_IDLE”
}
|
Complete Validation Logic at Nginx using Lua
base-validation.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
local jwt = require “resty.jwt”
local memcached = require “resty.memcached”
local http = require “resty.http”
local cjson = require “cjson”
local date = require “date”
local redirect = “false”;
— Code block to connect to the Memcached
local memc, err = memcached:new()
local ok, err = memc:connect(“127.0.0.1”, 11211)
— Fetch the token from header
local token = ngx.req.get_headers()[“X-AUTH-TOKEN”];
if token == nil then
redirect = “true”;
status = “NO_TOKEN”;
else
— Verify the token using secret key
local jwt_obj = jwt:verify(“SampleSecretKey”,token,0)
if not jwt_obj[“verified”] then
redirect = “true”;
status = “INVALID_TOKEN”;
else
— Fetching data from JWT payload
local userName = jwt_obj.payload.sub;
local tokenId = jwt_obj.payload.TokenId;
— Get the content object from Memcached
local res = memc:get(tokenId)
if res == nil then
redirect = “true”;
status = “LOGGED_OUT”;
else
— Fetch the data from memcached content object
local jsonValue = cjson.decode(res)
local lastAccessTime = jsonValue.lastAccessTime;
local idleTimeOut = jsonValue.idleTimeOut;
local fingerprint = jsonValue.fingerprint;
— Fetch the fingerprint value from header
local fingerprintHeader = ngx.req.get_headers()[“FINGER-PRINT”]
if fingerprint ~= fingerprintHeader then
redirect = “true”;
status = “FINGERPRINT_MISMATCH”;
— Session idle check
else if idleTimeOut ~= –1 then
local currentTime = date(false);
currentTime = currentTime:fmt(“%d-%m-%Y %H:%M:%S”);
local currentDate = date(currentTime)
local updatedDate = date(lastAccessTime):addminutes(idleTimeOut);
updatedDate = date(updatedDate);
jsonValue.lastAccessTime = currentTime
local jsonString = cjson.encode(jsonValue)
if updatedDate < currentDate then
local ok,err = memc:delete(tokenId)
redirect = “true”;
status = “SESSION_IDLE”;
else
— Set the content in memcached
local ok,err = memc:set(tokenId,jsonString)
end
end
end
end
end
end
local ok, err = memc:close();
if redirect == “true” then
— Build json response at Nginx using Lua
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.header.content_type = “application/json; charset=utf-8”
ngx.say(cjson.encode({ status = status }))
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
|
Step 12
Once the request gone through the above mentioned validation logic, Nginx proxy_pass the request to the application.
sample-nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
server {
listen 443;
server_name blog.imaginea.com;
ssl on;
ssl_certificate default.crt;
ssl_certificate_key default.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
ssl_ciphers “EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH”;
ssl_prefer_server_ciphers on;
access_log access.log;
error_log error.log;
# We included “base-validation.lua” in “/” location. Means every request goes through the whole validation logic.
location / {
rewrite_by_lua_file base–validation.lua;
proxy_pass http://imaginea;
}
# For authenticate request there shouldn’t be any validation. So just proxy_pass the request
location /authenticate {
proxy_pass http://imaginea;
}
}
|
Step 13
Application sends a response of requested resource to the client.
How to achieve logout ?
There is a open question (unanswered) regarding how to achieve the log out at server side, if we go by the stateless authentication using JWT.
Mostly people are discussing about handling the log out at client side.
- When user clicks on logout, simply client can remove the token from local storage.
But i come up with a solution to achieve the logout at server side by make use of Memcached.
- When user clicks on logout, Remove the entry from Memcached which we put it in Step 7. And client also can delete the token from local storage as well. If you see the validation logic which i have completely covered in Step 11, there i’m checking the entry in memcached. If there is no entry in memcached, means user logged out from the application.