Skip to content

Instantly share code, notes, and snippets.

@thejhh
Last active March 7, 2021 17:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thejhh/61bf4d704b30ef9297d42881d20a6b0b to your computer and use it in GitHub Desktop.
Save thejhh/61bf4d704b30ef9297d42881d20a6b0b to your computer and use it in GitHub Desktop.
Simple stateless JWT Authentication REST service, written in TypeScript, using experimental request mapping annotations
// Copyright (c) 2020-2021 Jaakko-Heikki Heusala. All rights reserved.
//
// Stateless JWT Email Authentication REST service for development purposes.
//
// The flow diagram is here: https://docs.google.com/drawings/d/1MzOY16-gTuEN7A0pRogTkM5oqARpErEF1mW9dzDUD_M/edit
import Request, {GetMapping, PostMapping, RequestBody, RequestMapping} from "./shared/nor/ts/Request";
import Json from "./shared/nor/ts/Json";
import {isLoginRequestBodyDTO} from "./types/LoginRequestBodyDTO";
import LoginJwtPayload, {AUTH_JWT_TYPE_PROPERTY} from "./types/LoginJwtPayload";
import JwtService from "./JwtService";
import CRYPTO from "crypto";
import LoginResponseBodyDTO from "./types/LoginResponseBodyDTO";
import {LOGIN_JWT_EXPIRATION_TIME} from "./auth-constants";
import AuthJwtType from "./types/AuthJwtType";
import LogService from "./shared/nor/ts/LogService";
import AuthEmailService from "./AuthEmailService";
import CreateSessionResponseBodyDTO from "./types/CreateSessionResponseDTO";
import {isCreateSessionRequestBodyDTO} from "./types/CreateSessionRequestBodyDTO";
import SessionJwtPayload from "./types/SessionJwtPayload";
import JwtGraveyardService from "./JwtGraveyardService";
import VerifySessionResponseBodyDTO from "./types/VerifySessionResponseDTO";
import {isVerifySessionRequestBodyDTO} from "./types/VerifySessionRequestBodyDTO";
import DeleteSessionResponseBodyDTO from "./types/DeleteSessionResponseDTO";
import {isDeleteSessionRequestBodyDTO} from "./types/DeleteSessionRequestBodyDTO";
const LOG = LogService.createLogger('AuthController');
@RequestMapping('/')
export class AuthController {
@GetMapping('/')
static getIndex () : any {
return {};
}
@GetMapping('/login', '/authenticate', '/verify', '/delete')
static getResource () : any {
return {};
}
@PostMapping('/login')
public static async login (
@RequestBody body: Json | undefined
) : Promise<LoginResponseBodyDTO | undefined> {
if (!isLoginRequestBodyDTO(body)) {
Request.throwBadRequestError("Body not type of LoginResponseBodyDTO");
return undefined;
}
try {
const email = body.email;
const jti = AuthController._generateJtiString();
const iat = AuthController._getCurrentTime();
const exp = AuthController._getExpirationTime(iat, LOGIN_JWT_EXPIRATION_TIME);
const payload : LoginJwtPayload = {
[AUTH_JWT_TYPE_PROPERTY]: AuthJwtType.LOGIN,
email,
jti,
iat,
exp
};
const userCode = AuthController._generateUserCodeString();
LOG.debug(`The generated user code was: "${userCode}"`);
const signature = JwtService.signLoginJwt(payload, userCode);
await AuthEmailService.sendUserCodeMessage(email, userCode);
return {
email : email,
token : signature,
tokenId : jti,
tokenExpiration : exp,
tokenCreated : iat,
tokenPayload : payload
};
} catch (err) {
LOG.error('login: Error: ', err);
Request.throwInternalErrorRequestError("Internal Error");
return;
}
}
@PostMapping('/authenticate')
public static async createSession (
@RequestBody body: Json | undefined
) : Promise<CreateSessionResponseBodyDTO | undefined> {
if (!isCreateSessionRequestBodyDTO(body)) {
Request.throwBadRequestError("Body not type of CreateSessionResponseBodyDTO");
return undefined;
}
try {
const token : string = body.token;
const userCode : string = body.userCode;
LOG.debug(`The provided user code was: "${userCode}" for "${token}"`);
const jti = AuthController._generateJtiString();
const iat = AuthController._getCurrentTime();
const exp = AuthController._getExpirationTime(iat, LOGIN_JWT_EXPIRATION_TIME);
if (!JwtService.verifyLoginJwt(token, userCode)) {
// FIXME: Check if a better HTTP status could be used
Request.throwBadRequestError("Bad credentials");
return undefined;
}
const decodedJwt = JwtService.decodeLoginJwt(token);
if (JwtGraveyardService.isJwtInvalidatedAndInvalidateIfNot(decodedJwt.payload.jti, decodedJwt.payload.exp)) {
// FIXME: Check if a better HTTP status could be used
Request.throwBadRequestError("Bad credentials");
return undefined;
}
const email = decodedJwt.payload.email;
const payload : SessionJwtPayload = {
[AUTH_JWT_TYPE_PROPERTY]: AuthJwtType.SESSION,
email_verified: email,
jti,
iat,
exp
};
const signature = JwtService.signSessionJwt(payload);
return {
email : email,
token : signature,
tokenId : jti,
tokenExpiration : exp,
tokenCreated : iat,
tokenPayload : payload
};
} catch (err) {
LOG.error('login: Error: ', err);
Request.throwInternalErrorRequestError("Internal Error");
return;
}
}
@PostMapping('/verify')
public static async verifySession (
@RequestBody body: Json | undefined
) : Promise<VerifySessionResponseBodyDTO | undefined> {
if (!isVerifySessionRequestBodyDTO(body)) {
Request.throwBadRequestError("Body not type of VerifySessionResponseBodyDTO");
return undefined;
}
try {
const token : string = body.token;
LOG.debug(`The provided session token is: `, token);
if (!JwtService.verifySessionJwt(token)) {
// FIXME: Check if a better HTTP status could be used
Request.throwBadRequestError("Bad credentials");
return undefined;
}
const decodedJwt = JwtService.decodeSessionJwt(token);
const payload : SessionJwtPayload = decodedJwt.payload;
const jti : string = payload.jti;
const exp : number = payload.exp;
if (JwtGraveyardService.isJwtInvalidated(jti, exp)) {
// FIXME: Check if a better HTTP status could be used
Request.throwBadRequestError("Bad credentials");
return undefined;
}
const signature : string = decodedJwt.signature;
const email : string = payload.email_verified;
const iat : number = payload.iat;
return {
email : email,
token : signature,
tokenId : jti,
tokenExpiration : exp,
tokenCreated : iat,
tokenPayload : payload
};
} catch (err) {
LOG.error('login: Error: ', err);
Request.throwInternalErrorRequestError("Internal Error");
return;
}
}
@PostMapping('/delete')
public static async deleteSession (
@RequestBody body: Json | undefined
) : Promise<DeleteSessionResponseBodyDTO | undefined> {
if (!isDeleteSessionRequestBodyDTO(body)) {
Request.throwBadRequestError("Body not type of DeleteSessionResponseBodyDTO");
return undefined;
}
try {
const token : string = body.token;
LOG.debug(`The provided session token is: `, token);
if (!JwtService.verifySessionJwt(token)) {
// FIXME: Check if a better HTTP status could be used
Request.throwBadRequestError("Bad credentials");
return undefined;
}
const decodedJwt = JwtService.decodeSessionJwt(token);
const payload : SessionJwtPayload = decodedJwt.payload;
const jti : string = payload.jti;
const exp : number = payload.exp;
if (JwtGraveyardService.isJwtInvalidatedAndInvalidateIfNot(jti, exp)) {
// FIXME: Check if a better HTTP status could be used
Request.throwBadRequestError("Bad credentials");
return undefined;
}
const signature : string = decodedJwt.signature;
const email : string = payload.email_verified;
const iat : number = payload.iat;
return {
email : email,
token : signature,
tokenId : jti,
tokenExpiration : exp,
tokenCreated : iat,
tokenPayload : payload
};
} catch (err) {
LOG.error('login: Error: ', err);
Request.throwInternalErrorRequestError("Internal Error");
return;
}
}
private static _generateJtiString () : string {
return CRYPTO.randomBytes(16).toString('hex');
}
private static _generateUserCodeString () : string {
return CRYPTO.randomBytes(4).toString('hex');
}
private static _getCurrentTime () : number {
return Math.floor(Date.now() / 1000);
}
private static _getExpirationTime (currentTime: number, expirationTime: number) : number {
return currentTime + expirationTime;
}
}
export default AuthController;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment