import { Injectable , Logger } from '@nestjs/common' ; import { InjectModel } from '@nestjs/mongoose' ; import { randomBytes } from 'crypto' ; import { sign , SignOptions , verify } from 'jsonwebtoken' ; import * as moment from 'moment' ; import { Types } from 'mongoose' ; import { InstanceType , ModelType } from 'typegoose' ; import { BaseService } from '../../shared/base.service' ; import { ConfigurationService } from '../../shared/configuration/configuration.service' ; import { NotFoundError } from '../../shared/errors/not-found.error' ; import { MapperService } from '../../shared/mapper/mapper.service' ; import { JwtPayload } from '../jwt-payload' ; import { LoginResponse } from '../models/login-response' ; import { RefreshToken } from '../models/refresh-token.model' ; import { LoginResponseVm } from '../view-models/login-response-vm.model' ; import uuid = require ( 'uuid' ) ; @ Injectable ( ) export class TokenService extends BaseService < RefreshToken > { private readonly logger = new Logger ( TokenService . name ) ; private readonly jwtOptions : SignOptions ; private readonly jwtKey : string ; private refreshTokenTtl : number ; private expiresInDefault : string | number ; private readonly usersExpired : number [ ] = [ ] ; constructor ( @ InjectModel ( RefreshToken . modelName ) tokenModel , private readonly configurationService , private readonly mapperService ) { super ( ) ; this . _model = tokenModel ; this . expiresInDefault = this . configurationService . JWT . AccessTokenTtl ; this . jwtOptions = { expiresIn : this . expiresInDefault } ; this . jwtKey = this . configurationService . JWT . Key ; this . refreshTokenTtl = this . configurationService . JWT . RefreshTokenTtl ; this . mapperService . createMap ( LoginResponse . name , LoginResponseVm . name ) ; } async getAccessTokenFromRefreshToken ( refreshToken , oldAccessToken , clientId , ipAddress ) { try { const token = await this . findOne ( { value : refreshToken } ) ; const currentDate = new Date ( ) ; if ( ! token ) { throw new NotFoundError ( 'Refresh token not found' ) ; } if ( token . expiresAt < currentDate ) { throw new Error ( 'Refresh token expired' ) ; } const oldPayload = await this . validateToken ( oldAccessToken , true ) ; const payload = { sub : oldPayload . sub , } ; const accessToken = await this . createAccessToken ( payload ) ; await this . deleteById ( token . id ) ; accessToken . refreshToken = await this . createRefreshToken ( { userId : oldPayload . sub , clientId , ipAddress , } ) ; return this . mapperService . map < LoginResponseVm > ( accessToken , LoginResponseVm . name , LoginResponse . name ) ; } catch ( error ) { this . logger . error ( error ) ; throw error ; } } async createAccessToken ( payload , expires = this . expiresInDefault ) { const options = this . jwtOptions ; expires > 0 ? ( options . expiresIn = expires ) : delete options . expiresIn ; options . jwtid = uuid ( ) ; const signedPayload = sign ( payload , this . jwtKey , options ) ; const token = { accessToken : signedPayload , expiresIn : expires , } ; return this . mapperService . map < LoginResponseVm > ( token , LoginResponseVm . name , LoginResponse . name ) ; } async createRefreshToken ( tokenContent ) { const { userId , clientId , ipAddress } = tokenContent ; const token = new RefreshToken . model ( ) ; const refreshToken = randomBytes ( 64 ) . toString ( 'hex' ) ; token . userId = Types . ObjectId ( userId ) ; token . value = refreshToken ; token . clientId = clientId ; token . ipAddress = ipAddress ; token . expiresAt = moment ( ) . add ( this . refreshTokenTtl , 'd' ) . toDate ( ) ; await this . create ( token ) ; return refreshToken ; } param userId async deleteRefreshTokenForUser ( userId ) { await this . delete ( { userId : Types . ObjectId ( userId ) } ) ; await this . revokeTokenForUser ( userId ) ; } param userId param value async deleteRefreshToken ( userId , value ) { await this . delete ( { value } ) ; await this . revokeTokenForUser ( userId ) ; } async decodeAndValidateJWT ( token ) { if ( token ) { try { const payload = await this . validateToken ( token ) ; return await this . validatePayload ( payload ) ; } catch ( error ) { return null ; } } } async validatePayload ( payload ) { const tokenBlacklisted = await this . isBlackListed ( payload . sub , payload . exp ) ; if ( ! tokenBlacklisted ) { return { id : payload . sub , } ; } return null ; } private async validateToken ( token , ignoreExpiration = false ) { return verify ( token , this . configurationService . JWT . Key , { ignoreExpiration , } ) as JwtPayload ; } private async isBlackListed ( id , expire ) { return this . usersExpired [ id ] && expire < this . usersExpired [ id ] ; } private async revokeTokenForUser ( userId ) { this . usersExpired [ userId ] = moment ( ) . add ( this . expiresInDefault , 's' ) . unix ( ) ; } }