Search code examples
node.jsangularsessionexpressjson-web-token

Session Managment between NodeJS and Angular using JSONWebToken


I'm trying to build an application in such a way that NodeJS serves as the backend with all business logic exposing JSON REST services to be consumed by the angular 4 app which is nothing but a dumb client. So far so good, however I'm having a hard time figuring the Session management.

I found that token based authentication is a way to go since you might one day be serving mobile apps however, I have a problem: if I go with JSONWebToken strategy on the server side with token expiration set to half an hour, then my client will need to re authenticate it self after half an hour which doesn't seem like a good fit, because then it may force the user to sign in again that is already working on the client application which is not how any web app works. Should I also need to maintain session management at Angular level and auto sign in if my token expires on server but then it violates the principle of a dumb client or I should scrap it altogether implement sessions at NodeJS it self? Another thing is if I implement the WebTokenStrategy I found that for every request that comes from the client I'll be making a trip to database to verify a user which I can cache in session if I'm doing session management on NodeJS.

Last thing that I have a hard time figuring out is okay I can secure my resources on NodeJS but then I also need my routes and pages to be served depending on user rights in my client application, should I also store this information in the NodeJS database and serve by the same API server but I think this again violates the single responsibility principle or should there be another database for this client site route and user management.

Can someone suggest a good approach and if possible with examples?

Thanks.


Solution

  • No a JSON web token do not required a trip to the database since you encode the information you want on the payload. However you can implement a redis strategy if you want to be able to revoke them (For right changes for example). The signature part will be used by your server to ensure the authenticity (thanks to your server-side JWT secret).

    You can also choose the expiration time you want. But if you want to limit it to 30 minutes you can also implement a renew strategy. (Ask for a new token before the old one will expire soon : The server will just deliver a new token with the same data encode. For the front end renew strategy you can use such a lib :

    'use strict';
    
    /**
     * Helper class to decode and find JWT expiration.
     */
    class JwtHelper {
    
      urlBase64Decode(str) {
        let output = str.replace(/-/g, '+').replace(/_/g, '/');
        switch (output.length % 4) {
          case 0: { break; }
          case 2: { output += '=='; break; }
          case 3: { output += '='; break; }
          default: {
            throw 'Illegal base64url string!';
          }
        }
        return this.b64DecodeUnicode(output);
      }
    
      // credits for decoder goes to https://github.com/atk
      b64decode(str) {
        let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
        let output = '';
    
        str = String(str).replace(/=+$/, '');
    
        if (str.length % 4 == 1) {
          throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
        }
    
        for (
          // initialize result and counters
          let bc = 0, bs, buffer, idx = 0;
          // get next character
          buffer = str.charAt(idx++);
          // character found in table? initialize bit storage and add its ascii value;
          ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
            // and if not first of each 4 characters,
            // convert the first 8 bits to one ascii character
            bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
        ) {
          // try to find character in table (0-63, not found => -1)
          buffer = chars.indexOf(buffer);
        }
        return output;
      }
    
      // https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem
      b64DecodeUnicode(str) {
        return decodeURIComponent(Array.prototype.map.call(this.b64decode(str), (c) => {
          return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));
      }
    
      decodeToken(token) {
        let parts = token.split('.');
    
        if (parts.length !== 3) {
          throw new Error('JWT must have 3 parts');
        }
    
        let decoded = this.urlBase64Decode(parts[1]);
        if (!decoded) {
          throw new Error('Cannot decode the token');
        }
    
        return JSON.parse(decoded);
      }
    
      getTokenExpirationDate(token) {
        let decoded;
        decoded = this.decodeToken(token);
    
        if (!decoded.hasOwnProperty('exp')) {
          return null;
        }
    
        let date = new Date(0); // The 0 here is the key, which sets the date to the epoch
        date.setUTCSeconds(decoded.exp);
    
        return date;
      }
    
      isTokenExpired(token, offsetSeconds) {
        let date = this.getTokenExpirationDate(token);
        offsetSeconds = offsetSeconds || 0;
    
        if (date == null) {
          return false;
        }
    
        // Token expired?
        return !(date.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000)));
      }
    }
    
    const jwtHelper =  new JwtHelper();
    
    const decodedData = jwtHelper.decodeToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ');
    
    console.log(decodedData)