UP | HOME
Land of Lisp

Zhao Wei

How can man die better than facing fearful odds, for the ashes of his fathers and the temples of his Gods? -- By Horatius.

Auth-in-Vuejs

1. Introduction

This article introduce how I implement authentication and authorization for Vuejs app. The whole Vuejs project consist of 4 parts:

  • Vuejs-frontend, for rendering html pages.
  • Vuejs-backend, our APIs service.
  • oidc-provider, authorization server for delegation.
  • postgres, for storing RBA models and user credentials.

2. How 0Auth2.0 works?

2.1. What is OAuth2.0?

  • It is a delegation protocol. It enable a 3rd party application to obtain limited access to an HTTP service.
  • Its related components:
    • resource ower, usually a person has access to an API and can delegate access to that API.
    • protected resources, the component that the resource ower has access to. Usually these protected resources are some APIs.
    • client, an application or a software that want to access the protected resources on behalf of resource owner.

2.2. An example

Suppose We have a cloud photo-storage service and a photo print service. We want to use the print service to print photo stored in phote-storage. Howerver, those two services are running by different companies.

  • resource owner, some user (the browser to visit print service).
  • protected resource, APIs (photos in photo-storage).
  • client, print-service which needs to call APIs of photo-storage (to access photos).

OAuth introduce an additional component into the picture: authorization server(AS).

  • AS is trusted by the protected resources. So, protected resource delegate its credentails verification to AS.
  • AS issue special security credentials – called access-token to client. This access-token is then used by client(photo-storage APIs).

General steps to acquire an access-token:

  1. The client first sends the resource owner to the authorization server in order to request that the resource owner authorize this client.
  2. The resource owner authenticates to the authorization server and is generally presented with a choice of whether to authorize the client making the request.
  3. The client is able to ask for a subset of functionality, or scopes.
  4. Once the authorization grant has been made, the client can then request an access token from the authorization server.
    • An authorziation grant is the means by which an OAuth client is given access to a protected resource using the OAuth Protocol, and if successful it ultimately results in the client getting a token.
    • Don’t misunderstand authorization code as it, the entire OAuth process is the authorization grant.
  5. This access token can be used at the protected resource to access the API, as granted by the resource owner.

Based on how the client request an access-token (step 4), there could be different grant types. The most commonly used are:

  • Authorization Code (the one I used in Vuejs development).
  • Password
  • Client credentials
  • Implicit

2.3. Authorization code grant

Authorization code is a temporary code (authorization code) that the client will exchange for an access-token. This code is obtained from authorization server when user agree on what the information(usually permissions) the client is requesting.

authorization-code-grant.png
  • step2’s response is an HTTP redirect, it causes step 3: cause the browser to send an HTTP GET to the autorization server. step2 have the following query parameters:
    • response_type
    • scope
    • client_id
    • redirect_uri
    • state
  • step5, the client could receive
    • code, known as authorization code.
    • state, matches the value that it sent in the step 2.
  • step6, POST request with passing its client_id and client_secret as an HTTP Basic authorization header.

    Authorization: Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x
    
    • This separation between different HTTP connections ensures that the client can authenticate itself directly without other components being able to see or manipulate the token request.
  • step7, authorization server need to ensure the request from step6 is legitimate, issue accessToken if valid.
    • check the client’s credentials (passed in the authentication header) to see which client is requesting acess.
    • check code (authorization code) from the body. It incldues
      1. which client made the initial authorization request
      2. which user authorized it
      3. what it is authorized for.
    • if the code has not been used previously and the client making this request is the same as the client that made the original request, then the authorization server generates and returns a new access token for the client

      HTTP 200 OK
      Date: Fri, 31 Jul 2015 21:19:03 GMT
      Content-type: application/json
      
      {
        “access_token”: “987tghjkiu6trfghjuytrghj”,
        “token_type”: “Bearer”
      }
      
      • The response could also include a refresh token (used to get new access tokens without asking for authorization again)
      • additional info could be scopes and expiration time.
  • step8, the client get the access token from step7 response and present it to the protect resource
    • present the access token as OAuth Bearer token

      GET /resource HTTP/1.1
      Host: localhost:9002
      Accept: application/json
      Connection: keep-alive
      Authorization: Bearer 987tghjkiu6trfghjuytrghj
      
    • can store this access token in a secure place for as long as it wants to use the token, even after the user has left.
  • step9, protected resource use the access token to determine whether it is still valid
    • HOW to validate? A protected resource has a number of options for doing this token lookup. The simplest option is for the resource server and the authorization server to share a database that contains the token information. The authorization server writes new tokens into the database when they are generated, and the resource server reads tokens from the store when they are presented.

2.4. Obtain refreshed access-token

Client could request refreshed access-token when the access-token expired. This time, the token is never sent to the protected resources. So, the client uses the refresh token to request new access token without involving the resource owner (user or browser).

refresh-token.png

3. How authentication and authorization are implemented in Vuejs?

