Lesson 39-Express Authentication and Authorization

Authentication

Authentication Principles

Authentication is the process of verifying a user’s identity, ensuring they are who they claim to be. In web applications, this typically involves users providing a username and password, which the system validates. Once validated, the user is considered “authenticated” and can access protected resources.

Basic Authentication

HTTP Basic Authentication is a simple mechanism that uses the Authorization header, part of the HTTP standard. When a client attempts to access a protected resource, the server sends a 401 Unauthorized response, prompting the client to provide credentials. The client then includes an Authorization header in the format Basic base64(username:password).

Implementation Steps

  • Server Requests Authentication: When a client tries to access a protected resource, the server returns a 401 Unauthorized response with a WWW-Authenticate header indicating the required authentication scheme.
  • Client Sends Credentials: Upon receiving the 401 response, the client includes encoded username and password in the Authorization header of subsequent requests.
  • Server Validates Credentials: The server decodes the Authorization header, verifies the username and password, and grants access if valid; otherwise, it returns another 401 response.

Code Implementation

We will use Node.js with the Express framework to implement HTTP Basic Authentication.

Step 1: Set Up Express Application

Create a basic Express application.

const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.send('Welcome to the public area.');
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});

Step 2: Add Protected Route

Add a protected route that requires authentication.

app.get('/protected', (req, res) => {
    if (req.headers.authorization && req.headers.authorization.startsWith('Basic ')) {
        const authHeader = req.headers.authorization.split(' ')[1];
        const decodedAuthHeader = Buffer.from(authHeader, 'base64').toString('ascii');
        const [username, password] = decodedAuthHeader.split(':');

        if (username === 'admin' && password === 'secret') {
            res.send('Welcome to the protected area.');
        } else {
            res.status(401).send('Unauthorized');
        }
    } else {
        res.status(401).send('Unauthorized');
    }
});

Step 3: Send 401 Response

To comply with HTTP Basic Authentication standards, return a 401 Unauthorized response with a WWW-Authenticate header when credentials are missing or incorrect.

app.get('/protected', (req, res) => {
    if (!req.headers.authorization || !req.headers.authorization.startsWith('Basic ')) {
        res.set('WWW-Authenticate', 'Basic realm="Protected Area"');
        res.status(401).send('Unauthorized');
        return;
    }

    const authHeader = req.headers.authorization.split(' ')[1];
    const decodedAuthHeader = Buffer.from(authHeader, 'base64').toString('ascii');
    const [username, password] = decodedAuthHeader.split(':');

    if (username === 'admin' && password === 'secret') {
        res.send('Welcome to the protected area.');
    } else {
        res.status(401).send('Unauthorized');
    }
});

Step 4: Test with Postman or Browser

Use Postman or a browser’s developer tools to send a GET request to /protected, including an Authorization header with the value Basic YWRtaW46c2VjcmV0 (Base64 encoding of admin:secret).

Security and Limitations

While HTTP Basic Authentication is simple, it has significant security limitations:

  • Transmission Security: Credentials are sent in plaintext, so HTTPS is required to prevent man-in-the-middle attacks.
  • Persistence Issues: It does not support persistent sessions, requiring credentials to be sent with every request.

Form-Based Authentication

Form-based authentication involves users entering a username and password in an HTML form, which is submitted to the server for validation. If valid, the server creates a session and sends a session identifier (usually a cookie) to the client for subsequent requests to identify the user.

Implementation Steps

  • Create a Form: Build a frontend form to collect username and password.
  • Handle Form Submission: Set up a backend route to process form submissions.
  • Validate User: Verify the provided credentials on the server.
  • Create Session: If credentials are valid, create a session and store the session ID in a cookie.
  • Protect Routes: Check the session ID in the cookie for protected routes and validate its authenticity.

Code Implementation

Using Express and express-session Middleware

Step 1: Set Up Express and Session Middleware

const express = require('express');
const session = require('express-session');
const app = express();

app.use(session({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: true,
}));

app.use(express.urlencoded({ extended: true }));

