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.
- resource ower, usually a person has access to an API and can delegate access to that API.
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:
- The client first sends the resource owner to the authorization server in order to request that the resource owner authorize this client.
- The resource owner authenticates to the authorization server and is generally presented with a choice of whether to authorize the client making the request.
- The client is able to ask for a subset of functionality, or scopes.
- 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.
- 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.
- 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.
- 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
andclient_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.
- 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
- which client made the initial authorization request
- which user authorized it
- what it is authorized for.
- which client made the initial authorization request
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.
- The response could also include a refresh token (used to get new access tokens without asking for authorization again)
- check the client’s credentials (passed in the authentication header) to see which client is requesting acess.
- 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.
- 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).
3. How authentication and authorization are implemented in Vuejs?
3.1. Diagram of authorization-code-grant for Vuejs-frontend and backend
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 signaccess_token
.- Depending on auth-server implemetation, the
access_token
maybe is proccess(re-created using thatjwtSignKey
) in backend before forward to frontend.
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.
- The most important thing OIDC provider need to do is to validate the username and password when resources owner log (see step-3 in authorization code grant).
- Check The OAuth 2.0 Authorization Framework and OAuth 2.0
- Check OAuth2.0 – 3.Protocol Endpoints
3.4. What Vuejs backend should do (provide APIs)?
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('');
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.
- Notice, the options are common to different authorization provider implementations. However, their logout endpoints(Here, the node-oidc-provider use
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 } };
- To verify
access_token
orid_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 theredirectUri
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).
- AS(authorization server, or auth-provider) is trusted by the protected resources. So, protected resource delegate its credentails verification to AS.
- 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.