import { ClassTransformOptions } from 'class-transformer' ; import { ValidatorOptions } from 'class-validator' ; import { encode } from 'querystring' ; import { Controller , Get , Param , Query , Redirect , Render , Req , Res , UsePipes , ValidationPipe , } from '@nestjs/common' ; import { AppConfig } from '@fc/app' ; import { validateDto } from '@fc/common' ; import { ConfigService } from '@fc/config' ; import { CoreMissingIdentityException , CoreRoutes , CoreService , Interaction , } from '@fc/core' ; import { IdentityProviderAdapterMongoService } from '@fc/identity-provider-adapter-mongo' ; import { LoggerLevelNames , LoggerService } from '@fc/logger' ; import { MinistriesService } from '@fc/ministries' ; 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 , SessionBadFormatException , SessionCsrfService , SessionNotFoundException , SessionService , } from '@fc/session' ; import { Core , OidcIdentityDto } from '../dto' ; import { CoreFcaInvalidIdentityException } from '../exceptions' ; import { CoreFcaService } from '../services' ; @ Controller ( ) export class CoreFcaController { constructor ( private readonly logger , private readonly oidcProvider , private readonly identityProvider , private readonly serviceProvider , private readonly ministries , private readonly core , private readonly config , private readonly oidcClient , 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 . FCA_FRONT_HISTORY_BACK_URL ) async getFrontHistoryBackURL ( @ Req ( ) req , @ Res ( ) res , @ Session ( 'OidcClient' ) sessionOidc ) { const { spName } = ( await sessionOidc . get ( ) ) || { } ; if ( ! spName ) { throw new SessionBadFormatException ( ) ; } const { params } = await this . oidcProvider . getInteraction ( req , res ) ; const { redirect_uri : redirectURI , state } = params ; const redirectURIQuery = { state , error_description : 'User auth aborted' , error : 'access_denied' , } ; const jsonResponse = { spName , redirectURIQuery , redirectURI , } ; return res . json ( jsonResponse ) ; } @ Get ( CoreRoutes . FCA_FRONT_DATAS ) async getFrontData ( @ Req ( ) req , @ Res ( ) res , todo @ Session ( 'OidcClient' ) sessionOidc ) { const { spName } = ( await sessionOidc . get ( ) ) || { } ; if ( ! spName ) { throw new SessionBadFormatException ( ) ; } const { params } = await this . oidcProvider . getInteraction ( req , res ) ; const { scope } = this . config . get < OidcClientConfig > ( 'OidcClient' ) ; const csrfToken = this . csrfService . get ( ) ; await this . csrfService . save ( sessionOidc , csrfToken ) ; const { acr_values , client_id , redirect_uri , response_type , } = params ; const redirectToIdentityProviderInputs = { acr_values , csrfToken , redirect_uri , response_type , scope , } ; const ministries = await this . ministries . getList ( ) ; const { idpFilterExclude , idpFilterList } = await this . serviceProvider . getById ( client_id ) ; const identityProvidersList = await this . identityProvider . getFilteredList ( { blacklist : idpFilterExclude , idpList : idpFilterList , } ) ; const identityProviders = identityProvidersList . map ( ( { active , display , title , uid } ) => ( { active , display , name : title , uid , } ) ) ; const jsonResponse = { identityProviders , ministries , redirectToIdentityProviderInputs , redirectURL : '/api/v2/redirect-to-idp' , serviceProviderName : spName , } ; this . logger . trace ( { method : 'GET' , name : 'CoreRoutes.FCA_FRONT_DATAS' , response : jsonResponse , route : CoreRoutes . FCA_FRONT_DATAS , } ) ; return res . json ( jsonResponse ) ; } @ Get ( CoreRoutes . INTERACTION ) @ Render ( 'interaction' ) async getInteraction ( @ Req ( ) req , @ Res ( ) res , @ Session ( 'OidcClient' ) sessionOidc ) { const session = await sessionOidc . get ( ) ; if ( ! session ) { this . logger . trace ( { route : CoreRoutes . INTERACTION } , LoggerLevelNames . WARN ) ; throw new SessionNotFoundException ( 'OidcClient' ) ; } this . logger . trace ( { method : 'GET' , name : 'CoreRoutes.INTERACTION' , route : CoreRoutes . INTERACTION , session , } ) ; const { params } = await this . oidcProvider . getInteraction ( req , res ) ; const { acr_values : acrValues } = 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 ; } return { } ; } @ Get ( CoreRoutes . INTERACTION_VERIFY ) @ UsePipes ( new ValidationPipe ( { whitelist : true } ) ) async getVerify ( @ Res ( ) res , @ Param ( ) _params , todo author Hugues
     *  date ticket @ Session ( 'OidcClient' ) sessionOidc ) { await this . core . verify ( sessionOidc ) ; const { urlPrefix } = this . config . get < AppConfig > ( 'App' ) ; const url = `${ urlPrefix }/login` ; this . logger . trace ( { method : 'GET' , name : 'CoreRoutes.INTERACTION_VERIFY' , redirect : url , route : CoreRoutes . INTERACTION_VERIFY , } ) ; res . redirect ( url ) ; } TODO see https @ Get ( CoreRoutes . INTERACTION_LOGIN ) async getLogin ( @ Req ( ) req , @ Res ( ) res , todo author Hugues
     *  date ticket @ Session ( 'OidcClient' ) sessionOidc ) { const { spIdentity } = await sessionOidc . get ( ) ; if ( ! spIdentity ) { this . logger . trace ( { route : CoreRoutes . INTERACTION_LOGIN } , LoggerLevelNames . WARN ) ; throw new CoreMissingIdentityException ( ) ; } /**
         * 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 ( { method : 'GET' , name : 'CoreRoutes.INTERACTION_LOGIN' , route : CoreRoutes . INTERACTION_LOGIN , spIdentity , } ) ; const session = await sessionOidc . get ( ) ; 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 response = { statusCode : 302 , url : `${ urlPrefix }${ OidcClientRoutes . OIDC_CALLBACK }?${ encode ( query ) }` , } ; this . logger . trace ( { method : 'GET' , name : 'OidcClientRoutes.OIDC_CALLBACK_LEGACY' , providerUid : params . providerUid , } ) ; return response ; } TODO see https @ Get ( OidcClientRoutes . OIDC_CALLBACK ) @ UsePipes ( new ValidationPipe ( { whitelist : true } ) ) 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 ) ; todo author Arnaud & Hugues
         *   date ticket const tokenParams = { state : idpState , nonce : idpNonce , } ; const extraParams = { sp_id : spId , } ; const { accessToken , acr , amr } = await this . oidcClient . getTokenFromProvider ( idpId , tokenParams , req , extraParams ) ; 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 , } ; await 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 , identityExchange , } ) ; res . redirect ( url ) ; } private async validateIdentity ( idpId , identity ) { const validatorOptions = { forbidNonWhitelisted : true , forbidUnknownValues : true , whitelist : true , } ; const transformOptions = { excludeExtraneousValues : true , } ; const errors = await validateDto ( identity , OidcIdentityDto , validatorOptions , transformOptions ) ; if ( errors . length ) { this . logger . trace ( { errors } , LoggerLevelNames . WARN ) ; throw new CoreFcaInvalidIdentityException ( ) ; } this . logger . trace ( { validate : { identity , idpId } } ) ; return true ; } }