Step 2: Create Login Route

app.post('/login', (req, res) => {
    const { username, password } = req.body;
    if (username === 'admin' && password === 'password') {
        req.session.authenticated = true;
        res.redirect('/dashboard');
    } else {
        res.status(401).send('Invalid credentials');
    }
});

Step 3: Protect Routes

app.get('/dashboard', (req, res) => {
    if (req.session.authenticated) {
        res.send('Welcome to your dashboard');
    } else {
        res.redirect('/login');
    }
});

JSON Web Tokens (JWT)

JWT is an open standard (RFC 7519) for securely transmitting information between parties. It consists of three parts: Header, Payload, and Signature. JWT is ideal for stateless environments since all authentication information is contained in the token, eliminating the need for server-side session storage.

JWT Workflow

  • Generate Token: After a user logs in, the server generates a JWT and sends it to the client.
  • Client Stores Token: The client typically stores the JWT in local storage or a cookie.
  • Attach to Requests: The client attaches the JWT to the Authorization header in subsequent requests.
  • Verify Token: The server verifies the JWT’s signature and payload to confirm its validity.

Stateless Authentication with JWT

Step 1: Install JWT Package

npm install jsonwebtoken

Step 2: Create JWT Token

const jwt = require('jsonwebtoken');

app.post('/login', (req, res) => {
    const { username, password } = req.body;
    if (username === 'admin' && password === 'password') {
        const token = jwt.sign({ username }, 'your-secret-key', { expiresIn: '1h' });
        res.json({ token });
    } else {
        res.status(401).send('Invalid credentials');
    }
});

Step 3: Protect Routes

function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    if (token == null) return res.sendStatus(401);

    jwt.verify(token, 'your-secret-key', (err, user) => {
        if (err) return res.sendStatus(403);
        req.user = user;
        next();
    });
}

app.get('/dashboard', authenticateToken, (req, res) => {
    res.send(`Welcome to your dashboard, ${req.user.username}`);
});

OAuth 2.0

OAuth 2.0 is an open standard for authorizing applications to access user resources on another service without sharing credentials. It is primarily used for authorization, not authentication.

OAuth 2.0 Workflow Overview

  • Authorization Request: The client application redirects the user to the authorization server’s login page.
  • User Authentication: The user logs in and authorizes the client application.
  • Authorization Code: The authorization server returns an authorization code to the client.
  • Token Request: The client uses the authorization code to request an access token from the authorization server.
  • Access Token: The authorization server returns an access token to the client.
  • Resource Access: The client uses the access token to access resources on the resource server.

Third-Party Authentication Integration

Integrating OAuth 2.0 in Express typically involves using a library like Passport.js, which provides strategies to simplify OAuth integration.

Step 1: Configure Passport OAuth Strategy

const passport = require('passport');
const GitHubStrategy = require('passport-github').Strategy;

passport.use(new GitHubStrategy({
    clientID: 'YOUR_CLIENT_ID',
    clientSecret: 'YOUR_CLIENT_SECRET',
    callbackURL: 'http://localhost:3000/auth/github/callback'
}, function(accessToken, refreshToken, profile, cb) {
    // Query database to find or create user
    User.findOrCreate({ githubId: profile.id }, function(err, user) {
        return cb(err, user);
    });
}));

Step 2: Set Up OAuth Login Routes

app.get('/auth/github',
    passport.authenticate('github'));

app.get('/auth/github/callback',
    passport.authenticate('github', { failureRedirect: '/login' }),
    function(req, res) {
        // Successful authentication, redirect to homepage
        res.redirect('/');
    });

OpenID Connect

OpenID Connect is an identity authentication layer built on OAuth 2.0, allowing client applications to obtain access tokens and user information, such as name and email address.

Differences Between OpenID Connect and OAuth

  • OAuth 2.0: Focuses on authorization, allowing one application to access resources on another.
  • OpenID Connect: Adds identity authentication capabilities to OAuth 2.0, enabling applications to verify user identity.

Implementation Example

