How to implement role based authorization in Node.js using token based authentication? - node.js

How can I implement multiple authentications in nodejs for a education firm, having three role- student, parent, and admin using REST API approach i.e token based authentication.

0. Concept
A JWT token (https://jwt.io/), contains a payload, within this payload you can specify a custom role object. Within the role object you can set boolean values to determine if the role is student, parent or admin.
Example Payload
{
...
"role": {
student: true,
parent: false,
admin: false,
}
}
When you go to generate your token for a specific user, attach the payload details to the token. (Ofcourse you would adjust the payload details depending on whether you want the user to be a student, parent or admin).
Whenever a user makes a request in the future using their token, you can make a callback to check their token and look at the payload.role object to see what role the user has and then make a decision as to whether they are authorized to perform a specific action or not.
1. Generating the token
See https://www.npmjs.com/package/jsonwebtoken for more information on generating a token.
const payload = {
userid: 123,
role: {
student: false,
parent: false,
admin: true,
},
};
const signOptions = {
issuer: this.config.jwt.issuer,
subject: payload.userid,
audience: this.config.jwt.audience,
expiresIn: "730d",
algorithm: "RS256",
};
const token = jwt.sign(payload, this.config.jwt.privateKey.replace(/\\n/g, "\n"), signOptions);
2. Middleware To Check Role
You should already have something like this if youre are using passport with JWT authentication.
const authGuard = PassportMiddleware.authenticate("jwt", { session: false });
router.get("/admin", authGuard, controller.index);
We need a new middleware to handle the checking of roles. We will call this middleware adminGuard. After authenticating using the authGuard middleware the req object will contain a user (which is the jwt payload). Now that we have the user information we can check what role they have.
const adminGuard = (req, res, next) => {
if(req.user && !req.user.role.admin) {
next(new Error('You are not an admin'));
} else {
next();
}
}
router.get("/admin", authGuard, adminGuard, controller.index);
You can create a new middleware guard for each role.

Related

Auth0 & Next-Auth Malformed JWT

I am attempting to setup authentication in my NextJS project and I am using Next-Auth. I am currently trying to setup a simple GET /me route that would be hit through React Query using the access_token retrieved by a successful Auth0 session Login.
BUT: the access_token received form Next-Auth w/ Auth0 useSession() is malformed
EDIT: I think the issue is that next-auth / auth0 is storing the token as an encrypted JWE. I need to figure out how to decrypt this and pass it to my api
https://github.com/nextauthjs/next-auth/issues/243
https://github.com/nextauthjs/next-auth/pull/249
https://github.com/nextauthjs/next-auth/discussions/5214
FRONTEND
in my pages > api > auth > [...nextauth].js I have the following configuration
const authOptions = {
providers: [
Auth0Provider({
clientId: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
issuer: process.env.AUTH0_ISSUER,
idToken: true,
}),
],
// Configure callbacks 👉 https://next-auth.js.org/configuration/callbacks
callbacks: {
// The JWT callback is called any time a token is written to
jwt: ({ token, user, account, profile, isNewUser }) => {
if (account) {
token.access_token = account.access_token;
token.id_token = account.id_token;
token.auth0_id = token.sub;
token.type = account.token_type;
}
delete token.name;
delete token.picture;
delete token.sub;
return token;
},
// The session callback is called before a session object is returned to the client
session: ({ session, user, token }) => {
const newSession = {
user: {
auth0_id: token.auth0_id,
email: token.email,
},
token: {
access_token: token.access_token,
id_token: token.id_token,
token_type: token.type,
},
};
return newSession;
},
},
secret: process.env.NEXTAUTH_SECRET,
};
export default NextAuth(authOptions);
Auth0 Config
in Auth0 Dashboard: Auth0 > Applications > Applications > <PROJECT_NAME> > AdvancedSettings > OAuth the signature algorithm is RS256
Successful Login Landing Page
here I am using const { data: session, status } = useSession(); to extract the value of the current session (which matches the shape created in the session callback of pages > api > auth > [...nextauth].js -- and has the access_token)
_app.jsx component
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
return (
<SessionProvider session={session}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</SessionProvider>
);
}
API Requests (from the frontend)
for each API request to the server I am setting the headers as such
if (token) headers["authorization"] = `Bearer ${token}`;
headers["Content-Type"] = "application/json";
Server Middleware
I created an auth middleware function that serves 2 purposes.
Validate the JWT passed to the route (🚨 THIS IS WHERE THINGS BREAK 🚨)
Attempt to find a user in my postgres DB with matching auth0_id (auth0|)
below is the auth middleware
// Auth0
import { isPublicRoute } from "../services/auth0/index.js";
import { expressjwt } from "express-jwt";
import jwks from "jwks-rsa";
// 👀 I have copied this directly from the Auth0 Dashboard: Applications > APIs > QuickStart
// 🚨 the express-jwt library is failing - error below
const validator = expressjwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: "https://dev-ikfyop4g.us.auth0.com/.well-known/jwks.json",
}),
audience: "thunderbolt",
issuer: "https://dev-ikfyop4g.us.auth0.com/",
algorithms: ["RS256"],
});
// NOTE: 👀 we are not actually getting to this function
// This function will retrieve the user and feed it into the request
// into a populated user model, if the id is not in the database it
// will create a new user and pull the base data from auth0
const userInjector = async (req, res, next) => {
if (isPublicRoute(req)) return next();
if (!req.auth0?.sub) throw badImplementation("JWT missing in userInjector");
req.user = await userFromReq(req);
console.log("THE USER INJECTOR RESULT :: req.user", req.user);
next();
};
const auth = () => {
return [validator, userInjector];
};
export default auth;
Inside my server I am importing this auth middleware function and using like this
server.use(auth());
THE express-jwt ERROR
UnauthorizedError: jwt malformed
at new UnauthorizedError (/Users/mrt/Documents/MrT/code/M/bolt/node_modules/express-jwt/dist/errors/UnauthorizedError.js:22:28)
at /Users/mrt/Documents/MrT/code/M/bolt/node_modules/express-jwt/dist/index.js:133:35
at step (/Users/mrt/Documents/MrT/code/M/bolt/node_modules/express-jwt/dist/index.js:33:23)
at Object.next (/Users/mrt/Documents/MrT/code/M/bolt/node_modules/express-jwt/dist/index.js:14:53)
at fulfilled (/Users/mrt/Documents/MrT/code/M/bolt/node_modules/express-jwt/dist/index.js:5:58)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
The access_token that I am receiving from Auth0 is malformed. Upon further investigation I would agree. The AccessToken I am receiving (as a JWT) has 5 parts (parts are strings separated by a period ..
Access Token From Auth0 Session
eyJhbGc<REMOVED_CHUNK>uYXV0aDAuY29tLyJ9.. // 2 period w/ nothing inbetween??
zOKQ2<REMOVED_CHUNK>rSm6.
9qbS5yndGKkrZ9Tc_dL8ZOuHtp_-e58uGqvGHgcpcFS8-s6SEHJYZ0_g7Ii7aYQe4AdbeK9ekW-704X_6C1r5JH3-9yBz<REMOVED_CHUNK>6Rn3Q8U0YC_x8Vp9pF_EA4GHjevXrh3HFBzCY4AEAx-Rmnzk4tZDgk3oU2rsY1NleMTwpIj0h29KIsukg113uMt5KCWKVnosSI-psaBu<REMOVED_CHUNK>lf0R_y5ClcXF6XY0ezIvuwoSQOmhulMlPsTxzBVGeoIhsooNntgAc4s.
ojmkoO_CO<REMOVED_CHUNK>URg
If everything is configured for RS256 why is Next-Auth w/ Auth0 sending me a malformed JWT.
EDIT: a 5 part JWT is a JSON Web Encryption (JWE) token...?

How can I get the id of the user that makes a request

How can I get the id of the user that creates a new article. I have created my auth middleware already with JWT. Here is my create article code.
exports.createArticle = (req, res) => {
const { title, article } = req.body;
const query = {
text: 'INSERT INTO article (title, article) VALUES ($1, $2) RETURNING *',
values: [title, article],
};
pool
.query(query)
.then((response) => {
const { id, created_on } = response.rows[0];
res.status(201).json({
message: 'Article successfully posted',
articleId: id,
createdOn: created_on,
title,
});
})
.catch((error) => {
res.status(400).json({
error,
});
});
};
If you are creating a payload that has the user's id in it, and then passing it to jwt.sign() then you can get the id of the user, once you authenticate the user's request by validating token that is being sent with the request.
Steps:
Note:
this are just a simple steps for better understanding,
ignore syntax or anything too specific to errors etc.
User sends log-in request.
you validate the credentials and send a token to the client side.
payload = { userId: userId , //rest info}
toekn = jwt.sign(payload, "key")
// send the token back, your methods may vary
User request for article creation and you receive token in the request, cookies, header or any other mechanism that you use
you use middleware to authenticate the request with token
jwt.verify(token, "key", (err, decodedUserInfo) => {
//dont proceed if err just throw error
//this decodedInfo has all the info that you have in payload while creating the token in step 2.
req.user = decodedUserInfo;
next();
}
as your req object has user info now, once your middleware is passed, you can use this info in your further operations.
You can get that from the JWT claims. Here is a list of standard claims: JWT Claims.
Subject, email, name or nickname would be suitable, depending on your case.
If you need an internal id, such as a database primary key - then use the subject id to link to that internal id. It is not considered good practice to leak out internal references to the outside.
How to get the subject id:
const jwt = require("jsonwebtoken");
// this is just for test, get a real verified token from your middleware
const testToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const token = jwt.decode(testToken);
let subjectId = token.sub; // then lookup and insert into database
You can find out more by reading the OpenID Connect specification here: OpenID Connect Specification section 2.2 for the ID Token part.

How to implement user based permissions on certain applications?

I currently have a rest api implemented with nodejs and express.
There is a single login endpoint for users to authenticate with and I am using JWT to authorize requests.
The API has multiple 'apps' within the api, for example a 'returns app' which is for users to handle customer returns.
I want to assign a role and and several apps to each user, so that a user may be able to access the 'returns app' and 'reviews app' and perform actions but not be authorized to the 'purchase app'
I currently have some middleware that will identify the user and attach it to the request:
module.exports = function(req, res, next) {
// verify user with JWT
req.user = JWT;
next();
}
how can I implement role based permission per application?
I want to be able to assign a user multiple apps and roles something along these lines:
user1: {
"role": "editor"
"apps": ["returns", "reviews"]
}
Is there a good way to implement this type of functionality?
EDIT:
If the JWT contains:
const user = {
name: John,
role: "editor",
apps: ["returns", "reviews"]
}
How should I parse the JWT with middleware to determine the users authorization. e.g. an 'editor' role has the
Thanks
You could set the permissions in the jwt payload when you create it.
const payload = {
role: "editor",
apps: ["returns", "reviews"]
}
jwt.sign(payload, jwtSecret)
And give access to the different apps based on the permissions contained in the jwt you receive.
Edit:
The middleware handling the permissions could look like this:
function reviewsPermissions(req, res, next) {
const token = jwt.decode(req.headers.authorization)
if (token.apps.includes('reviews') && token.role === 'editor')
next()
else
res.status(401).json({ error: "Access restricted to editors."})
}
router.post('/reviews', reviewsPermissions, (req, res) => {
// Your code
})

Decoding the token doesn't give user profile

I have been trying to solve this for several days.
I followed the tutorial in Auth0's documentation.
After decoding the token with express-jwt:
export let headerJWTCheck = expressJwt({
secret: '*************************',
audience: '******************************'
});
the content of req.user doesn't have the profile and roles that I need for role restrictions in the API.
Instead the content is in the form:
{ iss: 'https://******.eu.auth0.com/',
sub: 'google-oauth2|***********************',
aud: '********************',
exp: **************,
iat: **************}
In the front end I already get the user profile information I need, but I can't progress beyond that.
I'm using a function to restric the roles:
export function requireRole(role: string) {
return function (req, res, next) {
console.log(req.user);
var appMetadata = req.user.profile._json.app_metadata || {};
var roles = appMetadata.roles || [];
if (roles.indexOf(role) != -1) {
next();
} else {
res.redirect('/unauthorized');
}
}
but req.user.profile is always undefined.
In the main express application definition I have:
app.use(cookieParser());
app.use(session({
.................
}));
configurePassport();
app.use(passport.initialize());
app.use(passport.session());
The express-jwt library is initializing req.user based on the contents of the token that was included in the request.
If you require information at the req.user level you'll either need to include that information directly on the token or do an enrichment of req.user before running the role checks. For example, you could enrich req.user by running additional code that gets the roles based on the sub claim (user identifier).
The solution was to add the following code the auth0-lock configuration:
auth: {
params: {
scope: 'openid app_metadata roles'
}
}
Which added the app_metadata and the roles array to req.user after decoding the token.

Using JWT for Loopback authentication

I'm trying to understand how I can fit a custom JWT routing into loopbacks security model. My application has an authentication "dance" involving SMS that results in a valid JWT token using the excellent description. I'm using the jsonwebtoken and things work as expected. After obtaining the token my angular.js client sends the token with each request in the Authorisation: JWT ..token.. header (found conflicting documentation, one says JWT, one Bearer, but I can figure that out).
Now I want to make use of the token inside a loopback application. I'd like to use the ACL system loopback provides. I did read the following resources:
Authenticate a Node.js API with JSON Web Tokens
Loopback, loggin in users
Third-party login (Passport)
Loopback, making authenticated requests
Passport strategy for JWT
Loopback OAuth2.0 source
Loopback satelizer
And I'm not clear what my next steps are. I have working:
User 'login' - generating a JWT
User login using username/password (to be retired)
Working ACL implementation in loopback (when I access an ACL protected resource I get, as expected a 4xx error)
My JWT token properly (?) in the header of the request
I need:
based on the JWT token a valid user with roles compatible to loopback ACL
Help is very much appreciated
The solution turned out to be much simpler that I though it would be. For starters loopback does use its own jwt webtokens to keep a (stateless) user session. After establishing identity (in my case extracting the mobile number from my JWT token) I just need to lookup the member and generate the loopback native JWT token. My endpoint definition was this:
Member.remoteMethod(
'provideSMSToken', {
accepts: [{
arg: 'mobilenumber',
type: 'string',
description: 'Phone number including +65 and no spaces'
}, {
arg: 'token',
type: 'string',
description: 'the token received through SMS'
}],
returns: {
arg: 'token',
type: 'string'
},
description: 'provide SMS token to confirm login',
http: {
path: '/smsauthenticate',
verb: 'post'
},
isStatic: true
}
);
and the provideSMSToken function like that:
// Exchange the SMS Token with a login token
Member.provideSMSToken = function(mobilenumber, token, cb) {
var app = Member.app;
// CHeck if the token does exist for the given phone number
// if yes, check for the respective memeber
if (!app.smsVerificationToken || !app.smsVerificationToken[mobilenumber] || app.smsVerificationToken[mobilenumber] !== token) {
var wrongToken = new Error("Wrong or missing token");
cb(wrongToken, "Wrong or missing token");
} else {
var timetolive = 86400;
Member.lookupByPhone(mobilenumber, function(err, theOne) {
if (err) {
cb(err, "Sorry, no such member here!");
} else {
// We can provide a token now for authentication
// using the default createAccessToken method
theOne.createAccessToken(timetolive, function(err, accesstoken) {
cb(err, accesstoken);
})
}
});
}
}
Works like a charm

Resources