import { isEmpty } from 'lodash' ; import { Adapter , AdapterConstructor } from 'oidc-provider' ; import { Inject } from '@nestjs/common' ; import { LoggerLevelNames , LoggerService } from '@fc/logger' ; import { IServiceProviderAdapter } from '@fc/oidc' ; import { Redis , REDIS_CONNECTION_TOKEN } from '@fc/redis' ; import { OidcProviderParseRedisResponseException , OidcProviderStringifyPayloadForRedisException , } from '../exceptions' ; /** Circular reference for type checking */ import { OidcProviderService } from '../oidc-provider.service' ; const consumable = new Set ( [ 'AuthorizationCode' , 'RefreshToken' , 'DeviceCode' ] ) ; /**
 * Prefix everything in redis.
 */ export const OIDC_PROVIDER_REDIS_PREFIX = 'OIDC-P' ; see OidcProviderService . getConfig see https see https export class OidcProviderRedisAdapter implements Adapter { constructor ( /**
     * Bound arguments
     */ private readonly logger , @ Inject ( REDIS_CONNECTION_TOKEN ) private readonly redis , private readonly serviceProvider , /**
     * Instantiation time argument,
     * must remain the last one.
     */ private readonly contextName ) { this . contextName = contextName ; } /**
     * Method to get a bounded class
     */ static getConstructorWithDI ( oidcProviderService , serviceProviderService ) { /**
         * Bind services we want to inject from our regular NestJs service.
         *
         * NB: Thoses services are privates but we need them to keep NestJs context.
         */ const boundConstructor = OidcProviderRedisAdapter . bind ( null , oidcProviderService . logger , oidcProviderService . redis , serviceProviderService ) ; see https boundConstructor . prototype = OidcProviderRedisAdapter . prototype ; return boundConstructor ; } private grantKeyFor ( id ) { if ( ! id ) { return null ; } const key = `${ OIDC_PROVIDER_REDIS_PREFIX }:grant:${ id }` ; return key ; } private userCodeKeyFor ( userCode ) { if ( ! userCode ) { return null ; } const key = `${ OIDC_PROVIDER_REDIS_PREFIX }:userCode:${ userCode }` ; return key ; } private uidKeyFor ( uid ) { if ( ! uid ) { return null ; } const key = `${ OIDC_PROVIDER_REDIS_PREFIX }:uid:${ uid }` ; return key ; } private key ( id ) { return `${ OIDC_PROVIDER_REDIS_PREFIX }:${ this . contextName }:${ id }` ; } private parsedPayload ( payload ) { try { return JSON . parse ( payload ) ; } catch ( error ) { throw new OidcProviderParseRedisResponseException ( ) ; } } private saveKey ( multi , key , data ) { let dataFormated ; try { dataFormated = JSON . stringify ( data ) ; } catch ( error ) { see OidcProviderService . throwError throw new OidcProviderStringifyPayloadForRedisException ( ) ; } const hasContext = consumable . has ( this . contextName ) ; const store = hasContext ? { payload : dataFormated } : dataFormated ; const command = hasContext ? 'hmset' : 'set' ; ( multi [ command ] as Function ) ( key , store ) ; } private async saveGrantId ( multi , grantId , key , expiresIn ) { const grantKey = this . grantKeyFor ( grantId ) ; if ( ! grantKey ) { return ; } multi . rpush ( grantKey , key ) ; const ttl = await this . redis . ttl ( grantKey ) ; if ( expiresIn > ttl ) { multi . expire ( grantKey , expiresIn ) ; } } private addSetAndExpireOnMulti ( key , id , expiresIn , multi ) { if ( key ) { multi . set ( key , id ) ; multi . expire ( key , expiresIn ) ; } } async upsert ( id , payload , expiresIn ) { this . logger . debug ( 'Upsert' ) ; const key = this . key ( id ) ; this . logger . trace ( { upsert : { id , payload , expiresIn , key } } ) ; if ( expiresIn && isNaN ( expiresIn ) ) { throw new TypeError ( `expiresIn MUST be a number, <${ typeof expiresIn }> given.` ) ; } const multi = this . redis . multi ( ) ; this . saveKey ( multi , key , payload ) ; if ( expiresIn ) { multi . expire ( key , expiresIn ) ; } const { grantId , userCode , uid } = payload ; await this . saveGrantId ( multi , grantId , key , expiresIn ) ; const userCodeKey = this . userCodeKeyFor ( userCode ) ; const uidKey = this . uidKeyFor ( uid ) ; this . addSetAndExpireOnMulti ( userCodeKey , id , expiresIn , multi ) ; this . addSetAndExpireOnMulti ( uidKey , id , expiresIn , multi ) ; await multi . exec ( ) ; } private async findServiceProvider ( spId ) { const sp = await this . serviceProvider . getById ( spId ) ; this . logger . trace ( { spId , sp } ) ; return sp ; } private async findInRedis ( id ) { const key = this . key ( id ) ; const command = consumable . has ( this . contextName ) ? 'hgetall' : 'get' ; const data = await this . redis [ command ] ( key ) ; if ( isEmpty ( data ) ) { this . logger . trace ( { find : { id , key , data } } , LoggerLevelNames . WARN ) ; return void 0 ; } this . logger . trace ( { find : { id , key , data } } ) ; const wrappedData = typeof data === 'string' ? { payload : data } : data ; const { payload , ... rest } = wrappedData ; const parsedPayload = this . parsedPayload ( payload ) ; return { ... rest , ... parsedPayload , } ; } async find ( id ) { this . logger . debug ( 'Find' ) ; if ( this . contextName === 'Client' ) { return await this . findServiceProvider ( id ) ; } return await this . findInRedis ( id ) ; } async findByUid ( uid ) { const id = await this . redis . get ( this . uidKeyFor ( uid ) ) ; return this . find ( id ) ; } async findByUserCode ( userCode ) { const id = await this . redis . get ( this . userCodeKeyFor ( userCode ) ) ; return this . find ( id ) ; } async destroy ( id ) { this . logger . debug ( 'Destroy' ) ; this . logger . trace ( { destroy : { id , key : this . key ( id ) } } ) ; const key = this . key ( id ) ; await this . redis . del ( key ) ; } async revokeByGrantId ( grantId ) { this . logger . debug ( 'RevokeByGrantId' ) ; this . logger . trace ( { revoke : { grantId , grantKey : this . grantKeyFor ( grantId ) , } , } ) ; const multi = this . redis . multi ( ) ; const tokens = await this . redis . lrange ( this . grantKeyFor ( grantId ) , 0 , - 1 ) ; tokens . forEach ( ( token ) => multi . del ( token ) ) ; multi . del ( this . grantKeyFor ( grantId ) ) ; await multi . exec ( ) ; } async consume ( id ) { this . logger . debug ( 'Consume' ) ; this . logger . trace ( { consume : { id , key : this . key ( id ) } } ) ; await this . redis . hset ( this . key ( id ) , 'consumed' , Math . floor ( Date . now ( ) / 1000 ) ) ; } }