Using OpenID Connect in Express can be achieved with Passport.js’s OpenID Connect strategy.

Step 1: Configure Passport OpenID Connect Strategy

const OpenIDConnectStrategy = require('passport-openidconnect').Strategy;

passport.use(new OpenIDConnectStrategy({
    issuer: 'https://your-op-server.com',
    authorizationURL: 'https://your-op-server.com/authorize',
    tokenURL: 'https://your-op-server.com/token',
    userInfoURL: 'https://your-op-server.com/userinfo',
    clientID: 'YOUR_CLIENT_ID',
    clientSecret: 'YOUR_CLIENT_SECRET',
    callbackURL: 'http://localhost:3000/auth/openid/callback',
    scope: ['openid', 'profile', 'email']
}, function(issuer, profile, done) {
    User.findOrCreate({ openid: profile.id }, function(err, user) {
        return done(err, user);
    });
}));

Step 2: Set Up OpenID Connect Login Routes

app.get('/auth/openid',
    passport.authenticate('openid-connect'));

app.get('/auth/openid/callback',
    passport.authenticate('openid-connect', { failureRedirect: '/login' }),
    function(req, res) {
        // Successful authentication, redirect to homepage
        res.redirect('/');
    });

Multi-Factor Authentication (MFA)

Types of MFA

  • Knowledge Factor: Information only the user knows, such as a password or security question answer.
  • Possession Factor: Something the user possesses, like a smartphone or hardware token.
  • Biometric Factor: A user’s physical characteristic, such as a fingerprint or facial recognition.

Implementing MFA

Implementing MFA in an Express application can involve methods like Time-Based One-Time Passwords (TOTP), as used by Google Authenticator.

Step 1: Generate and Distribute Secret Key

const speakeasy = require('speakeasy');

// Generate a TOTP secret
const secret = speakeasy.generateSecret({ length: 20 });

// Send the secret as a QR code to the user
res.send(`<img src="${secret.otpauth_url}" />`);

Step 2: Verify TOTP

app.post('/mfa', (req, res) => {
    const code = req.body.code;
    const verified = speakeasy.totp.verify({
        secret: secret.base32,
        encoding: 'base32',
        token: code
    });

    if (verified) {
        // MFA verification successful
        res.send('MFA verification successful');
    } else {
        // MFA verification failed
        res.status(401).send('MFA verification failed');
    }
});

Authentication Libraries and Middleware

Passport.js is a popular authentication middleware for Node.js and Express, supporting various authentication strategies, including MFA.

Introduction to Passport.js

Passport.js is a flexible and modular authentication middleware offering strategies for local username/password, OAuth, OpenID Connect, MFA, and more.

Implementing MFA with Passport.js

Assuming basic username/password authentication is implemented, add TOTP-based MFA.

Step 1: Configure Passport.js

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const SpeakeasyStrategy = require('passport-speakeasy').Strategy;

passport.use(new LocalStrategy(
    function(username, password, done) {
        // Implement local username/password authentication logic
    }
));

passport.use(new SpeakeasyStrategy(
    function(user, token, done) {
        // Implement TOTP verification logic
    }
));

Step 2: Set Up MFA Routes

app.post('/login', passport.authenticate('local'), (req, res) => {
    // After successful username/password validation, redirect to MFA page
    res.redirect('/mfa');
});

app.post('/mfa', passport.authenticate('speakeasy'), (req, res) => {
    // After successful MFA verification, log in the user
    req.login(user, function(err) {
        if (err) { return next(err); }
        return res.redirect('/');
    });
});

Authorization

Role-Based Access Control (RBAC)

RBAC Concept

RBAC is an authorization model where users are assigned roles with specific permissions. Each role defines a set of operations, and users access resources based on their roles.

Permission Assignment and Management

In Express, RBAC can be implemented using middleware.

Step 1: Define Roles and Permissions

const roles = {
    admin: ['read', 'write', 'delete'],
    editor: ['read', 'write'],
    viewer: ['read']
};

Step 2: Create Authorization Middleware