3.1. Diagram of authorization-code-grant for Vuejs-frontend and backend

vuejs-oauth.png
  • A simple middleware used in backend to verify access_token and compare required scopse:

    export const checkAuth = (req: any, res:any, next:any) => {
      console.log(">> enAuth: ", enableAuth);
      if(!enableAuth) {
        next();
      } else {
        console.log(">> check middleware");
        let bearerToken = req.headers.authorization;
        console.log(">> bearerToken: ", bearerToken);
        let permissionsToken = bearerToken.slice(bearerToken.indexOf(' ') + 1);
        console.log(">> permissionsToken:", permissionsToken);
    
        try {
          let result = jwt.verify(permissionsToken, Buffer.from(jwtSignKey, 'base64'));
          console.log(">> jwt.verify result:", result);
    
          console.log(">> userPermissions:", result.scope);
          let route = req.route.path;
          console.log(">> request.route:", route);
    
          let key = route.slice(route.lastIndexOf("/") + 1);
          let neededPermissions = authPermissionsMap[key];
    
    
          if (!neededPermissions) {
            // If we didn't specify route in authPermissionsMap, then we let it pass.
            console.log(`>> ${key} does not need to check permissions`);
            next();
          } else if (checkPermissions(result.scope, neededPermissions)) {
            console.log(">> permission granted.");
            next();
          } else {
            console.log(">> You do not have enough permissions.");
            res.status(403).json({message: "You do not have enough permissions."});
          }
        } catch(err) {
          console.log(">> jwt.verify error:", err.message);
          res.status(403).json({tokenVerifyError: err.message});
        }
      }
    };
    
    // where key represent the backend API path
    export const authPermissionsMap: any = {
      // userManagement
      "listAllUsers": "UserView",
      "addUser": "UserModify",
      ... 
      "addRolesToPermission": "PermissionModify",
    }
    
  • Use middleware for some endpoint

    router.get('/listAllRoles', checkAuth, async (req: Request, res: Response) => {
      // Do something
    });
    
  • Notice
    • jwtSignKey used in the middleware is the key used to sign access_token.
    • Depending on auth-server implemetation, the access_token maybe is proccess(re-created using that jwtSignKey) in backend before forward to frontend.
vuejs-oauth.png

3.2. Define our Role-based-access(RBA) control model in PostgreSQL

We define User, Role, Permission 3 basic entities as 3 tables

  • user_master
    Notice: when store username and password, we should NEVER store plain password. Instead, we should store password hash.

    const salt = bcrypt.genSaltSync(10);
    const passwordHash = bcrypt.hashSync("<user_password>", salt);
    

    To validate password:

    let isMatch = await bcrypt.compare(plainPasswordGotFromLoginForm, passwordHashReadFromDB)
    
  • role_master
  • permission_master

For their associations, we define UserRole, RolePermission as many-to-many relationships.

  • user_role table

    CREATE TABLE user_role (
           user_id varchar(36) REFERENCE user_master (user_id) ON UPDATE CASCADE,
           role_id varchar(36) REFERENCE role_master (role_id) ON UPDATE CASCADE,
           PRIMARY KEY (user_id, role_id)
    );
    
  • role_permission table

    CREATE TABLE role_permission (
           role_id varchar(36) REFERENCE role_master (role_id) ON UPDATE CASCADE,
           permission_id varchar(36) REFERENCE permission_master (permission_id) ON UPDATE CASCADE,
           PRIMARY KEY (role_id, permission_id)
    );
    

Multiple examples to use such models like:

  • Add permission into a role

    export const addPermissionToRole = async (permissionId, roleId) => {
      const role = await findRoleById(roleId);
      const permission = await findPermissionById(permissionId);
    
      if (!role) {
        console.log(">> no such role");
        return false;
      }
      if (permission) {
        console.log(">> no such permission");
        return false;
      }
    
      let text = "insert into role_permission (role_id, permission_id) values ($1, $2)";
      let values = [roleId, permissionId];
    
      try {
        await client.query(text, values);
        return true;
      } catch (err) {
        console.log('error=', err);
        return false;
      }
    }
    
  • Find a user’s associated roles (using js)

    export const findUserRoles = async (userId) => {
      const text = "select role_master.role_id, role_master.role_name from role_master inner join user_role on user_role.user_id = role_master.role_id inner join user_master on user_master.user_id = user_role.user_id where user_master.user_id = $1";
    
      try {
        return await client.query(text, [userId]);
      } catch (err) {
        return null;
      }
    }
    
  • Remove permission from a role

    export const removePermissionFromRole = async (permissionId, roleId) => {
      const text = "delete from role_permission where (role_id, permission_id) = ((select role_master.role_id from role_master where role_master.role_id = $1), (select permission_master.permission_id from permission_master where permission_master.permission_id =$2))";
    
      const values = [roleId, permissionId];
      let result = null;
    
      try {
        result = await client.query(text, values);
      } catch (err) {
        console.log('error=', err);
      } finally {
        return result.rowCount == 1;
      }
    }
    
  • Delete role with transactions

    export const deleteRole = async (roleId) => {
      let result = false;
    
      try {
        await client.query('BEGIN');
    
        // need to delete many-to-many relationships before delete that role
        let text = "delete from role_permission where role_permission.role_id = $1";
        await client.query(text. [roleId]);
    
        text = "delete from user_role where user_role.role_id = $1";
        await client.query(text, [roleId]);
    
        text = "delete from role_master where role_master.role_id = $1";
        await client.query(text, [roleId]);
    
        await client.query('COMMIT');
    
        return = true;
    
      } catch (err) {
        console.log('error=', err);
        await client.query('ROLLBACK');
        result = false;
      } finally {
        return result;
      }
    }
    

