TODO import { JWK } from 'jose-openid-client' ; import { Test , TestingModule } from '@nestjs/testing' ; import { CryptographyService } from '@fc/cryptography' ; import { LoggerService } from '@fc/logger' ; import { SERVICE_PROVIDER_SERVICE_TOKEN } from '@fc/oidc' ; import { OidcClientGetEndSessionUrlException , OidcClientIdpBlacklistedException , OidcClientInvalidStateException , OidcClientMissingCodeException , OidcClientMissingStateException , OidcClientTokenFailedException , } from '../exceptions' ; import { TokenParams } from '../interfaces' ; import { OidcClientConfigService } from './oidc-client-config.service' ; import { OidcClientIssuerService } from './oidc-client-issuer.service' ; import { OidcClientUtilsService } from './oidc-client-utils.service' ; describe ( 'OidcClientUtilsService' , ( ) => { let service ; const postLogoutRedirectUriMock = 'https://postLogoutRedirectUriMock' ; const providerUidMock = 'providerUidMockValue' ; const stateMock = 'stateMockValue' ; const idTokenMock = 'idTokenMockValue' ; const endSessionUrlWithParamsMock = `https://endSessionUrlMockMock?id_token_hint=${ idTokenMock }&post_logout_redirect_uri=${ postLogoutRedirectUriMock }&state=${ stateMock }` ; const loggerServiceMock = { setContext : jest . fn ( ) , trace : jest . fn ( ) , debug : jest . fn ( ) , businessEvent : jest . fn ( ) , } as unknown as LoggerService ; const IdentityProviderServiceMock = { getList : jest . fn ( ) } ; const IssuerClientMock = jest . fn ( ) ; const cryptoServiceMock = { genRandomString : jest . fn ( ) , } ; const randomStringMock = 'randomStringMockValue' ; const IssuerProxyMock = { discover : jest . fn ( ) , } as any ; const oidcClientIssuerServiceMock = { getClient : jest . fn ( ) , } ; const oidcClientConfigServiceMock = { get : jest . fn ( ) , reload : jest . fn ( ) , } ; const authorizationUrlMock = jest . fn ( ) ; const callbackParamsMock = jest . fn ( ) ; const callbackMock = jest . fn ( ) ; const userinfoMock = jest . fn ( ) ; const revokeMock = jest . fn ( ) ; const endSessionUrlMock = jest . fn ( ) ; const clientMock = { authorizationUrl : authorizationUrlMock , callbackParams : callbackParamsMock , callback : callbackMock , userinfo : userinfoMock , revoke : revokeMock , endSessionUrl : endSessionUrlMock , metadata : { redirect_uris : [ 'redirect' , 'uris' ] , response_types : [ 'response' , 'types' ] , discoveryUrl : 'mock well-known url' , } , } ; const createOidcClientMock = jest . fn ( ) ; const serviceProviderServiceMock = { shouldExcludeIdp : jest . fn ( ) , } ; beforeEach ( async ( ) => { const module = await Test . createTestingModule ( { providers : [ OidcClientUtilsService , LoggerService , CryptographyService , OidcClientConfigService , OidcClientIssuerService , { provide : SERVICE_PROVIDER_SERVICE_TOKEN , useValue : serviceProviderServiceMock , } , ] , } ) . overrideProvider ( OidcClientIssuerService ) . useValue ( oidcClientIssuerServiceMock ) . overrideProvider ( OidcClientConfigService ) . useValue ( oidcClientConfigServiceMock ) . overrideProvider ( CryptographyService ) . useValue ( cryptoServiceMock ) . overrideProvider ( LoggerService ) . useValue ( loggerServiceMock ) . compile ( ) ; service = module . get < OidcClientUtilsService > ( OidcClientUtilsService ) ; jest . resetAllMocks ( ) ; IdentityProviderServiceMock . getList . mockResolvedValue ( 'IdentityProviderServiceMock Resolve Value' ) ; authorizationUrlMock . mockResolvedValue ( 'authorizationUrlMock Resolve Value' ) ; callbackParamsMock . mockResolvedValue ( { state : 'callbackParamsState' , code : 'callbackParamsCode' , } ) ; callbackMock . mockResolvedValue ( 'callbackMock Resolve Value' ) ; userinfoMock . mockResolvedValue ( 'userinfoMock Resolve Value' ) ; revokeMock . mockResolvedValue ( void 0 ) ; IssuerProxyMock . discover . mockResolvedValue ( { Client : IssuerClientMock , } ) ; oidcClientIssuerServiceMock . getClient . mockResolvedValue ( clientMock ) ; oidcClientConfigServiceMock . get . mockResolvedValue ( { issuer : 'http://foo.bar' , configuration : { } , jwks : { keys : [ ] } , } ) ; cryptoServiceMock . genRandomString . mockReturnValue ( randomStringMock ) ; } ) ; describe ( 'constructor()' , ( ) => { it ( 'should be defined' , ( ) => { expect ( service ) . toBeDefined ( ) ; } ) ; } ) ; describe ( 'getAuthorizeUrl()' , ( ) => { it ( 'should call authorizationUrl' , async ( ) => { const state = 'someState' ; const nonce = 'someNonce' ; const scope = 'foo_scope bar_scope' ; const providerId = 'myidp' ; const acr_values = 'eidas1' ; service [ 'createOidcClient' ] = createOidcClientMock ; await service . getAuthorizeUrl ( { state , scope , idpId : providerId , acr_values , nonce , } ) ; expect ( authorizationUrlMock ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call authorizationUrl without claims parameters' , async ( ) => { const state = 'someState' ; const nonce = 'someNonce' ; const scope = 'foo_scope bar_scope' ; const providerId = 'myidp' ; const acr_values = 'eidas1' ; service [ 'createOidcClient' ] = createOidcClientMock ; await service . getAuthorizeUrl ( { state , scope , idpId : providerId , acr_values , nonce , } ) ; expect ( authorizationUrlMock ) . toHaveBeenCalledWith ( { prompt : 'login' , state , scope , acr_values , nonce , claims : undefined , } ) ; } ) ; it ( 'should call authorizationUrl with claims parameters' , async ( ) => { const state = 'someState' ; const nonce = 'someNonce' ; const scope = 'foo_scope bar_scope' ; const providerId = 'myidp' ; const acr_values = 'eidas1' ; const claims = '{ id_token: { amr: { essential: true } } }' ; service [ 'createOidcClient' ] = createOidcClientMock ; await service . getAuthorizeUrl ( { state , scope , idpId : providerId , acr_values , nonce , claims , } ) ; expect ( authorizationUrlMock ) . toHaveBeenCalledWith ( { prompt : 'login' , state , scope , acr_values , nonce , claims , } ) ; } ) ; it ( 'should resolve to object containing state & authorizationUrl' , async ( ) => { const state = 'randomStateMock' ; const nonce = 'randomNonceMock' ; const scope = 'foo_scope bar_scope' ; const providerId = 'myidp' ; const acr_values = 'eidas1' ; service [ 'createOidcClient' ] = createOidcClientMock ; const url = await service . getAuthorizeUrl ( { state , scope , idpId : providerId , acr_values , nonce , } ) ; expect ( state ) . toEqual ( 'randomStateMock' ) ; expect ( url ) . toBe ( 'authorizationUrlMock Resolve Value' ) ; } ) ; } ) ; describe ( 'wellKnownKeys()' , ( ) => { it ( 'should return keys' , async ( ) => { const JwkKeyMock = { toJWK : jest . fn ( ) . mockReturnValueOnce ( 'a' ) . mockReturnValueOnce ( 'b' ) , } ; const spy = jest . spyOn ( JWK , 'asKey' ) . mockReturnValue ( JwkKeyMock as any ) ; oidcClientConfigServiceMock . get . mockReturnValueOnce ( { jwks : { keys : [ 'foo' , 'bar' ] } , } ) ; const result = await service . wellKnownKeys ( ) ; expect ( spy ) . toHaveBeenCalledTimes ( 2 ) ; expect ( spy ) . toHaveBeenCalledWith ( 'foo' ) ; expect ( spy ) . toHaveBeenCalledWith ( 'bar' ) ; expect ( JwkKeyMock . toJWK ) . toHaveBeenCalledTimes ( 2 ) ; expect ( result ) . toEqual ( { keys : [ 'a' , 'b' ] } ) ; } ) ; } ) ; describe ( 'buildAuthorizeParameters()' , ( ) => { it ( 'should call crypto to generate state' , async ( ) => { const result = await service . buildAuthorizeParameters ( ) ; expect ( result . state ) . toBeDefined ( ) ; expect ( result . state ) . toBe ( randomStringMock ) ; } ) ; it ( 'should return parameters + generated state' , async ( ) => { const result = await service . buildAuthorizeParameters ( ) ; expect ( result ) . toEqual ( { state : randomStringMock , nonce : randomStringMock , } ) ; } ) ; } ) ; describe ( 'buildExtraParameters' , ( ) => { it ( 'should return empty object' , ( ) => { const parametersMock = undefined ; const expected = { } ; const result = service [ 'buildExtraParameters' ] ( parametersMock ) ; expect ( result ) . toEqual ( expected ) ; } ) ; it ( 'should return object with argument in exchangeBody property' , ( ) => { const parametersMock = { foo : 'bar' } ; const expected = { exchangeBody : parametersMock } ; const result = service [ 'buildExtraParameters' ] ( parametersMock ) ; expect ( result ) . toEqual ( expected ) ; } ) ; } ) ; describe ( 'getTokenSet()' , ( ) => { const req = { session : { codeVerifier : 'codeVerifierValue' } } ; const providerId = 'foo' ; const params = { state : 'callbackParamsState' , nonce : 'callbackParamsNonce' , } ; it ( 'should call extractParams with callbackParams' , async ( ) => { const extraParamsMockedReturn = { mock : 'value' } ; service [ 'buildExtraParameters' ] = jest . fn ( ) . mockReturnValue ( extraParamsMockedReturn ) ; await service . getTokenSet ( req , providerId , params ) ; expect ( callbackMock ) . toHaveBeenCalled ( ) ; expect ( callbackMock ) . toHaveBeenCalledWith ( 'redirect,uris' , { code : 'callbackParamsCode' , state : 'callbackParamsState' , } , { response_type : 'response,types' , state : 'callbackParamsState' , nonce : 'callbackParamsNonce' , } , extraParamsMockedReturn ) ; } ) ; it ( 'should return resolve value of client.callback' , async ( ) => { const result = await service . getTokenSet ( req , providerId , params ) ; expect ( result ) . toBe ( 'callbackMock Resolve Value' ) ; } ) ; it ( 'should throw if something unexpected goes wrong in extractParams' , async ( ) => { const errorMock = new Error ( 'lol' ) ; callbackMock . mockRejectedValueOnce ( errorMock ) ; expect ( service . getTokenSet ( req , providerId , params ) ) . rejects . toThrow ( OidcClientTokenFailedException ) ; } ) ; } ) ; describe ( 'extractParams()' , ( ) => { const req = { session : { codeVerifier : 'codeVerifierValue' } } ; const state = 'callbackParamsState' ; it ( 'should throw if code is not provided in url' , async ( ) => { callbackParamsMock . mockResolvedValueOnce ( { state : 'callbackParamsState' , } ) ; expect ( service [ 'extractParams' ] ( req , clientMock , state ) ) . rejects . toThrow ( OidcClientMissingCodeException ) ; } ) ; it ( 'should throw if state is not provided in url' , async ( ) => { callbackParamsMock . mockResolvedValueOnce ( { code : 'callbackParamsCode' , } ) ; expect ( service [ 'extractParams' ] ( req , clientMock , state ) ) . rejects . toThrow ( OidcClientMissingStateException ) ; } ) ; it ( 'should throw if state in url does not match state in session' , async ( ) => { callbackParamsMock . mockResolvedValueOnce ( { state : 'callbackParamsState' , code : 'callbackParamsCode' , } ) ; const invalidState = 'notTheSameStateAsInRequest' ; expect ( service [ 'extractParams' ] ( req , clientMock , invalidState ) ) . rejects . toThrow ( OidcClientInvalidStateException ) ; } ) ; } ) ; describe ( 'revokeToken()' , ( ) => { it ( 'should call client.revoke' , async ( ) => { const accessToken = 'accessTokenValue' ; const providerId = 'providerIdValue' ; await service . revokeToken ( accessToken , providerId ) ; expect ( revokeMock ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; } ) ; describe ( 'getUserInfo()' , ( ) => { it ( 'should return client.userinfo result' , async ( ) => { const accessToken = 'accessTokenValue' ; const providerId = 'providerIdValue' ; service [ 'createOidcClient' ] = createOidcClientMock ; const result = await service . getUserInfo ( accessToken , providerId ) ; expect ( result ) . toBe ( 'userinfoMock Resolve Value' ) ; } ) ; } ) ; describe ( 'getEndSessionUrl()' , ( ) => { beforeEach ( ( ) => { oidcClientIssuerServiceMock . getClient . mockResolvedValueOnce ( clientMock ) ; clientMock . endSessionUrl . mockReturnValueOnce ( endSessionUrlWithParamsMock ) ; } ) ; it ( 'should retrieves the client instance by calling utils.getClient()' , async ( ) => { await service . getEndSessionUrl ( providerUidMock , stateMock , idTokenMock , postLogoutRedirectUriMock ) ; expect ( oidcClientIssuerServiceMock . getClient ) . toHaveBeenCalledTimes ( 1 ) ; expect ( oidcClientIssuerServiceMock . getClient ) . toHaveBeenCalledWith ( providerUidMock ) ; } ) ; it ( 'should call client.endSessionUrl() with given parameters' , async ( ) => { await service . getEndSessionUrl ( providerUidMock , stateMock , idTokenMock , postLogoutRedirectUriMock ) ; expect ( clientMock . endSessionUrl ) . toHaveBeenCalledTimes ( 1 ) ; expect ( clientMock . endSessionUrl ) . toHaveBeenCalledWith ( { id_token_hint : idTokenMock , state : stateMock , post_logout_redirect_uri : postLogoutRedirectUriMock , } ) ; } ) ; it ( 'should returns the endSessionUrl' , async ( ) => { const result = await service . getEndSessionUrl ( providerUidMock , stateMock , idTokenMock , postLogoutRedirectUriMock ) ; expect ( result ) . toStrictEqual ( endSessionUrlWithParamsMock ) ; } ) ; it ( 'should throw an OidcClientGetEndSessionUrlException' , async ( ) => { clientMock . endSessionUrl . mockReset ( ) . mockImplementationOnce ( ( ) => { throw new Error ( 'Unknown Error' ) ; } ) ; const expectedError = new OidcClientGetEndSessionUrlException ( ) ; await expect ( ( ) => service . getEndSessionUrl ( providerUidMock , stateMock , idTokenMock , postLogoutRedirectUriMock ) ) . rejects . toThrow ( expectedError ) ; } ) ; } ) ; describe ( 'checkIdpBlacklisted()' , ( ) => { it ( 'should return OidcClientRuntimeException isIdpBlacklist throw an error' , async ( ) => { const errorMock = new Error ( 'Une erreur technique est survenue, fermez l’onglet de votre navigateur et reconnectez-vous.' ) ; serviceProviderServiceMock . shouldExcludeIdp . mockRejectedValueOnce ( errorMock ) ; await expect ( service . checkIdpBlacklisted ( 'spId' , 'idpId' ) ) . rejects . toThrow ( errorMock ) ; } ) ; it ( 'should throw OidcClientIdpBlacklistedException because identity provider is blacklisted' , async ( ) => { const errorMock = new OidcClientIdpBlacklistedException ( ) ; serviceProviderServiceMock . shouldExcludeIdp . mockReturnValueOnce ( true ) ; await expect ( service . checkIdpBlacklisted ( 'spId' , 'idpId' ) ) . rejects . toThrow ( errorMock ) ; } ) ; it ( 'should return false because identity provider is not blacklisted' , async ( ) => { serviceProviderServiceMock . shouldExcludeIdp . mockReturnValueOnce ( false ) ; const result = await service . checkIdpBlacklisted ( 'spId' , 'idpId' ) ; expect ( result ) . toBeFalsy ( ) ; } ) ; } ) ; } ) ;