Showing posts with label passport. Show all posts
Showing posts with label passport. Show all posts

Thursday, July 4, 2019

Session-less roles authorization with Passport using Authorization header + JWT

const express = require('express');

const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const randtoken = require('rand-token');

const passport = require('passport');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');

const refreshTokens = {};
const SECRET = "sauce";

const options = {   
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: SECRET
};

passport.use(new JwtStrategy(options, (jwtPayload, done) => 
{
    const expirationDate = new Date(jwtPayload.exp * 1000);
    if (new Date() >= expirationDate) {
        return done(null, false);
    }

    const user = jwtPayload;
    done(null, user);
}));


const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(passport.initialize());

app.post('/login', (req, res, next) => 
{
    const { username, password } = req.body;
    
    const accessToken = generateAccessToken(username, getRole(username));    
    const refreshToken = randtoken.uid(42);

    refreshTokens[refreshToken] = username;

    res.json({accessToken, refreshToken});
});

function generateAccessToken(username, role, expiresInSeconds = 60)
{
    const user = {
        username,
        role
    };

    const accessToken = jwt.sign(user, SECRET, { expiresIn: expiresInSeconds });
    
    return accessToken;
}


function getRole(username)
{
    switch (username) {
        case 'linus':
            return 'admin';
        default:
            return 'user';        
    }
}


app.post('/token', (req, res) => 
{
    const { username, refreshToken } = req.body;
    
    if (refreshToken in refreshTokens && refreshTokens[refreshToken] === username) {        
        const accessToken = generateAccessToken(username, getRole(username));
        res.json({accessToken});
    }
    else {
        res.sendStatus(401);
    }
});

app.delete('/token/:refreshToken', (req, res, next) => 
{
    const { refreshToken } = req.params;
    if (refreshToken in refreshTokens) {
        delete refreshTokens[refreshToken];
    }

    res.send(204);
});

app.post('/restaurant-reservation', passport.authenticate('jwt', {session: false}), (req, res) => 
{
    const { user } = req;
    const { guestsCount } = req.body;

    res.json({user, guestsCount});
});

app.get('/user-accessible', authorize(), (req, res) => 
{
    res.json({message: 'for all users', user: req.user});
});

app.get('/admin-accessible', authorize('admin'), (req,res) => 
{
    res.json({message: 'for admins only', user: req.user});
});


function authorize(roles = []) 
{
    if (typeof roles === 'string') {
        roles = [roles];
    }

    return [
        passport.authenticate('jwt', {session: false}),

        (req, res, next) => 
        {
            if (roles.length > 0 && !roles.includes(req.user.role)) {
                return res.status(403).json({message: 'No access'});
            };

            return next();
        }
    ];
}

app.listen(8080);


Test login for non-admin:
$ curl -i -H "Content-Type: application/json" --request POST --data '{"username": "richard", "password": "stallman"}' http://localhost:8080/login


Output:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 253
ETag: W/"fd-eFe0WyxYJMwm+IYU2RknNmwLN7c"
Date: Thu, 04 Jul 2019 11:18:34 GMT
Connection: keep-alive

