Stateless Authentication implementation using JWT, Nginx+Lua and Memcached

Datetime:2016-08-23 01:27:17          Topic: Memcached  Nginx  Lua           Share

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

  1. 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.
  2. 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

  1. 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.
  2. 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.

-- 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.

If JWTtokenis not presentin theheader
 
Statuscode : 401 
{
  "status" : "NO_TOKEN"
}
 
If JWTtokenis invalid
 
Statuscode : 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

{
    "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

<dependency>
 <groupId>io.jsonwebtoken</groupId>
 <artifactId>jjwt</artifactId>
        <version>0.4</version>
</dependency>
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(CustomClaimcustomClaim, String tokenId) {
// Create a claim and put the data in to a claim.
Claimsclaims = 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

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

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…

{
  "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

<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

<dependency>
 <groupId>net.spy</groupId>
 <artifactId>spymemcached</artifactId>
 <version>2.12.0</version>
</dependency>
import net.spy.memcached.MemcachedClient;
 
public class MemcachedHandler {
 
MemcachedClientmemcachedClient = 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){
 MemcachedObjectmemcachedObject = new MemcachedObject();
 memcachedObject.setFingerPrint(fingerPrint);
 memcachedObject.setIdleTimeOut(idleTimeOut);
 SimpleDateFormatft = 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

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.

If thereis noentryin theMemcachedwithTokenId, meansuserloggedout
 
Statuscode : 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.

If fingerprintmismatchhappens
 
Statuscode : 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.

If sessionis idle
 
Statuscode : 401 
{
  "status" : "SESSION_IDLE"
}

Complete Validation Logic at Nginx using Lua

base-validation.lua

    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 ~= fingerprintHeaderthen
            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 < currentDatethen
              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

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_passhttp://imaginea;
  }
 
# For authenticate request there shouldn't be any validation. So just proxy_pass the request
 
  location /authenticate {
  proxy_passhttp://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.

Happy blogging…:)





About List