Last active
March 7, 2021 17:50
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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