import { encode } from 'querystring' ; import { Body , Controller , Get , Param , Post , Query , Redirect , Render , Req , Res , Type , UsePipes , ValidationPipe , } from '@nestjs/common' ; import { AppConfig } from '@fc/app' ; import { ConfigService } from '@fc/config' ; import { CoreMissingIdentityException , CoreRoutes , CoreService , CsrfToken , Interaction , } from '@fc/core' ; import { IdentityProviderAdapterMongoService } from '@fc/identity-provider-adapter-mongo' ; import { LoggerLevelNames , LoggerService } from '@fc/logger' ; import { NotificationsService } from '@fc/notifications' ; import { OidcSession } from '@fc/oidc' ; import { GetOidcCallback , OidcClientConfig , OidcClientRoutes , OidcClientService , OidcClientSession , } from '@fc/oidc-client' ; import { OidcProviderConfig , OidcProviderService } from '@fc/oidc-provider' ; import { ServiceProviderAdapterMongoService } from '@fc/service-provider-adapter-mongo' ; import { ISessionService , Session , SessionCsrfService , SessionInvalidCsrfConsentException , SessionNotFoundException , SessionService , } from '@fc/session' ; import { TrackingService } from '@fc/tracking' ; import { Core , OidcIdentityDto } from '../dto' ; import { ProcessCore } from '../enums' ; import { CoreFcpDatatransferConsentIdentityEvent , CoreFcpDatatransferInformationAnonymousEvent , CoreFcpDatatransferInformationIdentityEvent , } from '../events' ; import { CoreFcpInvalidEventClassException , CoreFcpInvalidIdentityException , } from '../exceptions' ; import { IIdentityCheckFeatureHandler } from '../interfaces' ; import { CoreFcpService } from '../services' ; export const datatransferEventsMap = { 'CONSENT:IDENTITY' : CoreFcpDatatransferConsentIdentityEvent , 'INFORMATION:ANONYMOUS' : CoreFcpDatatransferInformationAnonymousEvent , 'INFORMATION:IDENTITY' : CoreFcpDatatransferInformationIdentityEvent , } ; @ Controller ( ) export class CoreFcpController { constructor ( private readonly logger , private readonly oidcProvider , private readonly identityProvider , private readonly serviceProvider , private readonly core , private readonly config , private readonly notifications , private readonly oidcClient , private readonly tracking , private readonly sessionService , private readonly csrfService , private readonly coreService ) { this . logger . setContext ( this . constructor . name ) ; } @ Get ( CoreRoutes . DEFAULT ) getDefault ( @ Res ( ) res ) { const { defaultRedirectUri } = this . config . get < Core > ( 'Core' ) ; this . logger . trace ( { method : 'GET' , name : 'CoreRoutes.DEFAULT' , redirect : defaultRedirectUri , route : CoreRoutes . DEFAULT , } ) ; res . redirect ( 301 , defaultRedirectUri ) ; } @ Get ( CoreRoutes . INTERACTION ) @ UsePipes ( new ValidationPipe ( { whitelist : true } ) ) async getInteraction ( @ Req ( ) req , @ Res ( ) res , @ Param ( ) _params , todo author Hugues
     *  date ticket @ Session ( 'OidcClient' ) sessionOidc ) { const session = await sessionOidc . get ( ) ; if ( ! session ) { throw new SessionNotFoundException ( 'OidcClient' ) ; } const { spName } = session ; const { params , uid } = await this . oidcProvider . getInteraction ( req , res ) ; const { scope } = this . config . get < OidcClientConfig > ( 'OidcClient' ) ; const { acr_values : acrValues , client_id : clientId } = params ; const { configuration : { acrValues : allowedAcrValues } , } = this . config . get < OidcProviderConfig > ( 'OidcProvider' ) ; const rejected = await this . coreService . rejectInvalidAcr ( acrValues , allowedAcrValues , { req , res } ) ; if ( rejected ) { this . logger . trace ( { acrValues , allowedAcrValues , rejected } , LoggerLevelNames . WARN ) ; return ; } const { idpFilterExclude , idpFilterList } = await this . serviceProvider . getById ( clientId ) ; const providers = await this . identityProvider . getFilteredList ( { blacklist : idpFilterExclude , idpList : idpFilterList , } ) ; const authorizedProviders = providers . filter ( ( { maxAuthorizedAcr } ) => this . coreService . isAcrValid ( maxAuthorizedAcr , acrValues ) ) ; const csrfToken = this . csrfService . get ( ) ; await this . csrfService . save ( sessionOidc , csrfToken ) ; const notifications = await this . notifications . getNotifications ( ) ; const response = { csrfToken , notifications , params , providers : authorizedProviders , scope , spName , uid , } ; this . logger . trace ( { method : 'GET' , name : 'CoreRoutes.INTERACTION' , response , route : CoreRoutes . INTERACTION , } ) ; return res . render ( 'interaction' , response ) ; } @ Get ( CoreRoutes . INTERACTION_VERIFY ) @ UsePipes ( new ValidationPipe ( { whitelist : true } ) ) async getVerify ( @ Req ( ) req , @ Res ( ) res , @ Param ( ) _params , todo author Hugues
     *  date ticket @ Session ( 'OidcClient' ) sessionOidc ) { const { interactionId } = await sessionOidc . get ( ) ; await this . core . verify ( sessionOidc , req ) ; const { urlPrefix } = this . config . get < AppConfig > ( 'App' ) ; const url = `${ urlPrefix }/interaction/${ interactionId }/consent` ; this . logger . trace ( { method : 'GET' , name : 'CoreRoutes.INTERACTION_VERIFY' , redirect : url , route : CoreRoutes . INTERACTION_VERIFY , } ) ; res . redirect ( url ) ; } @ Get ( CoreRoutes . INTERACTION_CONSENT ) @ UsePipes ( new ValidationPipe ( { whitelist : true } ) ) @ Render ( 'consent' ) async getConsent ( @ Req ( ) req , @ Res ( ) res , @ Param ( ) _params , todo author Hugues
     *  date ticket @ Session ( 'OidcClient' ) sessionOidc ) { const { interactionId , spId , spIdentity : identity , spName , } = await sessionOidc . get ( ) ; const interaction = await this . oidcProvider . getInteraction ( req , res ) ; const scopes = await this . core . getScopesForInteraction ( interaction ) ; const claims = await this . core . getClaimsLabelsForInteraction ( interaction ) ; const consentRequired = await this . core . isConsentRequired ( spId ) ; const csrfToken = await this . csrfService . get ( ) ; await this . csrfService . save ( sessionOidc , csrfToken ) ; const response = { claims , consentRequired , csrfToken , identity , interactionId , scopes , spName , } ; this . logger . trace ( { method : 'GET' , name : 'CoreRoutes.INTERACTION_CONSENT' , response , route : CoreRoutes . INTERACTION_CONSENT , } ) ; return response ; } private getEventClass ( scopes , consentRequired ) { const dataType = scopes . every ( ( scope ) => scope === 'openid' ) ? 'ANONYMOUS' : 'IDENTITY' ; const consentOrInfo = consentRequired ? 'CONSENT' : 'INFORMATION' ; const classMapKey = `${ consentOrInfo }:${ dataType }` ; if ( ! ( classMapKey in datatransferEventsMap ) ) { throw new CoreFcpInvalidEventClassException ( ) ; } return datatransferEventsMap [ classMapKey ] ; } private async trackDatatransfer ( trackingContext , interaction , spId ) { const scopes = await this . core . getScopesForInteraction ( interaction ) ; const consentRequired = await this . core . isConsentRequired ( spId ) ; const claims = await this . core . getClaimsForInteraction ( interaction ) ; const eventClass = this . getEventClass ( scopes , consentRequired ) ; const context = { ... trackingContext , claims , } ; this . tracking . track ( eventClass , context ) ; } @ Post ( CoreRoutes . INTERACTION_LOGIN ) @ UsePipes ( new ValidationPipe ( { whitelist : true } ) ) async getLogin ( @ Req ( ) req , @ Res ( ) res , @ Body ( ) body , todo author Hugues
     *  date ticket @ Session ( 'OidcClient' ) sessionOidc ) { const { _csrf : csrfToken } = body ; const session = await sessionOidc . get ( ) ; if ( ! session ) { throw new SessionNotFoundException ( 'OidcClient' ) ; } const { spId , spIdentity } = session ; try { await this . csrfService . validate ( sessionOidc , csrfToken ) ; } catch ( error ) { this . logger . trace ( { error } , LoggerLevelNames . WARN ) ; throw new SessionInvalidCsrfConsentException ( error ) ; } if ( ! spIdentity ) { this . logger . trace ( { spIdentity } , LoggerLevelNames . WARN ) ; throw new CoreMissingIdentityException ( ) ; } this . logger . trace ( { csrfToken , spIdentity } ) ; const interaction = await this . oidcProvider . getInteraction ( req , res ) ; const trackingContext = req ; await this . trackDatatransfer ( trackingContext , interaction , spId ) ; await this . core . sendAuthenticationMail ( session ) ; /**
         * We need to set an alias with the sub since later (findAccount) we do not have access
         * to the sessionId, nor the interactionId.
         */ await this . sessionService . setAlias ( spIdentity . sub , req . sessionId ) ; this . logger . trace ( { data : { req , res , session } , method : 'POST' , name : 'CoreRoutes.INTERACTION_LOGIN' , route : CoreRoutes . INTERACTION_LOGIN , } ) ; return this . oidcProvider . finishInteraction ( req , res , session ) ; } @ Get ( OidcClientRoutes . OIDC_CALLBACK_LEGACY ) @ UsePipes ( new ValidationPipe ( { whitelist : true } ) ) @ Redirect ( ) async getLegacyOidcCallback ( @ Query ( ) query , @ Param ( ) params ) { const { urlPrefix } = this . config . get < AppConfig > ( 'App' ) ; const queryParams = encode ( query ) ; const response = { statusCode : 302 , url : `${ urlPrefix }${ OidcClientRoutes . OIDC_CALLBACK }?${ queryParams }` , } ; this . logger . trace ( { method : 'GET' , name : 'OidcClientRoutes.OIDC_CALLBACK_LEGACY' , providerUid : params . providerUid , } ) ; return response ; } TODO see https @ Get ( OidcClientRoutes . OIDC_CALLBACK ) async getOidcCallback ( @ Req ( ) req , @ Res ( ) res , todo author Hugues
     *  date ticket @ Session ( 'OidcClient' ) sessionOidc ) { const session = await sessionOidc . get ( ) ; if ( ! session ) { throw new SessionNotFoundException ( 'OidcClient' ) ; } const { idpId , idpNonce , idpState , interactionId , spId } = session ; await this . oidcClient . utils . checkIdpBlacklisted ( spId , idpId ) ; const tokenParams = { state : idpState , nonce : idpNonce , } ; const { accessToken , acr , amr } = await this . oidcClient . getTokenFromProvider ( idpId , tokenParams , req ) ; const userInfoParams = { accessToken , idpId , } ; const identity = await this . oidcClient . getUserInfosFromProvider ( userInfoParams , req ) ; await this . validateIdentity ( idpId , identity ) ; const identityExchange = { amr , idpAccessToken : accessToken , idpAcr : acr , idpIdentity : identity , } ; sessionOidc . set ( { ... identityExchange } ) ; const { urlPrefix } = this . config . get < AppConfig > ( 'App' ) ; const url = `${ urlPrefix }/interaction/${ interactionId }/verify` ; this . logger . trace ( { method : 'GET' , name : 'OidcClientRoutes.OIDC_CALLBACK' , redirect : url , route : OidcClientRoutes . OIDC_CALLBACK , } ) ; res . redirect ( url ) ; } private async validateIdentity ( idpId , identity ) { const identityCheckHandler = await this . core . getFeature < IIdentityCheckFeatureHandler > ( idpId , ProcessCore . ID_CHECK ) ; const errors = await identityCheckHandler . handle ( identity ) ; if ( errors . length ) { this . logger . trace ( { errors } , LoggerLevelNames . WARN ) ; throw new CoreFcpInvalidIdentityException ( ) ; } } }