/**
 * This controller handles the call from the IDP after a successful
 * authentication. In the request headers there are all the attributes sent from
 * the IDP.
 */ import * as express from "express" ; import { fromNullable as fromNullableE , isLeft , left , right , toError } from "fp-ts/lib/Either" ; import { fromNullable } from "fp-ts/lib/Option" ; import { IResponseErrorForbiddenNotAuthorized , IResponseErrorInternal , IResponseErrorValidation , IResponsePermanentRedirect , IResponseSuccessJson , ResponseErrorForbiddenNotAuthorized , ResponseErrorInternal , ResponseErrorValidation , ResponsePermanentRedirect , ResponseSuccessJson } from "italia-ts-commons/lib/responses" ; import { UrlFromString } from "italia-ts-commons/lib/url" ; import { NewProfile } from "generated/io-api/NewProfile" ; import { errorsToReadableMessages } from "italia-ts-commons/lib/reporters" ; import { FiscalCode , NonEmptyString } from "italia-ts-commons/lib/strings" ; import { parse } from "date-fns" ; import * as appInsights from "applicationinsights" ; import UsersLoginLogService from "../services/usersLoginLogService" ; import { isOlderThan } from "../utils/date" ; import { SuccessResponse } from "../../generated/auth/SuccessResponse" ; import { UserIdentity } from "../../generated/auth/UserIdentity" ; import { AccessToken } from "../../generated/public/AccessToken" ; import { ClientErrorRedirectionUrlParams , clientProfileRedirectionUrl } from "../config" ; import { ISessionStorage } from "../services/ISessionStorage" ; import NotificationService from "../services/notificationService" ; import ProfileService from "../services/profileService" ; import TokenService from "../services/tokenService" ; import { BPDToken , MyPortalToken , SessionToken , WalletToken , ZendeskToken } from "../types/token" ; import { exactUserIdentityDecode , toAppUser , validateSpidUser , withUserFromRequest } from "../types/user" ; import { log } from "../utils/logger" ; import { withCatchAsInternalError } from "../utils/responses" ; export const SESSION_TOKEN_LENGTH_BYTES = 48 ; const SESSION_ID_LENGTH_BYTES = 32 ; export const AGE_LIMIT_ERROR_MESSAGE = "The age of the user is less than 14" ; export const AGE_LIMIT_ERROR_CODE = 1001 ; export const AGE_LIMIT = 14 ; export default class AuthenticationController { constructor ( private readonly sessionStorage , private readonly tokenService , private readonly getClientProfileRedirectionUrl , private readonly getClientErrorRedirectionUrl , private readonly profileService , private readonly notificationService , private readonly usersLoginLogService , private readonly testLoginFiscalCodes , private readonly hasUserAgeLimitEnabled , private readonly appInsightsTelemetryClient ? ) { } /**
     * The Assertion consumer service.
     */ public async acs ( userPayload ) { const errorOrSpidUser = validateSpidUser ( userPayload ) ; if ( isLeft ( errorOrSpidUser ) ) { log . error ( "acs: error validating the SPID user [%O] [%s]" , userPayload , errorOrSpidUser . value ) ; return ResponseErrorValidation ( "Bad request" , errorOrSpidUser . value ) ; } const spidUser = errorOrSpidUser . value ; if ( this . hasUserAgeLimitEnabled && ! isOlderThan ( AGE_LIMIT ) ( parse ( spidUser . dateOfBirth ) , new Date ( ) ) ) { const redirectionUrl = this . getClientErrorRedirectionUrl ( { errorCode : AGE_LIMIT_ERROR_CODE } ) ; log . error ( `acs: the age of the user is less than ${ AGE_LIMIT } yo [%s]` , spidUser . dateOfBirth ) ; this . appInsightsTelemetryClient ?. trackEvent ( { name : "spid.error.generic" , properties : { message : "User login blocked for reached age limits" , type : "INFO" } } ) ; return ResponsePermanentRedirect ( redirectionUrl ) ; } const [ errorOrIsBlockedUser , sessionToken , walletToken , myPortalToken , bpdToken , zendeskToken , sessionTrackingId ] = await Promise . all ( [ this . sessionStorage . isBlockedUser ( spidUser . fiscalNumber ) , this . tokenService . getNewTokenAsync ( SESSION_TOKEN_LENGTH_BYTES ) , this . tokenService . getNewTokenAsync ( SESSION_TOKEN_LENGTH_BYTES ) , this . tokenService . getNewTokenAsync ( SESSION_TOKEN_LENGTH_BYTES ) , this . tokenService . getNewTokenAsync ( SESSION_TOKEN_LENGTH_BYTES ) , this . tokenService . getNewTokenAsync ( SESSION_TOKEN_LENGTH_BYTES ) , this . tokenService . getNewTokenAsync ( SESSION_ID_LENGTH_BYTES ) ] ) ; if ( isLeft ( errorOrIsBlockedUser ) ) { const err = errorOrIsBlockedUser . value ; log . error ( `acs: error checking blocked user [${ err . message }]` ) ; return ResponseErrorInternal ( "Error while validating user" ) ; } const isBlockedUser = errorOrIsBlockedUser . value ; if ( isBlockedUser ) { return ResponseErrorForbiddenNotAuthorized ; } const user = toAppUser ( spidUser , sessionToken as SessionToken , walletToken as WalletToken , myPortalToken as MyPortalToken , bpdToken as BPDToken , zendeskToken as ZendeskToken , sessionTrackingId ) ; const [ errorOrIsSessionCreated , getProfileResponse ] = await Promise . all ( [ this . sessionStorage . set ( user ) , this . profileService . getProfile ( user ) ] ) ; if ( isLeft ( errorOrIsSessionCreated ) ) { const error = errorOrIsSessionCreated . value ; log . error ( `acs: error while creating the user session [${ error . message }]` ) ; return ResponseErrorInternal ( "Error while creating the user session" ) ; } const isSessionCreated = errorOrIsSessionCreated . value ; if ( isSessionCreated === false ) { log . error ( "Error creating the user session" ) ; return ResponseErrorInternal ( "Error creating the user session" ) ; } if ( getProfileResponse . kind === "IResponseErrorNotFound" ) { const isTestProfile = this . testLoginFiscalCodes . includes ( user . fiscal_code ) ; const newProfile = { email : spidUser . email , is_email_validated : fromNullable ( spidUser . email ) . map ( ( ) => true ) . getOrElse ( false ) , is_test_profile : isTestProfile } ; const createProfileResponse = await this . profileService . createProfile ( user , newProfile ) ; if ( createProfileResponse . kind !== "IResponseSuccessJson" ) { log . error ( "Error creating new user's profile: %s" , createProfileResponse . detail ) ; return ResponseErrorInternal ( createProfileResponse . kind ) ; } } else if ( getProfileResponse . kind !== "IResponseSuccessJson" ) { log . error ( "Error retrieving user's profile: %s" , getProfileResponse . detail ) ; return ResponseErrorInternal ( getProfileResponse . kind ) ; } try { await this . usersLoginLogService . logUserLogin ( { fiscalCode : spidUser . fiscalNumber , lastLoginAt : new Date ( ) , source : spidUser . email !== undefined ? "spid" : "cie" } ) ; } catch ( e ) { log . debug ( "Cannot notify userLogin: %s" , toError ( e ) . message ) ; } this . notificationService . deleteInstallation ( user . fiscal_code ) . then ( deleteInstallationResponse => { if ( deleteInstallationResponse . kind !== "IResponseSuccessJson" ) { log . debug ( "Cannot delete Notification Installation: %s" , deleteInstallationResponse . detail ) ; } } ) . catch ( err => { log . error ( "Cannot delete Notification Installation: %s" , JSON . stringify ( err ) ) ; } ) ; const urlWithToken = this . getClientProfileRedirectionUrl ( user . session_token ) ; return ResponsePermanentRedirect ( urlWithToken ) ; } /**
     * The Assertion consumer service for test-login
     */ public async acsTest ( userPayload ) { const acsResponse = await this . acs ( userPayload ) ; if ( acsResponse . kind === "IResponsePermanentRedirect" ) { const REDIRECT_URL = clientProfileRedirectionUrl . replace ( "{token}" , "" ) ; return fromNullableE ( new Error ( "Missing detail in ResponsePermanentRedirect" ) ) ( acsResponse . detail ) . chain < string > ( _ => { if ( _ . includes ( REDIRECT_URL ) ) { return right ( _ . replace ( REDIRECT_URL , "" ) ) ; } return left ( new Error ( "Unexpected redirection url" ) ) ; } ) . chain ( token => NonEmptyString . decode ( token ) . mapLeft ( err => new Error ( `Decode Error: [${ errorsToReadableMessages ( err ) }]` ) ) ) . fold < IResponseSuccessJson < AccessToken > | IResponseErrorInternal > ( err => ResponseErrorInternal ( err . message ) , token => ResponseSuccessJson ( { token } ) ) ; } return acsResponse ; } /**
     * Retrieves the logout url from the IDP.
     */ public async logout ( req ) { return withUserFromRequest ( req , user => withCatchAsInternalError ( async ( ) => { const errorOrResponse = await this . sessionStorage . del ( user ) ; if ( isLeft ( errorOrResponse ) ) { const error = errorOrResponse . value ; return ResponseErrorInternal ( error . message ) ; } const response = errorOrResponse . value ; if ( ! response ) { return ResponseErrorInternal ( "Error destroying the user session" ) ; } return ResponseSuccessJson ( { message : "ok" } ) ; } ) ) ; } /**
     * The Single logout service.
     */ public async slo ( ) { const url = { href : "/" } ; return ResponsePermanentRedirect ( url ) ; } /**
     * Returns the user identity stored after the login process.
     */ public readonly getUserIdentity = ( req ) : Promise < IResponseErrorValidation | IResponseErrorInternal | IResponseSuccessJson < UserIdentity > > => withUserFromRequest ( req , async ( user ) => UserIdentity . decode ( user ) . fold < IResponseErrorInternal | IResponseSuccessJson < UserIdentity > > ( _ => ResponseErrorInternal ( "Unexpected User Identity data format." ) , _ => exactUserIdentityDecode ( _ ) . fold < IResponseErrorInternal | IResponseSuccessJson < UserIdentity > > ( _1 => ResponseErrorInternal ( "Exact decode failed." ) , _1 => ResponseSuccessJson < UserIdentity > ( _1 ) ) ) ) ; }