Role based authentication in HapiJS - javascript

I am working on a rest API first project written with HapiJS.
After the login process the user gets a token to pass in the header of every request.
Users have different roles (admin, standard, guest, partners) and some Api endpoint are reachable only by users with a certain role.
Someone could help me in defining this check in a nice way, so without writing the check everytime inside the route?

Scopes
You can use scopes in hapi. When you authenticate the request by checking the header, you can set the scope property of the user's credentials:
var validateFunc = function (username, password, callback) {
... // Your logic here
return callback(null, true, {scope: 'admin'});
};
When defining a route you can set the scopes which are permitted to that endpoint in the config.auth.scope property:
server.route({
...
config: {
auth: {
strategy: 'simple',
scope: ['user', 'admin']
},
}
...
});
Now, only users who are authenticated with the scope of user or admin, will be able to access that route.
Process
Decide on some scopes (admin, superuser, guest etc)
Configure your authentication routine to correctly set the scope on the user's credentials
Configure your routes by setting the config.auth.scope to whomever is allowed to access it
Runnable Example
var Hapi = require('hapi');
var server = new Hapi.Server();
server.connection({ port: 4000 });
server.register(require('hapi-auth-basic'), function (err) {
if(err) {
throw err;
}
server.auth.strategy('simple', 'basic', {
validateFunc: function (username, password, callback) {
if (username === 'admin') {
return callback(null, true, {scope: 'admin'}); // They're an `admin`
}
if (username === 'user') {
return callback(null, true, {scope: 'user'}); // They're a `user`
}
return callback(null, false);
}
});
server.route([{
config: {
auth: {
strategy: 'simple',
scope: ['admin'] // Only admin
},
},
method: 'GET',
path: '/admin',
handler: function(request, reply) {
reply('Admin page');
}
}, {
config: {
auth: {
strategy: 'simple',
scope: ['user', 'admin'] // user or admin
},
},
method: 'GET',
path: '/user',
handler: function(request, reply) {
reply('User page');
}
}
]);
server.start(function () {
console.log('Started server');
});
});

Related

Next-Auth Redis Adapter support for user session persistence?

I was going through the next-auth documentation but didn't find any mention of connecting to custom configured Redis without the use of Upstash for a persistent session store.
My use case is straightforward. I am using Nginx as a load balancer between multiple nodes for my nextJS application and I would like to persist the session if in case the user logs in and refreshes the page as Nginx switches between nodes.
For e.g My Nginx config
server {
listen 80;
server_name _;
location / {
proxy_pass http://backend;
}
}
upstream backend {
ip_hash;
server <nextjs_app_ip_1>:8000;
server <nextjs_app_ip_2>:8000;
}
As you can see from the example Nginx config, there are multiple upstream server pointers here that require user session persistence.
I am using the credentials provider of next-auth as I have a Django-based auth system already available.
I did see the implementation of the next-auth adapter with Upstash. However, I have my own custom server running with Redis.
I tried connecting to Redis using ioredis which works fine as it is connected. However, I am not sure how can I use Redis here with next-auth to persist session and validate at the same time?
For e.g In express, you have a session store which you can pass your Redis Client with and it should automatically take care of persistence. Is there anything I can do to replicate the same behavior in my case?
For e.g In Express
App.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'secret$%^134',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // if true only transmit cookie over https
httpOnly: false, // if true prevent client side JS from reading the cookie
maxAge: 1000 * 60 * 10 // session max age in miliseconds
}
}))
My Code:
import CredentialsProvider from "next-auth/providers/credentials";
import {UpstashRedisAdapter} from "#next-auth/upstash-redis-adapter";
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL); //points to my custom redis docker container
export const authOptions = {
providers: [CredentialsProvider({
name: 'auth',
credentials: {
email: {
label: 'email',
type: 'text',
placeholder: 'jsmith#example.com'
},
password: {
label: 'Password',
type: 'password'
}
},
async authorize(credentials, req) {
const payload = {
email: credentials.email,
password: credentials.password
};
const res = await fetch(`my-auth-system-url`, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json'
}
});
const user = await res.json();
console.log("user", user);
if (!res.ok) {
throw new Error(user.exception);
}
// If no error and we have user data, return it
if (res.ok && user) {
return user;
}
// Return null if user data could not be retrieved
return null;
}
})],
adapter: UpstashRedisAdapter(redis),
pages: {
signIn: '/login'
},
jwt: {
secret: process.env.SECRET,
encryption: true
},
callbacks: {
jwt: async({token, user}) => {
user && (token.user = user)
return token
},
session: async({session, token}) => {
session.user = token.user
return session
},
async redirect({baseUrl}) {
return `${baseUrl}/`
}
},
session: {
strategy: "jwt",
maxAge: 3000
},
secret: process.env.SECRET,
debug: true
}
export default NextAuth(authOptions)
Thank you so much for the help.

