Lesson 48-Koa Authentication and Authorization

JWT Authentication

Installing Required Packages

First, you need to install the jsonwebtoken and koa-jwt packages.

npm install jsonwebtoken koa-jwt

Creating a JWT Token

A JWT Token typically consists of three parts: header, payload, and signature. In Koa2, you can use the jsonwebtoken package to generate a JWT Token.

const jwt = require('jsonwebtoken');

// Create Token
const createToken = (userId) => {
    return jwt.sign({ userId }, 'secretKey', { expiresIn: '1h' });
};

Verifying a JWT Token

In Koa2, you can use the koa-jwt middleware to verify incoming JWT Tokens.

const jwt = require('jsonwebtoken');
const koaJwt = require('koa-jwt');
const Koa = require('koa');

const app = new Koa();

// Middleware to verify Token
app.use(koaJwt({ secret: 'secretKey' }).unless({ path: [/^\/api\/public/] }));

app.use(async (ctx) => {
    ctx.body = 'Protected route';
});

In the code above, the .unless() method specifies paths that do not require Token verification, typically used for public APIs or login/registration endpoints.

Using JWT Token for Authentication

When a user logs in successfully, you can generate a JWT Token and return it to the client. The client can store the Token in local storage and include it in the Authorization header of subsequent requests.

app.use(async (ctx) => {
    if (ctx.request.method === 'POST' && ctx.request.url === '/login') {
        const { username, password } = ctx.request.body;
        // Assume we have a `users` array to store user information
        const user = users.find((u) => u.username === username && u.password === password);
        if (user) {
            const token = createToken(user.id);
            ctx.body = { token };
        } else {
            ctx.status = 401;
            ctx.body = { message: 'Invalid credentials' };
        }
    }
});

Handling Token Expiration and Refresh

JWT Tokens have an expiration time. When a Token expires, the client needs to re-authenticate or use a refresh Token to obtain a new access Token. In Koa2, you can use jsonwebtoken.verify() to check if a Token has expired.

try {
    const decoded = jwt.verify(token, 'secretKey');
    // Token is valid, proceed with the request
} catch (err) {
    if (err.name === 'TokenExpiredError') {
        // Token has expired, handle refresh logic
    } else {
        // Other errors, such as an invalid Token
    }
}

Error Handling

When using JWT, you need to handle potential errors, such as invalid or expired Tokens. You can use Koa2’s error-handling middleware to capture these errors.

app.use(async (ctx, next) => {
    try {
        await next();
    } catch (err) {
        if (err.name === 'UnauthorizedError') {
            ctx.status = 401;
            ctx.body = { message: 'Unauthorized' };
        } else {
            ctx.throw(500, err);
        }
    }
});

OAuth2 Integration

Registering the Application and Obtaining Credentials

First, you need to register your application with the OAuth2 provider and obtain a client ID and client secret. Using Google as an example:

  • Visit the Google Cloud Console (https://console.cloud.google.com/)
  • Create a new project
  • Enable the Google+ API
  • Create an OAuth2 client ID
  • Obtain the client ID and client secret

Installing Required Packages

In Koa2, you can use passport and passport-google-oauth20 to handle OAuth2 authentication.

npm install passport passport-google-oauth20 koa-passport

Configuring Passport

Configure Passport in Koa2 to handle OAuth2 authentication.

const passport = require('koa-passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(new GoogleStrategy({
    clientID: GOOGLE_CLIENT_ID,
    clientSecret: GOOGLE_CLIENT_SECRET,
    callbackURL: 'http://localhost:3000/auth/google/callback'
}, function(accessToken, refreshToken, profile, cb) {
    // Here, you can query the database to check if the user exists or create a new user
    // Then pass the user information to the cb function
    cb(null, profile);
}));

app.use(passport.initialize());

Implementing OAuth2 Authentication Routes

Next, implement OAuth2 authentication routes, typically including a route to initiate authentication and a callback route.

app.use(async (ctx) => {
    if (ctx.request.url === '/auth/google') {
        await passport.authenticate('google', { scope: ['profile', 'email'] })(ctx);
    }
});

app.use(async (ctx) => {
    if (ctx.request.url === '/auth/google/callback') {
        await passport.authenticate('google', { failureRedirect: '/login' })(ctx);
        ctx.session.user = ctx.state.user;
        ctx.redirect('/');
    }
});

Handling Authenticated User Information

After a user successfully authenticates via OAuth2, you may need to store their information in a session to identify them in subsequent requests.

app.use(async (ctx, next) => {
    if (ctx.session.user) {
        ctx.state.user = ctx.session.user;
    }
    await next();
});

Error Handling

During the OAuth2 authentication process, various errors may occur, such as user denial of authorization or authentication failure. Handle these errors to display user-friendly error messages.

app.use(async (ctx, next) => {
    try {
        await next();
    } catch (err) {
        if (err.name === 'OAuth2Error') {
            ctx.status = 401;
            ctx.body = { message: 'OAuth2 authentication failed.' };
        } else {
            ctx.throw(500, err);
        }
    }
});

User Permission Management

Defining Roles and Permissions

First, define the roles in your application and the permissions associated with each role. For example, you might have roles like “Admin,” “Editor,” and “Guest,” where “Admin” has access to all features, “Editor” can access editing and viewing functions, and “Guest” can only access public information.

const roles = {
    ADMIN: ['*'],
    EDITOR: ['view', 'edit'],
    GUEST: ['view']
};

Storing User Role Information

In the database, store user role information. This can be achieved by adding a “role” field to the user table or creating a separate “roles” table linked to the user table via a foreign key.

CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(255),
    role VARCHAR(255)
);

Implementing Permission Checks

In Koa2, you can implement permission checks using middleware. The middleware runs before processing a request, verifying if the user has permission to access the requested resource.

const checkPermission = (requiredRole) => async (ctx, next) => {
    const userRole = ctx.state.user?.role;
    if (!userRole || !roles[userRole].includes(requiredRole)) {
        ctx.status = 403;
        ctx.body = { message: 'Forbidden' };
        return;
    }
    await next();
};

app.use(checkPermission('ADMIN'), async (ctx) => {
    // This route is accessible only to Admins
    ctx.body = 'Admin route';
});

Using JWT to Pass User Information

To pass user information across requests, you can use JSON Web Tokens (JWT). After a user logs in, generate a JWT containing user role information and send it to the client. The client includes the JWT in the Authorization header of subsequent requests.

const jwt = require('jsonwebtoken');

const createToken = (user) => {
    return jwt.sign(user, 'secretKey', { expiresIn: '1h' });
};

Parsing JWT and Checking Permissions

When receiving a request with a JWT, parse the JWT to extract user information and verify if the user has permission to access the requested resource.

const jwt = require('jsonwebtoken');

const parseToken = async (ctx, next) => {
    const authHeader = ctx.request.header.authorization;
    if (authHeader) {
        const token = authHeader.split(' ')[1];
        try {
            const decoded = jwt.verify(token, 'secretKey');
            ctx.state.user = decoded;
        } catch (err) {
            ctx.status = 401;
            ctx.body = { message: 'Unauthorized' };
            return;
        }
    }
    await next();
};

app.use(parseToken);

Share your love