function authorize(role, resource, action) {
    return function(req, res, next) {
        if (roles[role].includes(action)) {
            next();
        } else {
            res.status(403).send('Access denied');
        }
    };
}

Step 3: Use Authorization Middleware

app.get('/admin', authorize('admin', 'admin', 'read'), (req, res) => {
    res.send('Admin panel');
});

Attribute-Based Access Control (ABAC)

How ABAC Works

ABAC is a dynamic authorization model that makes access decisions based on attributes of users, resources, and the environment. This allows decisions to consider factors like time, location, or device type, in addition to user roles.

Advantages of Implementing ABAC

  • Flexibility: ABAC supports fine-grained access control, dynamically adjusting permissions based on context.
  • Scalability: ABAC expresses authorization rules through attributes, making it easy to extend and modify.

Implementing ABAC

Implement ABAC in Express using a library like casbin, which provides robust ABAC support.

Step 1: Install casbin

npm install casbin

Step 2: Define Policy

const Casbin = require('casbin');

async function loadPolicy() {
    const e = new Casbin.Enforcer('path/to/model.conf', 'path/to/policy.csv');
    await e.loadPolicy();
    return e;
}

const enforcer = loadPolicy();

Step 3: Check Permissions

app.get('/secure-resource', async (req, res) => {
    const userId = req.user.id;
    const resourceId = '123';
    const action = 'read';

    try {
        const allowed = await enforcer.enforce(userId, resourceId, action);
        if (allowed) {
            res.send('Access granted');
        } else {
            res.status(403).send('Access denied');
        }
    } catch (error) {
        res.status(500).send(error);
    }
});

Access Control Lists (ACL)

ACL Concept

ACL is a resource-based access control mechanism where each resource has an associated list defining which subjects can perform specific operations on it. ACLs allow fine-grained access control for each resource.

Comparison of ACL and RBAC

  • ACL: Resource-based access control, with each resource having an independent access control list specifying who can access it and how.
  • RBAC: Role-based access control, where users are assigned roles that define accessible resources and operations.

Permission Checks

Performing Permission Checks Before Route Handling

In Express, permission checks are typically implemented via middleware, intercepting requests before they reach the route handler to verify permissions.

Step 1: Define Permission Check Middleware

function checkPermission(permission) {
    return function(req, res, next) {
        // Check if the user has the required permission
        if (req.user.permissions.includes(permission)) {
            next(); // Permission granted, proceed with request
        } else {
            res.status(403).send('Access Denied'); // Permission denied, reject request
        }
    };
}

Step 2: Use Permission Check Middleware

app.get('/admin', checkPermission('admin-access'), (req, res) => {
    res.send('Admin panel');
});

Authorization Libraries and Frameworks

Using casbin for Authorization

casbin is a powerful authorization library supporting RBAC, ABAC, and ACL.

Step 1: Install casbin

npm install casbin

Step 2: Define Model and Policy

const Casbin = require('casbin');

// Define model
const modelText = `
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`;

// Create enforcer
const e = new Casbin.Enforcer('model.conf', 'policy.csv');

// Load policy
await e.loadPolicy();

Step 3: Check Permissions

app.get('/secure-resource', async (req, res) => {
    const userId = req.user.id;
    const resourceId = '123';
    const action = 'read';

    try {
        const allowed = await e.enforce(userId, resourceId, action);
        if (allowed) {
            res.send('Access granted');
        } else {
            res.status(403).send('Access denied');
        }
    } catch (error) {
        res.status(500).send(error);
    }
});

Using feathers-authentication for Authorization

feathers-authentication is an authentication and authorization plugin for Feathers.js, also compatible with Express applications.

Step 1: Install feathers-authentication

npm install @feathersjs/authentication-client @feathersjs/authentication-jwt

Step 2: Configure feathers-authentication

const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');

const app = feathers();

app.configure(authentication());

Step 3: Use feathers-authentication for Authorization

app.service('users').find().then(data => {
    console.log(data);
}).catch(err => {
    console.error(err);
});
Share your love