import { Injectable , Type } from '@nestjs/common' ; import { EventBus } from '@nestjs/cqrs' ; import { InjectModel } from '@nestjs/mongoose' ; import { asyncFilter , validateDto } from '@fc/common' ; import { ConfigService , validationOptions } from '@fc/config' ; import { CryptographyService } from '@fc/cryptography' ; import { LoggerService } from '@fc/logger' ; import { ClientMetadata , IdentityProviderMetadata , IssuerMetadata , } from '@fc/oidc' ; import { IIdentityProviderAdapter } from '@fc/oidc-client' ; import { DiscoveryIdpAdapterMongoDTO , IdentityProviderAdapterMongoConfig , IdentityProviderAdapterMongoDTO as NoDiscoveryIdoAdapterMongoDTO , } from './dto' ; import { IdentityProviderUpdateEvent } from './events' ; import { FilteringOptions } from './interfaces' ; import { IdentityProvider } from './schemas' ; const CLIENT_METADATA = [ 'client_id' , 'client_secret' , 'post_logout_redirect_uris' , 'redirect_uris' , 'response_types' , 'id_token_signed_response_alg' , 'token_endpoint_auth_method' , 'revocation_endpoint_auth_method' , 'id_token_encrypted_response_alg' , 'id_token_encrypted_response_enc' , 'userinfo_encrypted_response_alg' , 'userinfo_encrypted_response_enc' , 'userinfo_signed_response_alg' , ] ; const ISSUER_METADATA = [ 'issuer' , 'token_endpoint' , 'authorization_endpoint' , 'jwks_uri' , 'userinfo_endpoint' , 'end_session_endpoint' , ] ; @ Injectable ( ) export class IdentityProviderAdapterMongoService implements IIdentityProviderAdapter { private listCache : IdentityProviderMetadata [ ] ; constructor ( @ InjectModel ( 'IdentityProvider' ) private readonly identityProviderModel , private readonly crypto , private readonly config , private readonly eventBus , private readonly logger ) { this . logger . setContext ( this . constructor . name ) ; } async onModuleInit ( ) { this . initOperationTypeWatcher ( ) ; this . logger . debug ( 'Initializing identity-provider' ) ; await this . getList ( ) ; } private initOperationTypeWatcher ( ) { const watch = this . identityProviderModel . watch ( ) ; this . logger . debug ( `Provider's database OperationType watcher initialization.` ) ; watch . on ( 'change' , this . operationTypeWatcher . bind ( this ) ) ; } param { object } stream returns { void } private operationTypeWatcher ( stream ) { const operationTypes = [ 'insert' , 'update' , 'delete' , 'rename' , 'replace' ] ; const isListenedOperation = operationTypes . includes ( stream . operationType ) ; if ( isListenedOperation ) { this . eventBus . publish ( new IdentityProviderUpdateEvent ( ) ) ; } } private async findAllIdentityProvider ( ) { const rawResult = await this . identityProviderModel . find ( { } , { _id : false , active : true , authzURL : true , clientID : true , client_secret : true , discovery : true , discoveryUrl : true , display : true , eidas : true , featureHandlers : true , id_token_encrypted_response_alg : true , id_token_encrypted_response_enc : true , id_token_signed_response_alg : true , image : true , issuer : true , jwksURL : true , name : true , post_logout_redirect_uris : true , redirect_uris : true , response_types : true , revocation_endpoint_auth_method : true , title : true , tokenURL : true , token_endpoint_auth_method : true , uid : true , url : true , userInfoURL : true , userinfo_encrypted_response_alg : true , userinfo_encrypted_response_enc : true , userinfo_signed_response_alg : true , } ) . lean ( ) ; const identityProviders = await asyncFilter < IdentityProviderMetadata [ ] > ( rawResult , async ( doc ) => { const dto = this . getIdentityProviderDTO ( doc . discovery ) ; const errors = await validateDto ( doc , dto , validationOptions ) ; if ( errors . length > 0 ) { this . logger . warn ( `"${ doc . uid }" was excluded from the result at DTO validation.` ) ; this . logger . trace ( { errors } ) ; } return errors . length === 0 ; } ) ; this . logger . trace ( { identityProviders } ) ; return identityProviders ; } param refreshCache async getList ( refreshCache ? ) { if ( refreshCache || ! this . listCache ) { this . logger . debug ( 'Refresh cache from DB' ) ; const list = await this . findAllIdentityProvider ( ) ; this . listCache = list . map ( this . legacyToOpenIdPropertyName . bind ( this ) ) ; this . logger . trace ( { step : 'REFRESH' , list , listCache : this . listCache } ) ; } else { this . logger . trace ( { list : this . listCache , step : 'CACHE' } ) ; } return this . listCache ; } param idpList param isBlackListed async getFilteredList ( options ) { const providers = await this . getList ( ) ; const { blacklist , idpList } = options ; const filteredProviders = providers . filter ( ( { uid } ) => { const idpFound = idpList . includes ( uid ) ; return blacklist ? ! idpFound : idpFound ; } ) ; this . logger . trace ( { blacklist , filteredProviders } ) ; return filteredProviders ; } async getById ( id , refreshCache = false ) { const providers = await this . getList ( refreshCache ) ; const provider = providers . find ( ( { uid } ) => uid === id ) ; this . logger . trace ( { provider } ) ; return provider ; } private legacyToOpenIdPropertyName ( source ) { const mapping = { authzURL : 'authorization_endpoint' , clientID : 'client_id' , endSessionURL : 'end_session_endpoint' , jwksURL : 'jwks_uri' , tokenURL : 'token_endpoint' , url : 'issuer' , userInfoURL : 'userinfo_endpoint' , eidas : 'maxAuthorizedAcr' , } ; const result = { ... source , } ; Object . entries ( mapping ) . forEach ( ( [ legacyName , oidcName ] ) => { result [ oidcName ] = source [ legacyName ] ; Reflect . deleteProperty ( result , legacyName ) ; } ) ; result . client_secret = this . decryptClientSecret ( source . client_secret ) ; result . maxAuthorizedAcr = `eidas${ source [ 'eidas' ] }` ; TODO see https return this . toPanvaFormat ( result ) ; } private toPanvaFormat ( result ) { const panvaFormatted = { client : { } as ClientMetadata , issuer : { } as IssuerMetadata , } ; Object . entries ( result ) . forEach ( ( [ key , value ] ) => { if ( CLIENT_METADATA . includes ( key ) ) { panvaFormatted . client [ key ] = value ; } else if ( ISSUER_METADATA . includes ( key ) ) { panvaFormatted . issuer [ key ] = value ; } else { panvaFormatted [ key ] = value ; } } ) ; return panvaFormatted as IdentityProviderMetadata ; } param clientSecret private decryptClientSecret ( clientSecret ) { const { clientSecretEncryptKey } = this . config . get < IdentityProviderAdapterMongoConfig > ( 'IdentityProviderAdapterMongo' ) ; return this . crypto . decrypt ( clientSecretEncryptKey , Buffer . from ( clientSecret , 'base64' ) ) ; } private getIdentityProviderDTO ( discovery ) { return discovery ? DiscoveryIdpAdapterMongoDTO : NoDiscoveryIdoAdapterMongoDTO ; } }