{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJpY2hhcmQiLCJyb2xlIjoidXNlciIsImlhdCI6MTU2MjIzOTExNCwiZXhwIjoxNTYyMjM5MTc0fQ.eVMIVLoq66wwnvX7R7_YE_Va5uUIupcWqZFJIql2VOo","refreshToken":"ZExtyNqF19XwCF8htNABs9rzwDV5lltlEQxGAGVaeV"}

Test non-admin role against user-accessible resource:
$ curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJpY2hhcmQiLCJyb2xlIjoidXNlciIsImlhdCI6MTU2MjIzOTExNCwiZXhwIjoxNTYyMjM5MTc0fQ.eVMIVLoq66wwnvX7R7_YE_Va5uUIupcWqZFJIql2VOo" --request GET  http://localhost:8080/user-accessible


Output:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 103
ETag: W/"67-cGYUdSCA9Hfxbr3kmo6EfcwJGFk"
Date: Thu, 04 Jul 2019 11:19:09 GMT
Connection: keep-alive

{"message":"for all users","user":{"username":"richard","role":"user","iat":1562239114,"exp":1562239174}}


Test non-admin role against admin-accessible resource:
$ curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJpY2hhcmQiLCJyb2xlIjoidXNlciIsImlhdCI6MTU2MjIzOTExNCwiZXhwIjoxNTYyMjM5MTc0fQ.eVMIVLoq66wwnvX7R7_YE_Va5uUIupcWqZFJIql2VOo" --request GET  http://localhost:8080/admin-accessible


Output:
HTTP/1.1 403 Forbidden
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 23
ETag: W/"17-wr3eIgD4+9Bp6mVHZCD0DXWfISk"
Date: Thu, 04 Jul 2019 11:19:16 GMT
Connection: keep-alive

{"message":"No access"}



Test login for admin:
$ curl -i -H "Content-Type: application/json" --request POST --data '{"username": "linus", "password": "torvalds"}' http://localhost:8080/login

Output:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 251
ETag: W/"fb-ffoTYONCm1BVpI+gaFqCXe7AX2g"
Date: Thu, 04 Jul 2019 11:23:05 GMT
Connection: keep-alive

{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpbnVzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTYyMjM5Mzg1LCJleHAiOjE1NjIyMzk0NDV9.hU09-ESu5VADYgC12R-CBtLga4lmGnpGC1AAwxg7t_Y","refreshToken":"8RItXk4v6kV8W68paZk3av34vj3oV5z1vnQdSLAZG7"}


Test admin role against admin-accessible resource:
$ curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpbnVzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTYyMjM5Mzg1LCJleHAiOjE1NjIyMzk0NDV9.hU09-ESu5VADYgC12R-CBtLga4lmGnpGC1AAwxg7t_Y" --request GET  http://localhost:8080/admin-accessible


Output:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 104
ETag: W/"68-MXNiAugqialVCopW9uv3AMKWHjU"
Date: Thu, 04 Jul 2019 11:23:42 GMT
Connection: keep-alive

{"message":"for admins only","user":{"username":"linus","role":"admin","iat":1562239385,"exp":1562239445}}


And of course user-accessible resource is available to admin role too:
$ curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpbnVzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTYyMjM5Mzg1LCJleHAiOjE1NjIyMzk0NDV9.hU09-ESu5VADYgC12R-CBtLga4lmGnpGC1AAwxg7t_Y" --request GET  http://localhost:8080/user-accessible


Output:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 102
ETag: W/"66-fLBX3f6AfyVZNVcfBNUakwJko5w"
Date: Thu, 04 Jul 2019 11:23:51 GMT
Connection: keep-alive

{"message":"for all users","user":{"username":"linus","role":"admin","iat":1562239385,"exp":1562239445}}

Session-less authentication with Passport using Authorization header + JWT

const express = require('express');

const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const randtoken = require('rand-token');

const passport = require('passport');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');

const refreshTokens = {};
const SECRET = "sauce";

const options = {   
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: SECRET
};

passport.use(new JwtStrategy(options, (jwtPayload, done) => 
{
    const expirationDate = new Date(jwtPayload.exp * 1000);
    if (new Date() >= expirationDate) {
        return done(null, false);
    }

    const user = jwtPayload;
    done(null, user);
}));


const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(passport.initialize());

app.post('/login', (req, res, next) => 
{
    const { username, password } = req.body;
    
    const accessToken = generateAccessToken(username, getRole(username));    
    const refreshToken = randtoken.uid(42);

    refreshTokens[refreshToken] = username;

    res.json({accessToken, refreshToken});
});

function generateAccessToken(username, role, expiresInSeconds = 60)
{
    const user = {
        username,
        role
    };

    const accessToken = jwt.sign(user, SECRET, { expiresIn: expiresInSeconds });
    
    return accessToken;
}


function getRole(username)
{
    switch (username) {
        case 'linus':
            return 'admin';
        default:
            return 'user';        
    }
}


app.post('/token', (req, res) => 
{
    const { username, refreshToken } = req.body;
    
    if (refreshToken in refreshTokens && refreshTokens[refreshToken] === username) {        
        const accessToken = generateAccessToken(username, getRole(username));
        res.json({accessToken});
    }
    else {
        res.sendStatus(401);
    }
});

app.delete('/token/:refreshToken', (req, res, next) => 
{
    const { refreshToken } = req.params;
    if (refreshToken in refreshTokens) {
        delete refreshTokens[refreshToken];
    }

    res.send(204);
});

app.post('/restaurant-reservation', passport.authenticate('jwt', {session: false}), (req, res) => 
{
    const { user } = req;
    const { guestsCount } = req.body;

    res.json({user, guestsCount});
});


app.listen(8080);

Do login:
$ curl -i -H "Content-Type: application/json" --request POST --data '{"username": "linus", "password": "torvalds"}' http://localhost:8080/login

Output:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 465
ETag: W/"1d1-fL53kZkGhDz1fE7e8SWj1fsPmqk"
Date: Thu, 04 Jul 2019 05:34:50 GMT
Connection: keep-alive

{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpbnVzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTYyMjE4NDkwLCJleHAiOjE1NjIyMTg1NTB9.BJzSGzbVgy_WZJAABoQ_xCPgf6OZgiezn7KxAiVrkm4","refreshToken":"KPImAwtuR4KMlU6RA7cdoouBJ3JY2XxiRzd4hTu3gU"}



Test authentication middleware:
$ curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpbnVzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTYyMjE4NDkwLCJleHAiOjE1NjIyMTg1NTB9.BJzSGzbVgy_WZJAABoQ_xCPgf6OZgiezn7KxAiVrkm4" \
-H "Content-Type: application/json" \
--request POST --data '{"guestsCount": 7}' \
http://localhost:8080/restaurant-reservation

Output:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 94
ETag: W/"5e-IgmolWlx16EymTgKApJWpv27g74"
Date: Thu, 04 Jul 2019 05:35:38 GMT
Connection: keep-alive

{"user":{"username":"linus","role":"admin","iat":1562218490,"exp":1562218550},"guestsCount":7}


Do again the command above after 60 seconds. Output:
HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Date: Thu, 04 Jul 2019 05:36:24 GMT
Connection: keep-alive
Content-Length: 12

Unauthorized


As the existing access token expired, we need to get new access token using the refresh token:
$ curl -i -H "Content-Type: application/json" \
--request POST \
--data '{"username": "linus", "refreshToken": "KPImAwtuR4KMlU6RA7cdoouBJ3JY2XxiRzd4hTu3gU"}' \
http://localhost:8080/token


Output:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 191
ETag: W/"bf-is3S3uVXUUs7vW0DjF+cQ+bzj70"
Date: Thu, 04 Jul 2019 05:37:56 GMT
Connection: keep-alive

{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpbnVzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTYyMjE4Njc2LCJleHAiOjE1NjIyMTg3MzZ9.bsy2oAm8qU8R6xM5b4SpxqJm8l8Ca4ssRu6VMMtZZq4"}


Test authentication middleware using the new access token:
$ curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpbnVzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTYyMjE4NzY0LCJleHAiOjE1NjIyMTg4MjR9.Fw_HFTJmTc-dOjzExoJF_Wk2QGwYINaG_d6cuoc6fjQ" \
-H "Content-Type: application/json" \
--request POST --data '{"guestsCount": 7}' \
http://localhost:8080/restaurant-reservation

Output:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 94
ETag: W/"5e-E8689sgC1zfAYBFoQnj/jmIPd+4"
Date: Thu, 04 Jul 2019 05:39:54 GMT
Connection: keep-alive

{"user":{"username":"linus","role":"admin","iat":1562218764,"exp":1562218824},"guestsCount":7}


If we need the user to re-login, just revoke (delete) the refresh token:
$ curl -i -X DELETE http://localhost:8080/token/KPImAwtuR4KMlU6RA7cdoouBJ3JY2XxiRzd4hTu3gU

Output:
HTTP/1.1 204 No Content
X-Powered-By: Express
ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI"
Date: Thu, 04 Jul 2019 05:43:43 GMT
Connection: keep-alive


Test getting new access token:
$ curl -i -H "Content-Type: application/json" \
--request POST \
--data '{"username": "linus", "refreshToken": "KPImAwtuR4KMlU6RA7cdoouBJ3JY2XxiRzd4hTu3gU"}' \
http://localhost:8080/token

Output:
HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Content-Length: 12
ETag: W/"c-dAuDFQrdjS3hezqxDTNgW7AOlYk"
Date: Thu, 04 Jul 2019 05:44:24 GMT
Connection: keep-alive

Related code, roles authorization: https://www.anicehumble.com/2019/07/session-less-authorization-with-passport-authorization-header-jwt.html

Saturday, April 28, 2018

Session-less Facebook and Google Login with Passport using Cookie + JWT

The id and display name returned to app after logging to Facebook or Google, is encrypted with JWT and then stored to cookie. Line 74 and 77





Authenticating a page or a service is done by adding the passport-jwt middleware to a route. Line 15 and line 22.





Extraction of JWT from the cookie is done by creating a custom extractor for passport-jwt. Line 66 and 73




Full code can be downloaded from https://github.com/MichaelBuen/test-code-auth


Here's the structure of ILoggedUserJwtPayload:

import { ILoggedUser } from './ILoggedUser';

export interface ILoggedUserJwtPayload
{
    // subject
    sub: ILoggedUser;

    // expires
    exp: number;
}

This is the ILoggedUserJwtPayload sub property's structure:

export interface ILoggedUser
{
    source: string | undefined; // provider, e.g., facebook, google
    id: string | undefined; // id
    shownName: string | undefined; // displayName
}


Here's another route authenticated by passport-jwt middleware:

app.get('/api/v1/me',
    passport.authenticate('jwt', {session: false}),
    (req, res) =>
    {
        const user = req.user as ILoggedUser;

        res.json(user);
    }
);



Happy coding!