3.3. What does OIDC provider should do?

For OIDC provider, I am using node-oidc-provider. It implements the OAuth2.0 protocol.

3.4. What Vuejs backend should do (provide APIs)?

  1. Generate login URL for Vuejs-frontend to direct user to authorization server for login.

    let scopes =['openid', 'profile'];
    let url = [
      `${authorizationProviderAddress}/auth`,
      `?client_id=${clientId}`,
      `&redirect_uri=${encodeURIComponent(redirectUri)}`,
      `&response_type=code`,
      `&scope=${encodeURIComponent(scopes.json(' '))}`,
      `&state=${crypto.randomBytes(16).toString('hex')}`,
      `&nonce=${crypto.randomBytes(8).toString('hex')}`
    ];
    return url.join('');
    

    See The OAuth 2.0 Authorization Framework – 4.1.1.

  2. Generate logout URL

    let options = [
      `client_id=${clientId}`,
      `returnTo=${encodeURIComponent(redirectUri.substring(0, redirectUri.indexOf('auth/callback')))}`,
      'federated'
    ];
    return `${authUrl}/session/end?${options.join('&')}`;
    
    • Notice, the options are common to different authorization provider implementations. However, their logout endpoints(Here, the node-oidc-provider use ${authUrl}/session/end) are probably different.
  3. Use authorization-code to obtain access-token

    // This authCode is obtained from parsing callback parameters after login in frontend
    let authCode = req.body.code
    let authHeader = 'Basic ' + Buffer.from(client_id + ':' + client_secret, 'utf8').toString('base64');
    let options = {
      method: 'POST',
      url: `${authProviderUrl}/token`,
      headers: {
        Authorization: authHeader
      },
      form: {
        code: authCode,
        client_id: clientId,
        grant_type: 'authorization_code',
        redirect_uri: redirectUri
      }
    };
    
  4. To verify access_token or id_token are not tampered.
    • Verify access-token using public JWT certificate

      /// Packages required 
      var jwt = require('jsonwebtoken');
      var jwksClient = require('jwks-rsa');
      
      // More about caching certificates: https://github.com/auth0/node-jwks-rsa as a way to fetch the keys.
      var client = jwksClient({
        cache: true,
        jwksUri: '<Usually_this_is_auth_provider/certs>'
      });
      
      var token = "ID_TOKEN / ACCESS_TOKEN";
      
      // Verify the token
      jwt.verify(token, getKey, {}, function (err, decoded) {
        if (decoded) {
          // Here you will find decoded token
          console.log(decoded);
        }
        if (err) {
          console.log('Unable to Verify with error ', err);
        }
      });
      
      function getKey(header, callback) {
        client.getSigningKey(header.kid, function (err, key) {
          var signingKey = key.publicKey || key.rsaPublicKey;
          callback(null, signingKey);
        });
      }
      
    • Or, verify tokens from auth-provider’s introspection endpoint. Need to check auth-provider document.

3.5. What Vuejs frontend should do?

  • Register client in authorization provider for:
    • client_id
    • client_secret
    • grant_types
    • redirect_uris
  • Provide callback endpoint which is the redirectUri described above. Someting like <https://<address>/auth/callback.
  • Handle callback after login from auth provider to get authorization-code
  • Pass authorization-code (do not store it) to Vuejs-backend APIs using post.
  • From Backend, exchange authorization-code for access-token.
  • Don’t forget to introspect access-token to validate it (endpoint is provided by auth-provider)
  • After that, we could aquire login user’s roles or permissions.
  • At last, we could based on login user’s permissions, hide particular section links (routes).
  • Every request send to backend need to be embedded with access_token

4. Summary

  • The OAuth is not a magic:
    • AS(authorization server, or auth-provider) is trusted by the protected resources. So, protected resource delegate its credentails verification to AS.
    • AS issue special security credentials – called access-token to client. This access-token is then used by client(photo-storage APIs).
  • Actually, you still need to compare username and password (provided into AS).
  • OAuth document doesn’t specify how to implement AS. However, it expects AS to follow some conventions to provider several key features, such as introspect accesstoken to introspect user profile.
  • At last, this document is just my note from my understanding. It doesn’t represent best practise.

5. References