diff --git a/CHANGELOG.md b/CHANGELOG.md index 03f0306ad..1b973cf32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +# 7.7.1 + +- [#1529](https://github.com/okta/okta-auth-js/pull/1529) fix: persist `extraParams` passed to `/authorize` and include them during token refresh + ## 7.7.0 ### Features diff --git a/lib/oidc/endpoints/token.ts b/lib/oidc/endpoints/token.ts index e98f2f1e5..8a257a5cc 100644 --- a/lib/oidc/endpoints/token.ts +++ b/lib/oidc/endpoints/token.ts @@ -141,8 +141,13 @@ export async function postRefreshToken( return name + '=' + encodeURIComponent(value!); }).join('&'); + let url = refreshToken.tokenUrl; + if (options.extraParams && Object.keys(options.extraParams).length >= 1) { + url += toQueryString(options.extraParams); + } + const params: TokenRequestParams = { - url: refreshToken.tokenUrl, + url, data, dpopKeyPair: options?.dpopKeyPair }; diff --git a/lib/oidc/exchangeCodeForTokens.ts b/lib/oidc/exchangeCodeForTokens.ts index 12cba44d0..b679ff020 100644 --- a/lib/oidc/exchangeCodeForTokens.ts +++ b/lib/oidc/exchangeCodeForTokens.ts @@ -37,6 +37,7 @@ export async function exchangeCodeForTokens(sdk: OktaAuthOAuthInterface, tokenPa acrValues, dpop, dpopPairId, + extraParams } = tokenParams; // postToTokenEndpoint() params @@ -64,6 +65,7 @@ export async function exchangeCodeForTokens(sdk: OktaAuthOAuthInterface, tokenPa responseType, ignoreSignature, acrValues, + extraParams }; try { diff --git a/lib/oidc/handleOAuthResponse.ts b/lib/oidc/handleOAuthResponse.ts index 02fa80cb7..ea92baddb 100644 --- a/lib/oidc/handleOAuthResponse.ts +++ b/lib/oidc/handleOAuthResponse.ts @@ -55,6 +55,7 @@ export async function handleOAuthResponse( ): Promise { const pkce = sdk.options.pkce !== false; + // The result contains an authorization_code and PKCE is enabled // `exchangeCodeForTokens` will call /token then call `handleOauthResponse` recursively with the result if (pkce && (res.code || res.interaction_code)) { @@ -106,6 +107,10 @@ export async function handleOAuthResponse( if (tokenParams.dpopPairId) { tokenDict.accessToken.dpopPairId = tokenParams.dpopPairId; } + + if (tokenParams.extraParams) { + tokenDict.accessToken.extraParams = tokenParams.extraParams; + } } if (refreshToken) { @@ -123,6 +128,10 @@ export async function handleOAuthResponse( if (tokenParams.dpopPairId) { tokenDict.refreshToken.dpopPairId = tokenParams.dpopPairId; } + + if (tokenParams.extraParams) { + tokenDict.refreshToken.extraParams = tokenParams.extraParams; + } } if (idToken) { @@ -137,6 +146,10 @@ export async function handleOAuthResponse( clientId: clientId! }; + if (tokenParams.extraParams) { + idTokenObj.extraParams = tokenParams.extraParams; + } + const validationParams: TokenVerifyParams = { clientId: clientId!, issuer: urls.issuer!, diff --git a/lib/oidc/renewToken.ts b/lib/oidc/renewToken.ts index fcf0dbdd7..d59e28b02 100644 --- a/lib/oidc/renewToken.ts +++ b/lib/oidc/renewToken.ts @@ -55,14 +55,15 @@ export async function renewToken(sdk: OktaAuthOAuthInterface, token: Token): Pro responseType = 'id_token'; } - const { scopes, authorizeUrl, userinfoUrl, issuer, dpopPairId } = token as (AccessToken & IDToken); + const { scopes, authorizeUrl, userinfoUrl, issuer, dpopPairId, extraParams } = token as (AccessToken & IDToken); return getWithoutPrompt(sdk, { responseType, scopes, authorizeUrl, userinfoUrl, issuer, - dpopPairId + dpopPairId, + extraParams }) .then(function (res) { return getSingleToken(token, res.tokens); diff --git a/lib/oidc/renewTokens.ts b/lib/oidc/renewTokens.ts index 3e47d9939..d7f853a47 100644 --- a/lib/oidc/renewTokens.ts +++ b/lib/oidc/renewTokens.ts @@ -41,6 +41,7 @@ export async function renewTokens(sdk, options?: RenewTokensParams): Promise; } export interface AccessToken extends AbstractToken { diff --git a/lib/oidc/types/meta.ts b/lib/oidc/types/meta.ts index 8b857a2b1..fd096d550 100644 --- a/lib/oidc/types/meta.ts +++ b/lib/oidc/types/meta.ts @@ -26,7 +26,8 @@ export interface OAuthTransactionMeta extends 'ignoreSignature' | 'nonce' | 'acrValues' | - 'enrollAmrValues' + 'enrollAmrValues' | + 'extraParams' > { urls: CustomUrls; diff --git a/lib/oidc/util/oauthMeta.ts b/lib/oidc/util/oauthMeta.ts index 6e00e1e6e..94f2ebeb2 100644 --- a/lib/oidc/util/oauthMeta.ts +++ b/lib/oidc/util/oauthMeta.ts @@ -20,6 +20,7 @@ export function createOAuthMeta( nonce: tokenParams.nonce!, ignoreSignature: tokenParams.ignoreSignature!, acrValues: tokenParams.acrValues, + extraParams: tokenParams.extraParams }; if (tokenParams.pkce === false) { diff --git a/samples/test/features/self-service-registration-custom-attribute.feature b/samples/test/features/self-service-registration-custom-attribute.feature index 1648132f8..7c4dd96a8 100644 --- a/samples/test/features/self-service-registration-custom-attribute.feature +++ b/samples/test/features/self-service-registration-custom-attribute.feature @@ -16,4 +16,4 @@ Feature: Add another Required Attribute to the Profile Enrollment Policy And she fills out another property And she submits the form # Then her user is created in the "Staged" state - Then she is redirected to the "Select Authenticator Method" page + Then she is redirected to the "Select Authenticator" page diff --git a/samples/test/features/self-service-registration.feature b/samples/test/features/self-service-registration.feature index 1fdf7a635..2d40b123b 100644 --- a/samples/test/features/self-service-registration.feature +++ b/samples/test/features/self-service-registration.feature @@ -20,8 +20,8 @@ Scenario: Mary signs up for an account with Password, setups up required Email f And she fills out her Last Name And she fills out her Email And she submits the form - Then she is redirected to the "Select Authenticator Method" page - When she selects the "email" method + Then she is redirected to the "Select Authenticator" page + When she selects the "Email" factor And she submits the form Then she sees a page to input a code for email authenticator enrollment When she inputs the correct code from her "Email" @@ -47,8 +47,8 @@ Scenario: Mary signs up for an account with Password, setups up required Email f And she fills out her Last Name And she fills out her Email And she submits the form - Then she is redirected to the "Select Authenticator Method" page - When she selects the "email" method + Then she is redirected to the "Select Authenticator" page + When she selects the "Email" factor And she submits the form Then she sees a page to input a code for email authenticator enrollment When she inputs the correct code from her "Email" @@ -90,8 +90,8 @@ Scenario: Mary signs up for an account with Password, sets up required Email fac And she fills out her Last Name And she fills out her Email And she submits the form - Then she is redirected to the "Select Authenticator Method" page - When she selects the "email" method + Then she is redirected to the "Select Authenticator" page + When she selects the "Email" factor And she submits the form Then she sees a page to input a code for email authenticator enrollment When she inputs the correct code from her "Email" @@ -118,8 +118,8 @@ Scenario: Mary signs up for an account with Password, setups up required Email f And she fills out her Last Name And she fills out her Email And she submits the form - Then she is redirected to the "Select Authenticator Method" page - When she selects the "email" method + Then she is redirected to the "Select Authenticator" page + When she selects the "Email" factor And she submits the form Then she sees a page to input a code for email authenticator enrollment When she clicks the Email magic link for email verification diff --git a/test/apps/app/src/testApp.ts b/test/apps/app/src/testApp.ts index e8301b7fb..c7f5470a7 100644 --- a/test/apps/app/src/testApp.ts +++ b/test/apps/app/src/testApp.ts @@ -674,6 +674,9 @@ class TestApp { scopes: this.config.defaultScopes ? [] : this.config.scopes, acrValues: this.config.acrValues, state: this.config.state, + extraParams: { + foo: 'bar' + } }, options); return this.oktaAuth.token.getWithRedirect(options) .catch(e => { diff --git a/test/spec/oidc/endpoints/token.ts b/test/spec/oidc/endpoints/token.ts index 5cb4596ac..770653ee6 100644 --- a/test/spec/oidc/endpoints/token.ts +++ b/test/spec/oidc/endpoints/token.ts @@ -40,6 +40,7 @@ describe('token endpoint', function() { var endpoint = '/oauth2/v1/token'; var codeVerifier = 'superfake'; var authorizationCode = 'notreal'; + var extraParams = { foo: 'bar' }; util.itMakesCorrectRequestResponse({ title: 'requests a token', @@ -55,7 +56,7 @@ describe('token endpoint', function() { data: { client_id: CLIENT_ID, grant_type: 'authorization_code', - redirect_uri: REDIRECT_URI + redirect_uri: REDIRECT_URI, }, headers: { 'Accept': 'application/json', @@ -80,7 +81,7 @@ describe('token endpoint', function() { clientId: CLIENT_ID, redirectUri: REDIRECT_URI, authorizationCode: authorizationCode, - codeVerifier: codeVerifier, + codeVerifier: codeVerifier }, { tokenUrl: ISSUER + endpoint }); @@ -156,6 +157,29 @@ describe('token endpoint', function() { }); + describe('postRefreshToken', () => { + var authClient; + + beforeEach(function() { + spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(true); + authClient = new OktaAuth({ + issuer: 'https://auth-js-test.okta.com' + }); + }); + + it('should append extra params as query params', async () => { + var httpRequest = jest.spyOn(mocked.http, 'httpRequest').mockImplementation(); + const refreshToken = tokens.standardRefreshTokenParsed; + await postRefreshToken(authClient, { extraParams }, refreshToken); + expect(httpRequest).toHaveBeenCalled(); + expect(httpRequest).toHaveBeenLastCalledWith(expect.any(OktaAuth), expect.objectContaining({ + url: 'https://auth-js-test.okta.com/oauth2/v1/token?foo=bar', + method: 'POST', + headers: {'Content-Type': 'application/x-www-form-urlencoded' } + })); + }); + }); + describe('dpop', () => { const ctx: any = {}; diff --git a/test/spec/oidc/exchangeCodeForTokens.ts b/test/spec/oidc/exchangeCodeForTokens.ts index d2e0ac86a..19d3cca55 100644 --- a/test/spec/oidc/exchangeCodeForTokens.ts +++ b/test/spec/oidc/exchangeCodeForTokens.ts @@ -52,7 +52,13 @@ describe('exchangeCodeForTokens', () => { (postToTokenEndpoint as jest.Mock).mockResolvedValue(oauthResponse); (handleOAuthResponse as jest.Mock).mockResolvedValue(tokenResponse); const acrValues = 'foo'; - const tokenParams = { ...testParams, acrValues }; + const tokenParams = { + ...testParams, + acrValues, + extraParams: { + foo: 'bar' + } + }; const urls = getOAuthUrls(sdk); await exchangeCodeForTokens(sdk, tokenParams); expect(postToTokenEndpoint).toHaveBeenCalledWith(sdk, testParams, urls); @@ -62,7 +68,8 @@ describe('exchangeCodeForTokens', () => { redirectUri: testParams.redirectUri, responseType: ['token', 'id_token'], scopes: ['openid', 'email'], - acrValues + acrValues, + extraParams: tokenParams.extraParams }, oauthResponse, urls); }); diff --git a/test/spec/oidc/parseFromUrl.ts b/test/spec/oidc/parseFromUrl.ts index 60dd3d341..65c76995f 100644 --- a/test/spec/oidc/parseFromUrl.ts +++ b/test/spec/oidc/parseFromUrl.ts @@ -51,7 +51,8 @@ describe('token.parseFromUrl', function() { oktaAuthArgs: { pkce: false, responseMode: 'query', - responseType: ['code'] + responseType: ['code'], + extraParams: { foo: 'bar' } }, searchMock: '?code=fake' + '&state=' + oauthUtil.mockedState, @@ -65,7 +66,8 @@ describe('token.parseFromUrl', function() { tokenUrl: 'https://auth-js-test.okta.com/oauth2/v1/token', authorizeUrl: 'https://auth-js-test.okta.com/oauth2/v1/authorize', userinfoUrl: 'https://auth-js-test.okta.com/oauth2/v1/userinfo' - } + }, + extraParams: { foo: 'bar' } }, expectedResp: { code: 'fake', diff --git a/test/spec/oidc/renewTokens.ts b/test/spec/oidc/renewTokens.ts index 985e51fec..5ae4fc5eb 100644 --- a/test/spec/oidc/renewTokens.ts +++ b/test/spec/oidc/renewTokens.ts @@ -293,6 +293,23 @@ describe('token.renewTokens', function() { }); }); + it('extraParams', async () => { + const { sdk, accessToken, idToken, getWihoutPromptResponse, withoutPromptSpy } = testContext; + accessToken.extraParams = { foo: 'bar' }; + const tokens = { fake: true }; + getWihoutPromptResponse.tokens = tokens; + const res = await renewTokens(sdk, {}); + expect(res).toBe(tokens); + expect(withoutPromptSpy).toHaveBeenCalledWith(sdk, { + authorizeUrl: accessToken.authorizeUrl, + issuer: idToken.issuer, + responseType: ['token', 'id_token'], + scopes: accessToken.scopes, + userinfoUrl: accessToken.userinfoUrl, + extraParams: { foo: 'bar' } + }); + }); + }); }); diff --git a/test/spec/oidc/util/handleOAuthResponse.ts b/test/spec/oidc/util/handleOAuthResponse.ts index 6d6837796..c41774b8d 100644 --- a/test/spec/oidc/util/handleOAuthResponse.ts +++ b/test/spec/oidc/util/handleOAuthResponse.ts @@ -59,16 +59,23 @@ describe('handleOAuthResponse', () => { expect(res.tokens.refreshToken!.refreshToken).toBe('foo'); }); it('returns all tokens from the response', async () => { - const tokenParams: TokenParams = { responseType: ['token', 'id_token', 'refresh_token'], dpop: true }; + const tokenParams: TokenParams = { + responseType: ['token', 'id_token', 'refresh_token'], + dpop: true, + extraParams: { foo: 'bar' } + }; const oauthRes = { id_token: 'foo', access_token: 'blar', refresh_token: 'bloo', token_type: 'DPoP' }; const res = await handleOAuthResponse(sdk, tokenParams, oauthRes, undefined as unknown as CustomUrls); expect(res.tokens).toBeTruthy(); expect(res.tokens.accessToken).toBeTruthy(); expect(res.tokens.accessToken!.accessToken).toBe('blar'); + expect(res.tokens.accessToken!.extraParams).toEqual({ foo: 'bar' }); expect(res.tokens.idToken).toBeTruthy(); expect(res.tokens.idToken!.idToken).toBe('foo'); + expect(res.tokens.idToken!.extraParams).toEqual({ foo: 'bar' }); expect(res.tokens.refreshToken).toBeTruthy(); expect(res.tokens.refreshToken!.refreshToken).toBe('bloo'); + expect(res.tokens.refreshToken!.extraParams).toEqual({ foo: 'bar' }); }); it('prefers "scope" value from endpoint response over method parameter', async () => { const tokenParams: TokenParams = { responseType: ['token', 'id_token', 'refresh_token'], scopes: ['profile'] }; @@ -78,7 +85,6 @@ describe('handleOAuthResponse', () => { expect(res.tokens.idToken!.scopes).toEqual(['openid', 'offline_access']); expect(res.tokens.refreshToken!.scopes).toEqual(['openid', 'offline_access']); }); - describe('errors', () => { it('does not throw if response contains only "error" without "error_description"', async () => { let errorThrown = false;