import { GaxiosOptions } from 'gaxios' ; import { describe , it } from 'mocha' ; import * as assert from 'assert' ; import * as querystring from 'querystring' ; import { Headers } from '../src/auth/oauth2client' ; import { ClientAuthentication , OAuthClientAuthHandler , getErrorFromOAuthErrorResponse , } from '../src/auth/oauth2common' ; /** Test class to test abstract class OAuthClientAuthHandler. */ class TestOAuthClientAuthHandler extends OAuthClientAuthHandler { testApplyClientAuthenticationOptions ( opts , bearerToken ? ) { return this . applyClientAuthenticationOptions ( opts , bearerToken ) ; } } /** Custom error object for testing additional fields on an Error. */ class CustomError extends Error { public readonly code ? : string ; constructor ( message , stack ? , code ? ) { super ( message ) ; this . name = 'CustomError' ; this . stack = stack ; this . code = code ; } } describe ( 'OAuthClientAuthHandler' , ( ) => { const basicAuth = { confidentialClientType : 'basic' , clientId : 'username' , clientSecret : 'password' , } ; const expectedBase64EncodedCred = 'dXNlcm5hbWU6cGFzc3dvcmQ=' ; const basicAuthNoSecret = { confidentialClientType : 'basic' , clientId : 'username' , } ; const expectedBase64EncodedCredNoSecret = 'dXNlcm5hbWU6' ; const reqBodyAuth = { confidentialClientType : 'request-body' , clientId : 'username' , clientSecret : 'password' , } ; const reqBodyAuthNoSecret = { confidentialClientType : 'request-body' , clientId : 'username' , } ; it ( 'should not process request when no client authentication is used' , ( ) => { const handler = new TestOAuthClientAuthHandler ( ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , data : { key1 : 'value1' , key2 : 'value2' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; handler . testApplyClientAuthenticationOptions ( actualOptions ) ; assert . deepStrictEqual ( originalOptions , actualOptions ) ; } ) ; it ( 'should process request with basic client auth' , ( ) => { const handler = new TestOAuthClientAuthHandler ( basicAuth ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , data : { key1 : 'value1' , key2 : 'value2' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; ( expectedOptions . headers as Headers ) . Authorization = `Basic ${ expectedBase64EncodedCred }` ; handler . testApplyClientAuthenticationOptions ( actualOptions ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; it ( 'should process request with secretless basic client auth' , ( ) => { const handler = new TestOAuthClientAuthHandler ( basicAuthNoSecret ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , data : { key1 : 'value1' , key2 : 'value2' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; ( expectedOptions . headers as Headers ) . Authorization = `Basic ${ expectedBase64EncodedCredNoSecret }` ; handler . testApplyClientAuthenticationOptions ( actualOptions ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; it ( 'should process GET (non-request-body) with basic client auth' , ( ) => { const handler = new TestOAuthClientAuthHandler ( basicAuth ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'GET' , headers : { 'Content-Type' : 'application/json' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; ( expectedOptions . headers as Headers ) . Authorization = `Basic ${ expectedBase64EncodedCred }` ; handler . testApplyClientAuthenticationOptions ( actualOptions ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; describe ( 'with request-body client auth' , ( ) => { const unsupportedMethods = [ undefined , 'GET' , 'DELETE' , 'TRACE' , 'OPTIONS' , 'HEAD' , ] ; unsupportedMethods . forEach ( method => { it ( `should throw on requests with unsupported HTTP method ${ method }` , ( ) => { const expectedError = new Error ( `${ method || 'GET' } HTTP method does not support request-body ` + 'client authentication' ) ; const handler = new TestOAuthClientAuthHandler ( reqBodyAuth ) ; const originalOptions = { method , url : 'https://www.example.com/path/to/api' , } ; assert . throws ( ( ) => { handler . testApplyClientAuthenticationOptions ( originalOptions ) ; } , expectedError ) ; } ) ; } ) ; it ( 'should throw on unsupported content-types' , ( ) => { const expectedError = new Error ( 'text/html content-types are not supported with request-body ' + 'client authentication' ) ; const handler = new TestOAuthClientAuthHandler ( reqBodyAuth ) ; const originalOptions = { headers : { 'Content-Type' : 'text/html' , } , method : 'POST' , url : 'https://www.example.com/path/to/api' , } ; assert . throws ( ( ) => { handler . testApplyClientAuthenticationOptions ( originalOptions ) ; } , expectedError ) ; } ) ; it ( 'should inject creds in non-empty json content' , ( ) => { const handler = new TestOAuthClientAuthHandler ( reqBodyAuth ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , data : { key1 : 'value1' , key2 : 'value2' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; expectedOptions . data . client_id = reqBodyAuth . clientId ; expectedOptions . data . client_secret = reqBodyAuth . clientSecret ; handler . testApplyClientAuthenticationOptions ( actualOptions ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; it ( 'should inject secretless creds in json content' , ( ) => { const handler = new TestOAuthClientAuthHandler ( reqBodyAuthNoSecret ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , data : { key1 : 'value1' , key2 : 'value2' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; expectedOptions . data . client_id = reqBodyAuthNoSecret . clientId ; expectedOptions . data . client_secret = '' ; handler . testApplyClientAuthenticationOptions ( actualOptions ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; it ( 'should inject creds in empty json content' , ( ) => { const handler = new TestOAuthClientAuthHandler ( reqBodyAuth ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; expectedOptions . data = { client_id : reqBodyAuth . clientId , client_secret : reqBodyAuth . clientSecret , } ; handler . testApplyClientAuthenticationOptions ( actualOptions ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; it ( 'should inject creds in non-empty x-www-form-urlencoded content' , ( ) => { const handler = new TestOAuthClientAuthHandler ( reqBodyAuth ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'content-Type' : 'application/x-www-form-urlencoded' , } , data : querystring . stringify ( { key1 : 'value1' , key2 : 'value2' } ) , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; expectedOptions . data = querystring . stringify ( { key1 : 'value1' , key2 : 'value2' , client_id : reqBodyAuth . clientId , client_secret : reqBodyAuth . clientSecret , } ) ; handler . testApplyClientAuthenticationOptions ( actualOptions ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; it ( 'should inject secretless creds in x-www-form-urlencoded content' , ( ) => { const handler = new TestOAuthClientAuthHandler ( reqBodyAuthNoSecret ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/x-www-form-urlencoded' , } , data : querystring . stringify ( { key1 : 'value1' , key2 : 'value2' } ) , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; expectedOptions . data = querystring . stringify ( { key1 : 'value1' , key2 : 'value2' , client_id : reqBodyAuth . clientId , client_secret : '' , } ) ; handler . testApplyClientAuthenticationOptions ( actualOptions ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; it ( 'should inject creds in empty x-www-form-urlencoded content' , ( ) => { const handler = new TestOAuthClientAuthHandler ( reqBodyAuth ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/x-www-form-urlencoded' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; expectedOptions . data = querystring . stringify ( { client_id : reqBodyAuth . clientId , client_secret : reqBodyAuth . clientSecret , } ) ; handler . testApplyClientAuthenticationOptions ( actualOptions ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; } ) ; it ( 'should process request with bearer token when provided' , ( ) => { const bearerToken = 'BEARER_TOKEN' ; const handler = new TestOAuthClientAuthHandler ( ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , data : { key1 : 'value1' , key2 : 'value2' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; ( expectedOptions . headers as Headers ) . Authorization = `Bearer ${ bearerToken }` ; handler . testApplyClientAuthenticationOptions ( actualOptions , bearerToken ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; it ( 'should prioritize bearer token over basic auth' , ( ) => { const bearerToken = 'BEARER_TOKEN' ; const handler = new TestOAuthClientAuthHandler ( basicAuth ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , data : { key1 : 'value1' , key2 : 'value2' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; ( expectedOptions . headers as Headers ) . Authorization = `Bearer ${ bearerToken }` ; handler . testApplyClientAuthenticationOptions ( actualOptions , bearerToken ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; it ( 'should prioritize bearer token over request body' , ( ) => { const bearerToken = 'BEARER_TOKEN' ; const handler = new TestOAuthClientAuthHandler ( reqBodyAuth ) ; const originalOptions = { url : 'https://www.example.com/path/to/api' , method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , data : { key1 : 'value1' , key2 : 'value2' , } , } ; const actualOptions = Object . assign ( { } , originalOptions ) ; const expectedOptions = Object . assign ( { } , originalOptions ) ; ( expectedOptions . headers as Headers ) . Authorization = `Bearer ${ bearerToken }` ; handler . testApplyClientAuthenticationOptions ( actualOptions , bearerToken ) ; assert . deepStrictEqual ( expectedOptions , actualOptions ) ; } ) ; } ) ; describe ( 'getErrorFromOAuthErrorResponse' , ( ) => { it ( 'should create expected error with code, description and uri' , ( ) => { const resp = { error : 'unsupported_grant_type' , error_description : 'The provided grant_type is unsupported' , error_uri : 'https://tools.ietf.org/html/rfc6749' , } ; const error = getErrorFromOAuthErrorResponse ( resp ) ; assert . strictEqual ( error . message , `Error code ${ resp . error }: ${ resp . error_description } ` + `- ${ resp . error_uri }` ) ; } ) ; it ( 'should create expected error with code and description' , ( ) => { const resp = { error : 'unsupported_grant_type' , error_description : 'The provided grant_type is unsupported' , } ; const error = getErrorFromOAuthErrorResponse ( resp ) ; assert . strictEqual ( error . message , `Error code ${ resp . error }: ${ resp . error_description }` ) ; } ) ; it ( 'should create expected error with code only' , ( ) => { const resp = { error : 'unsupported_grant_type' , } ; const error = getErrorFromOAuthErrorResponse ( resp ) ; assert . strictEqual ( error . message , `Error code ${ resp . error }` ) ; } ) ; it ( 'should preserve the original error properties' , ( ) => { const originalError = new CustomError ( 'Original error message' , 'Error stack' , '123456' ) ; const resp = { error : 'unsupported_grant_type' , error_description : 'The provided grant_type is unsupported' , error_uri : 'https://tools.ietf.org/html/rfc6749' , } ; const expectedError = new CustomError ( `Error code ${ resp . error }: ${ resp . error_description } ` + `- ${ resp . error_uri }` , 'Error stack' , '123456' ) ; const actualError = getErrorFromOAuthErrorResponse ( resp , originalError ) ; assert . strictEqual ( actualError . message , expectedError . message ) ; assert . strictEqual ( ( actualError as any ) . code , expectedError . code ) ; assert . strictEqual ( actualError . name , expectedError . name ) ; assert . strictEqual ( actualError . stack , expectedError . stack ) ; } ) ; } ) ;