src/auth/strategies/jwt.auth.ts

Summary

Maintainability
A
0 mins
Test Coverage
D
64%
import { Request, RequestMiddleware, RoutingContext } from "../../aliases";
import { IUser } from "../auth.container";
import { IAuth } from "../auth.interface";
import { JWT } from "../auth.types";

export class JWTUser<Decoding extends object, User> extends IUser<Decoding, User> {
    public token: string;
    private decoding: Decoding & JWT.DefaultFields | null;

    constructor(token: string, private readonly authService: JWTAuth<Decoding, User>) {
        super();
        this.token = token;
    }

    public decode(options?: JWT.DecodeOptions): Decoding &  JWT.DefaultFields {
        if (!this.decoding) {
            this.decoding = (this.authService.decodeToken(this.token, options) as Decoding & JWT.DefaultFields);
        }

        return this.decoding;
    }

    public verify(options?: JWT.VerifyOptions) {
        let result: [ Error | null, Decoding & JWT.DefaultFields | null];

        try {
            result = [ null, this.authService.verifyToken(this.token, options) ];

        } catch (err) {
            result = [ (err as Error), null];
        }

        this.decoding = result[1];

        return result;
    }

    public async load(options?: JWT.DecodeOptions) {
        return this.authService.deserialize(this.decode(options) as any);
    }

    public async assign(user: User, options?: JWT.SignOptions): Promise<string> {
        return this.authService.createToken(await this.authService.serialize(user), options);
    }
}

export abstract class JWTAuth<T extends object, U> extends IAuth<T, U, JWTUser<T, U>> {
    public secret: string;
    public container: string;
    public expiration: string | number;

    private jsonwebtoken = require("jsonwebtoken");

    /** JWT Methods */
    public createToken(data: T, options?: JWT.SignOptions): string {
        return this.jsonwebtoken.sign(data, this.secret, { expiresIn : this.expiration, ...options });
    }

    public verifyToken(token: string, options?: JWT.VerifyOptions): T & JWT.DefaultFields  {
        return (this.jsonwebtoken.verify(token, this.secret, options) as T & JWT.DefaultFields);
    }

    public decodeToken(token: string, options?: JWT.DecodeOptions): T & JWT.DefaultFields | null {
        return (this.jsonwebtoken.decode(token, options) as T & JWT.DefaultFields);
    }

    public abstract deserialize(decoding: T & JWT.DefaultFields | null): Promise<U | null | undefined> |  U | null | undefined;

    /** Extractions */
    public extractUser(ctx: Request): JWTUser<T, U> {
        return new JWTUser<T, U>(this.extractToken(ctx), this);
    }

    public getMiddleware(options?: any): RequestMiddleware {
        return async (request, response) => {
                const context = { request, response };
                const user = this.extractUser(request);

                const [ err ] = user.verify();

                /** Refresh hook */
                const refresh = await this.refresh(context, user, options);

                if (err && !refresh) {
                    response.status(401).send();
                    return;
                }

                const result = await this.authenticate(context, user, options);

                if (result !== true) {
                    response.status(403).send();
                    return;
                }

                request.locals =  { user };
        };
    }

    protected refresh(context: RoutingContext, user: JWTUser<T, U>, options?: any) {
        return false;
    }

    private extractToken(ctx: Request) {
        const header = ctx.headers[this.container];

        let def;
        if (header) {
            const parts = header.split(" ");
            if (parts.length === 2) {
                def = parts[1];
            }
        }

        return def || ctx.cookies[this.container];
    }
}