JWT Login - No authorization token was found in middleware

I followed a tutorial to add login and registration to my Node.js app using JWT token and I'm having a hard time logging in and redirecting to my 'logged in' admin page. User registration works great, but the login portion I can't figure out.
This is the tutorial I was following:
https://medium.freecodecamp.org/learn-how-to-handle-authentication-with-node-using-passport-js-4a56ed18e81e
My code for login looks like this:
router.post('/login', auth.optional, (req, res, next) => {
console.log(req.body);
var user = {
email: req.body.email,
password: req.body.password
}
if (!user.email) {
return res.status(422).json({
errors: {
email: 'is required',
},
});
}
if (!user.password) {
return res.status(422).json({
errors: {
password: 'is required',
},
});
}
return passport.authenticate('local', { session: false }, (err, passportUser, info) => {
if (err) {
return next(err);
}
if (passportUser) {
const user = passportUser;
user.token = passportUser.generateJWT();
console.log("TOKEN: " + user.token);
res.setHeader('Authorization', 'Token ' + user.token);
return res.json({ user: user.toAuthJSON() });
}
return res.status(400).json({
errors: {
message: info,
},
});
})(req, res, next);
});
My '/admin' "logged in" route looks like this:
router.get("/admin", auth.required, function(req, res) {
res.render('admin', {
user : req.user // get the user out of session and pass to template
});
});
I'm not sure how I can redirect to my '/admin' route while also passing the token because currently I am seeing the following error after logging in. Makes sense since I am not passing the token to the '/admin' route...but how do I do that? :)
UnauthorizedError: No authorization token was found at middleware
Thanks in advance for the help!
EDIT:
Still can't figure this out and don't really understand how this flow is supposed to work...where do the headers need to be set to the token and how do I redirect to my admin page once the login is successful.
Here is my middleware code if this helps:
const getTokenFromHeaders = (req) => {
console.log("REQ: " + JSON.stringify(req.headers));
const { headers: { authorization } } = req;
if(authorization && authorization.split(' ')[0] === 'Token') {
return authorization.split(' ')[1];
}
return null;
};
const auth = {
required: jwt({
secret: 'secret',
userProperty: 'payload',
getToken: getTokenFromHeaders,
}),
optional: jwt({
secret: 'secret',
userProperty: 'payload',
getToken: getTokenFromHeaders,
credentialsRequired: false,
}),
};
Your code does not have a problem. You seem to be confused with the login flow from server to client (Frontend/Web).
Let's first have a look the RESTFUL way of doing it. The article also refers to the same flow.
The RESTFUL API flow looks like this:
User requests for login:
POST: /api/v1/auth/login with username and password in request body.
If successful, user is returned with basic inforamtion and token.
If not, user is returned a 401 (Unauthorized) status code.
The login flow ends here.
The token provided earlier to the user is used to make subsequent calls to the backend, which a user can use to perform different operations on the sustem. In essence, it is the client which requests server for subsequent actions with the token provided in the login request.
So for your case, user after receiving the token should make a request for retrieving admin information from the backend.
But, I am assuming you are rendering views from your server-side and you want to render the admin view once the user is successfully logged in, and that's pretty straight forward.
Instead of your res.json() after successful login. You need to use res.render().
res.render('admin', {
user: user.toAuthJSON() // assuming your user contains the token already
})
Edit:
Since res.render() does not change the url in the browser. For that, you need to use res.redirect(). But the problem is, you can not send context in res.redirect().
To achieve that, you will need to pass in the user token as query paramter. See here.
TL;DR
// assuming you are using Node v7+
const querystring = require('querystring');
const query = querystring.stringify({
token: user.token,
});
const adminRoute = '/admin?' + query;
res.redirect(adminRoute)
And in your admin route, you need to slightly modify the code.
Verify the token belongs to a real user and get user information out of the token.
Render the admin template with user information retrieved from step 1.
router.get("/admin", function(req, res) {
// verify the token
const token = req.query.token;
const user = null;
jwt.verify(token, 'secret', function (err, decoded) {
if (err) {
res.status(401).send('Unauthorized user')
}
// decoded contains user
user = decoded.user
});
res.render('admin', {
user : user
});
});
I'm somewhat new to this as well, but I've got it working as follows.
In your server.js file:
const passport = require("passport");
const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
app.use(passport.initialize());
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = Keys.secretOrKey;
passport.use(
new JwtStrategy(opts, (jwt_payload, done) => {
// somefunction looks up the id in jwt payload and
// supplies passport the authenticated user via the "Done" function
somefunction.user(jwt_payload.id)
.then(user => {
if (user) {
return done(null, user);
}
return done(null, false);
});
})
);
In your API definitions
const jwt = require("jsonwebtoken");
router.post("/login", (req, res) => {
const { userInfo } = req.body;
// userInfo has username and password in it
// anotherFuction validates the user id and password combo
anotherFunction(userInfo.id, userInfo.password)
.then(isAuthenticated => {
if (isAuthenticated) {
const payload = {
id: user.sAMAccountName,
firstname: user.givenName,
lastname: user.sn
};
// Sign Token with the payload
jwt.sign(
payload,
Keys.secretOrKey,
{ expiresIn: 3600 },
(err, token) => {
res.json({
success: true,
token: "Bearer " + token
});
}
);
} else {
// don't mind the statuses ^_^'
return res.status(401).json({ error: "Login failed." });
}
})
.catch(err => {
return res.status(400).json(err);
});
});
After calling the API you want to set the auth token. The following lets you delete the token if nothing is passed in, effectively "Logging out".
const setAuthToken = token => {
if (token) {
// Apply to every request
axios.defaults.headers.common["Authorization"] = token;
} else {
// Delete Auth Header
delete axios.defaults.headers.common["Authorization"];
}
};
If you're trying to use it in the front end, you need to use jwt_decode to pull the values from the token and set it however you deem necessary. If using redux to store login data it should look something like this. As I feel that the discussion of using localstorage for jwtToken is outside of the scope of this, just know would need to check for the token.
if (localStorage.jwtToken) {
setAuthToken(localStorage.jwtToken);
const decoded = jwt_decode(localStorage.jwtToken);
store.dispatch({
type: USER_LOGIN,
payload: decoded
});
}
Hope this helped.
From one beginner in JWT to another. Good luck.

