Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/7.7' into jp-backport-7-7-1
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredperreault-okta committed Aug 20, 2024
2 parents 2f22636 + 45ea58f commit 0277ce1
Show file tree
Hide file tree
Showing 18 changed files with 117 additions and 22 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 6 additions & 1 deletion lib/oidc/endpoints/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
2 changes: 2 additions & 0 deletions lib/oidc/exchangeCodeForTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export async function exchangeCodeForTokens(sdk: OktaAuthOAuthInterface, tokenPa
acrValues,
dpop,
dpopPairId,
extraParams
} = tokenParams;

// postToTokenEndpoint() params
Expand Down Expand Up @@ -64,6 +65,7 @@ export async function exchangeCodeForTokens(sdk: OktaAuthOAuthInterface, tokenPa
responseType,
ignoreSignature,
acrValues,
extraParams
};

try {
Expand Down
13 changes: 13 additions & 0 deletions lib/oidc/handleOAuthResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export async function handleOAuthResponse(
): Promise<TokenResponse> {
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)) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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!,
Expand Down
5 changes: 3 additions & 2 deletions lib/oidc/renewToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion lib/oidc/renewTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ export async function renewTokens(sdk, options?: RenewTokensParams): Promise<Tok
const userinfoUrl = accessToken.userinfoUrl || sdk.options.userinfoUrl;
const issuer = idToken.issuer || sdk.options.issuer;
const dpopPairId = accessToken?.dpopPairId;
const extraParams = accessToken?.extraParams || idToken?.extraParams;

// Get tokens using the SSO cookie
options = Object.assign({
scopes,
authorizeUrl,
userinfoUrl,
issuer,
dpopPairId
dpopPairId,
extraParams
}, options);

if (sdk.options.pkce) {
Expand Down
6 changes: 6 additions & 0 deletions lib/oidc/renewTokensWithRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { TokenEndpointParams, postRefreshToken } from './endpoints/token';
import { findKeyPair } from './dpop';
import { isRefreshTokenInvalidError } from './util/errors';

/* eslint complexity:[0,8] */
export async function renewTokensWithRefresh(
sdk: OktaAuthOAuthInterface,
tokenParams: TokenParams,
Expand All @@ -31,6 +32,11 @@ export async function renewTokensWithRefresh(

try {
const renewTokenParams: TokenParams = Object.assign({}, tokenParams, { clientId });

if (refreshTokenObject.extraParams) {
renewTokenParams.extraParams = refreshTokenObject.extraParams;
}

const endpointParams: TokenEndpointParams = {...renewTokenParams};

if (dpop) {
Expand Down
1 change: 1 addition & 0 deletions lib/oidc/types/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface AbstractToken {
authorizeUrl: string;
scopes: string[];
pendingRemove?: boolean;
extraParams?: Record<string, string>;
}

export interface AccessToken extends AbstractToken {
Expand Down
3 changes: 2 additions & 1 deletion lib/oidc/types/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export interface OAuthTransactionMeta extends
'ignoreSignature' |
'nonce' |
'acrValues' |
'enrollAmrValues'
'enrollAmrValues' |
'extraParams'
>
{
urls: CustomUrls;
Expand Down
1 change: 1 addition & 0 deletions lib/oidc/util/oauthMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function createOAuthMeta(
nonce: tokenParams.nonce!,
ignoreSignature: tokenParams.ignoreSignature!,
acrValues: tokenParams.acrValues,
extraParams: tokenParams.extraParams
};

if (tokenParams.pkce === false) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 8 additions & 8 deletions samples/test/features/self-service-registration.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions test/apps/app/src/testApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
28 changes: 26 additions & 2 deletions test/spec/oidc/endpoints/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -80,7 +81,7 @@ describe('token endpoint', function() {
clientId: CLIENT_ID,
redirectUri: REDIRECT_URI,
authorizationCode: authorizationCode,
codeVerifier: codeVerifier,
codeVerifier: codeVerifier
}, {
tokenUrl: ISSUER + endpoint
});
Expand Down Expand Up @@ -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 = {};

Expand Down
11 changes: 9 additions & 2 deletions test/spec/oidc/exchangeCodeForTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -62,7 +68,8 @@ describe('exchangeCodeForTokens', () => {
redirectUri: testParams.redirectUri,
responseType: ['token', 'id_token'],
scopes: ['openid', 'email'],
acrValues
acrValues,
extraParams: tokenParams.extraParams
}, oauthResponse, urls);
});

Expand Down
6 changes: 4 additions & 2 deletions test/spec/oidc/parseFromUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand Down
17 changes: 17 additions & 0 deletions test/spec/oidc/renewTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
});
});

});

});
10 changes: 8 additions & 2 deletions test/spec/oidc/util/handleOAuthResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] };
Expand All @@ -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;
Expand Down

0 comments on commit 0277ce1

Please sign in to comment.