import * as assert from 'assert' ; import { describe , it , afterEach } from 'mocha' ; import * as qs from 'querystring' ; import * as nock from 'nock' ; import { createCrypto } from '../src/crypto/crypto' ; import { StsCredentials , StsCredentialsOptions , StsSuccessfulResponse , } from '../src/auth/stscredentials' ; import { ClientAuthentication , OAuthErrorResponse , getErrorFromOAuthErrorResponse , } from '../src/auth/oauth2common' ; nock . disableNetConnect ( ) ; describe ( 'StsCredentials' , ( ) => { const crypto = createCrypto ( ) ; const baseUrl = 'https://example.com' ; const path = '/token.oauth2' ; const tokenExchangeEndpoint = `${ baseUrl }${ path }` ; const basicAuth = { confidentialClientType : 'basic' , clientId : 'CLIENT_ID' , clientSecret : 'CLIENT_SECRET' , } ; const requestBodyAuth = { confidentialClientType : 'request-body' , clientId : 'CLIENT_ID' , clientSecret : 'CLIENT_SECRET' , } ; const stsCredentialsOptions = { grantType : 'urn:ietf:params:oauth:grant-type:token-exchange' , resource : 'https://api.example.com/' , audience : 'urn:example:cooperation-context' , scope : [ 'scope1' , 'scope2' ] , requestedTokenType : 'urn:ietf:params:oauth:token-type:access_token' , subjectToken : 'HEADER.SUBJECT_TOKEN_PAYLOAD.SIGNATURE' , subjectTokenType : 'urn:ietf:params:oauth:token-type:jwt' , actingParty : { actorToken : 'HEADER.ACTOR_TOKEN_PAYLOAD.SIGNATURE' , actorTokenType : 'urn:ietf:params:oauth:token-type:jwt' , } , } ; const partialStsCredentialsOptions = { grantType : 'urn:ietf:params:oauth:grant-type:token-exchange' , audience : 'urn:example:cooperation-context' , requestedTokenType : 'urn:ietf:params:oauth:token-type:access_token' , subjectToken : 'HEADER.SUBJECT_TOKEN_PAYLOAD.SIGNATURE' , subjectTokenType : 'urn:ietf:params:oauth:token-type:jwt' , } ; const stsSuccessfulResponse = { access_token : 'ACCESS_TOKEN' , issued_token_type : 'urn:ietf:params:oauth:token-type:access_token' , token_type : 'Bearer' , expires_in : 3600 , scope : 'scope1 scope2' , } ; const errorResponse = { error : 'invalid_request' , error_description : 'Invalid subject token' , error_uri : 'https://tools.ietf.org/html/rfc6749#section-5.2' , } ; function assertGaxiosResponsePresent ( resp ) { const gaxiosResponse = resp . res || { } ; assert ( 'data' in gaxiosResponse && 'status' in gaxiosResponse ) ; } function mockStsTokenExchange ( statusCode = 200 , response , request , additionalHeaders ? ) { const headers = Object . assign ( { 'content-type' : 'application/x-www-form-urlencoded' , } , additionalHeaders || { } ) ; return nock ( baseUrl ) . post ( path , qs . stringify ( request ) , { reqheaders : headers , } ) . reply ( statusCode , response ) ; } afterEach ( ( ) => { nock . cleanAll ( ) ; } ) ; describe ( 'exchangeToken()' , ( ) => { const additionalHeaders = { 'x-client-version' : '0.1.2' , } ; const options = { additional : { 'non-standard' : [ 'options' ] , other : 'some-value' , } , } ; const expectedRequest = { grant_type : stsCredentialsOptions . grantType , resource : stsCredentialsOptions . resource , audience : stsCredentialsOptions . audience , scope : stsCredentialsOptions . scope ?. join ( ' ' ) , requested_token_type : stsCredentialsOptions . requestedTokenType , subject_token : stsCredentialsOptions . subjectToken , subject_token_type : stsCredentialsOptions . subjectTokenType , actor_token : stsCredentialsOptions . actingParty ?. actorToken , actor_token_type : stsCredentialsOptions . actingParty ?. actorTokenType , options : JSON . stringify ( options ) , } ; const expectedPartialRequest = { grant_type : stsCredentialsOptions . grantType , audience : stsCredentialsOptions . audience , requested_token_type : stsCredentialsOptions . requestedTokenType , subject_token : stsCredentialsOptions . subjectToken , subject_token_type : stsCredentialsOptions . subjectTokenType , } ; const expectedRequestWithCreds = Object . assign ( { } , expectedRequest , { client_id : requestBodyAuth . clientId , client_secret : requestBodyAuth . clientSecret , } ) ; const expectedPartialRequestWithCreds = Object . assign ( { } , expectedPartialRequest , { client_id : requestBodyAuth . clientId , client_secret : requestBodyAuth . clientSecret , } ) ; describe ( 'without client authentication' , ( ) => { it ( 'should handle successful full request' , async ( ) => { const scope = mockStsTokenExchange ( 200 , stsSuccessfulResponse , expectedRequest , additionalHeaders ) ; const stsCredentials = new StsCredentials ( tokenExchangeEndpoint ) ; const resp = await stsCredentials . exchangeToken ( stsCredentialsOptions , additionalHeaders , options ) ; assertGaxiosResponsePresent ( resp ) ; delete resp . res ; assert . deepStrictEqual ( resp , stsSuccessfulResponse ) ; scope . done ( ) ; } ) ; it ( 'should handle successful partial request' , async ( ) => { const scope = mockStsTokenExchange ( 200 , stsSuccessfulResponse , expectedPartialRequest ) ; const stsCredentials = new StsCredentials ( tokenExchangeEndpoint ) ; const resp = await stsCredentials . exchangeToken ( partialStsCredentialsOptions ) ; assertGaxiosResponsePresent ( resp ) ; delete resp . res ; assert . deepStrictEqual ( resp , stsSuccessfulResponse ) ; scope . done ( ) ; } ) ; it ( 'should handle non-200 response' , async ( ) => { const scope = mockStsTokenExchange ( 400 , errorResponse , expectedRequest , additionalHeaders ) ; const expectedError = getErrorFromOAuthErrorResponse ( errorResponse ) ; const stsCredentials = new StsCredentials ( tokenExchangeEndpoint ) ; await assert . rejects ( stsCredentials . exchangeToken ( stsCredentialsOptions , additionalHeaders , options ) , expectedError ) ; scope . done ( ) ; } ) ; it ( 'should handle request timeout' , async ( ) => { const scope = nock ( baseUrl ) . post ( path , qs . stringify ( expectedRequest ) , { reqheaders : { 'content-type' : 'application/x-www-form-urlencoded' , } , } ) . replyWithError ( { code : 'ETIMEDOUT' } ) ; const stsCredentials = new StsCredentials ( tokenExchangeEndpoint ) ; await assert . rejects ( stsCredentials . exchangeToken ( stsCredentialsOptions , additionalHeaders , options ) , { code : 'ETIMEDOUT' , } ) ; scope . done ( ) ; } ) ; } ) ; describe ( 'with basic client authentication' , ( ) => { const creds = `${ basicAuth . clientId }:${ basicAuth . clientSecret }` ; it ( 'should handle successful full request' , async ( ) => { const scope = mockStsTokenExchange ( 200 , stsSuccessfulResponse , expectedRequest , Object . assign ( { Authorization : `Basic ${ crypto . encodeBase64StringUtf8 ( creds ) }` , } , additionalHeaders ) ) ; const stsCredentials = new StsCredentials ( tokenExchangeEndpoint , basicAuth ) ; const resp = await stsCredentials . exchangeToken ( stsCredentialsOptions , additionalHeaders , options ) ; assertGaxiosResponsePresent ( resp ) ; delete resp . res ; assert . deepStrictEqual ( resp , stsSuccessfulResponse ) ; scope . done ( ) ; } ) ; it ( 'should handle successful partial request' , async ( ) => { const scope = mockStsTokenExchange ( 200 , stsSuccessfulResponse , expectedPartialRequest , { Authorization : `Basic ${ crypto . encodeBase64StringUtf8 ( creds ) }` , } ) ; const stsCredentials = new StsCredentials ( tokenExchangeEndpoint , basicAuth ) ; const resp = await stsCredentials . exchangeToken ( partialStsCredentialsOptions ) ; assertGaxiosResponsePresent ( resp ) ; delete resp . res ; assert . deepStrictEqual ( resp , stsSuccessfulResponse ) ; scope . done ( ) ; } ) ; it ( 'should handle non-200 response' , async ( ) => { const expectedError = getErrorFromOAuthErrorResponse ( errorResponse ) ; const scope = mockStsTokenExchange ( 400 , errorResponse , expectedRequest , Object . assign ( { Authorization : `Basic ${ crypto . encodeBase64StringUtf8 ( creds ) }` , } , additionalHeaders ) ) ; const stsCredentials = new StsCredentials ( tokenExchangeEndpoint , basicAuth ) ; await assert . rejects ( stsCredentials . exchangeToken ( stsCredentialsOptions , additionalHeaders , options ) , expectedError ) ; scope . done ( ) ; } ) ; } ) ; describe ( 'with request-body client authentication' , ( ) => { it ( 'should handle successful full request' , async ( ) => { const scope = mockStsTokenExchange ( 200 , stsSuccessfulResponse , expectedRequestWithCreds , additionalHeaders ) ; const stsCredentials = new StsCredentials ( tokenExchangeEndpoint , requestBodyAuth ) ; const resp = await stsCredentials . exchangeToken ( stsCredentialsOptions , additionalHeaders , options ) ; assertGaxiosResponsePresent ( resp ) ; delete resp . res ; assert . deepStrictEqual ( resp , stsSuccessfulResponse ) ; scope . done ( ) ; } ) ; it ( 'should handle successful partial request' , async ( ) => { const scope = mockStsTokenExchange ( 200 , stsSuccessfulResponse , expectedPartialRequestWithCreds ) ; const stsCredentials = new StsCredentials ( tokenExchangeEndpoint , requestBodyAuth ) ; const resp = await stsCredentials . exchangeToken ( partialStsCredentialsOptions ) ; assertGaxiosResponsePresent ( resp ) ; delete resp . res ; assert . deepStrictEqual ( resp , stsSuccessfulResponse ) ; scope . done ( ) ; } ) ; it ( 'should handle non-200 response' , async ( ) => { const expectedError = getErrorFromOAuthErrorResponse ( errorResponse ) ; const scope = mockStsTokenExchange ( 400 , errorResponse , expectedRequestWithCreds , additionalHeaders ) ; const stsCredentials = new StsCredentials ( tokenExchangeEndpoint , requestBodyAuth ) ; await assert . rejects ( stsCredentials . exchangeToken ( stsCredentialsOptions , additionalHeaders , options ) , expectedError ) ; scope . done ( ) ; } ) ; } ) ; } ) ; } ) ;