http request responds with html instead of json

I've made some vimeo api calls to get videos, but when I do a get request on the server it responds with the html on that path instead of the data from the server. I'm also using angular-client-side-auth (https://github.com/fnakstad/angular-client-side-auth). I'm new to this, so I'm struggling to understand why this happens.
server.js
app.get('/api/mostviewed', function (req, res) {
MostViewed.find({}, { _id: 0, iframe: 1 }, function (err, docs) {
res.json(docs);
});
});
inside client-side-auth's routes.js there's this, which causes it all(This file is on the server-side):
{
path: '/*',
httpMethod: 'GET',
middleware: [function(req, res) {
var role = userRoles.public, username = '';
if(req.user) {
role = req.user.role;
username = req.user.username;
}
res.cookie('user', JSON.stringify({
'username': username,
'role': role
}));
res.render('index');
}]
}
How can I solve this? I want to maintain the path: '/*', or change it while keeping the function similar, so I can get my data from the server. Or is there a different way to solve this?
EDIT:
Solution
{
path: '/api/mostviewed',
httpMethod: 'GET',
middleware: [Video.getmostviewed]
},
inside Video.js I made this:
getmostviewed: function(req,res){
MostViewed.find({}, { _id: 0, iframe: 1 }, function (err, docs) {
res.json(docs);
});
}
In your case, client could not find logic corresponding to /api/mostviewed, thus reached /* and displayed html instead of json.
Possible solution
Add following similar logic before /*
{
path: '/api/*',
httpMethod: 'GET',
middleware: [function(req, res) { // sample middleware logic
var role = userRoles.public, username = '';
if(req.user) {
role = req.user.role;
username = req.user.username;
}
res.cookie('user', JSON.stringify({
'username': username,
'role': role
}));
}]
},

Send REST calls from Node server to third party application using OAuth

I'm implementing a server that handles chat messages. In some cases I want to access data from a JIRA instance. I'm using passport-atlassian-oauth strategy for authenticating with JIRA and BearerStrategy for requests, but my issue is that the authentication is only valid in the browser after a user has given "My Server" read and write access to JIRA. In many guides they just call res.redirect('/successfulLogin') or something similar after a successful authentication, but I would instead like to do a rest call to JIRA, process the data and send it to my connected client application.
How do I do that?
I'm completely new to all this and everything just spins around in my head. I save and have access to the token used for authentication and when I for instance navigate to .../test/access_token=?[token] in my browser it works.
passport.use(new BearerStrategy(
function(token, done) {
// Find user by token
client.smembers('access_token:' + token, function(err, replies) {
if (err) {
return done(err);
}
// if user found
// TODO: yet again, hard coded for one
if (replies.length > 0) {
console.log('SHOULD BE 1:', replies[0]);
client.hgetall('users:' + replies[0], function(err, user) {
if (err) {
return done(err);
}
if (!user) {
return done(null, false);
}
return done(null, user, {scope: 'all'});
});
}
});
}
));
As you can see it's hard coded for just one user and I'm using Redis as a "database".
passport.use(new AtlassianOAuthStrategy({
applicationURL: 'http://localhost:2990/jira',
callbackURL: '/auth/atlassian-oauth/callback',
consumerKey: RsaPublicKey,
consumerSecret: rsaPrivateKey,
clientId: 'MyBot'
},
function(accessToken, tokenSecret, profile, done) {
// Find user
client.hgetall('users:1', function(err, user) {
if(err) {
return done(err);
}
// user not found
if(!user) {
// create new user, no worries!
// TODO: HARD CODED FOR ONE USER
client.hmset('users:1', 'id', profile.id, 'access_token', accessToken, function(err, res) {
client.sadd('id:admin', '1');
client.sadd('access_token:'+ accessToken, '1');
client.hgetall(profile.id, function(err, user) {
return done(null, user);
});
});
} else {
// Update access token!
client.hmset(profile.id, 'access_token', accessToken, function() {
client.sadd('access_token:' + accessToken, '1', function() {
client.hgetall(profile.id, function(err, result) {
return done(null, user);
});
});
});
}
});
}
));
Here's the rest
app.get('/auth/atlassian-oauth',
passport.authenticate('atlassian-oauth', {session: false, scope: []}),
function(req, res) {
console.log('- Function: /auth/atlassian-oauth - should not be called)');
});
app.get('/auth/atlassian-oauth/callback',
passport.authenticate('atlassian-oauth', {session: false, failureRedirect: '/login'}),
function(req, res) {
console.log('- Function: /auth/atlassian-oauth/callback - Authentication successful!', req.user.access_token);
// Update access token!
// Should I even do this? Shouldn't I already have the correct token?
client.hmset('users:1', 'access_token', req.user.access_token, function() {
client.sadd('access_token:' + req.user.access_token, '1', function() {
client.hgetall('users:1', function(err, result) {
res.redirect('/test?access_token=' + req.user.access_token);
});
});
});
});
So now that you've seen some relevant (just tell me and I'll post more) code, how do I send a rest call to JIRA without getting a 401? :)
EDIT: Any help appreciated! You would make me really happy if you just can point me into the right direction!
Ok. I figured it out! First of all you want to save both you access token and token secret to you db in AtlassianOAuthStrategy. Second, in order to send a REST call to a third party service you can just use http request with OAuth:
var request = require('request');
var oauth = {
signature_method : 'RSA-SHA1',
consumer_key : RsaPublicKey,
private_key : rsaPrivateKey,
token : [get access_token from you db],
token_secret : [get token_secret from you db]'
};
var url = 'http://localhost:2990/jira/rest/api/2/issue/' + id;
request.get({url:url, oauth:oauth, json:true}, function (e, r, issue) {
console.log(issue)
});
Now that everything is working I'm going to start refactoring and reading some more documentation in order to make the design prettier and figure out how to use Redis properly :)

How to authenticate the URLs that serves Static files in Hapi JS

I have a route as below which serves the static pages:
{
method: 'GET',
path: '/webapp/{param*}',
config: {
handler: {
directory :{
path : Path.join(__dirname, '../../webapp/'),
index: true
}
}
}
}
So, I want to check if the user is logged in or not before it takes user to that url "/webapp/#blabla".
User Can only access that url if user is logged in.
I tried to add pre option with a function in the above route but no luck.
{
method: 'GET',
path: '/webapp/{param*}',
pre:[{method:checkUrl, assign:'m1'}],
config: {
handler: {
directory :{
path : Path.join(__dirname, '../../webapp/'),
index: true
}
}
}
}
And the checkUrl function is as:
var checkUrl = function(request, reply) {
if (!request.auth.isAuthenticated) {
// redirect to login page
reply.redirect('/login');
}
return true;
}
Why is that i cannot get redirected to login page?
It depends slightly on which auth scheme you're using but the same principle applies. Here's an example using hapi-auth-basic (adapted from the example in the README):
var Bcrypt = require('bcrypt');
var Hapi = require('hapi');
var Path = require('path');
var Inert = require('inert');
var server = new Hapi.Server();
server.connection({ port: 4000});
var users = {
john: {
username: 'john',
password: '$2a$10$iqJSHD.BGr0E2IxQwYgJmeP3NvhPrXAeLSaGCj6IR/XU5QtjVu5Tm', // 'secret'
name: 'John Doe',
id: '2133d32a'
}
};
var validate = function (request, username, password, callback) {
var user = users[username];
if (!user) {
return callback(null, false);
}
Bcrypt.compare(password, user.password, function (err, isValid) {
callback(err, isValid, { id: user.id, name: user.name });
});
};
server.register([
require('inert'),
require('hapi-auth-basic')
], function (err) {
server.auth.strategy('simple', 'basic', { validateFunc: validate });
server.route({
method: 'GET',
path: '/webapp/{param*}',
config: {
auth: 'simple', // THIS IS THE IMPORTANT BIT
handler: {
directory :{
path : Path.join(__dirname, 'files'),
index: true
}
}
}
});
server.start(function (err) {
if (err) {
throw err;
}
console.log('Server started!');
})
});
The important point is just to add an auth property to the route config with the strategy name. It's the same as you would do for any routes. Have a read of this tutorial, it might clear it up for you.
Are you able to adapt that to your needs?

Categories

Resources