diff --git a/.eslintignore b/.eslintignore index 57e8af8ba..c05a53ab9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,3 +6,4 @@ node_modules /build/types /build/cjs /samples/templates +/samples/generated/webpack-spa/public/*-bundle.* diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aaf4ce04..58ba8b88c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 4.5.0 + +### Features + +- [#567](https://github.com/okta/okta-auth-js/pull/567) Adds new methods: + - `token.prepareTokenParams` + - `token.exchangeCodeForTokens` + - `pkce.generateVerifier` + - `pkce.computeChallenge` + and constant: + - `pkce.DEFAULT_CODE_CHALLENGE_METHOD` + This API allows more control over the `PKCE` authorization flow and is enabled for both browser and nodeJS. + ## 4.4.0 ### Features diff --git a/README.md b/README.md index d68e02f19..b2b8556bb 100644 --- a/README.md +++ b/README.md @@ -624,7 +624,7 @@ var config = { ``` ## API Reference - + * [signIn](#signinoptions) * [signInWithCredentials](#signinwithcredentialsoptions) * [signInWithRedirect](#signinwithredirectoptions) @@ -676,6 +676,8 @@ var config = { * [token.getUserInfo](#tokengetuserinfoaccesstokenobject-idtokenobject) * [token.verify](#tokenverifyidtokenobject) * [token.isLoginRedirect](#tokenisloginredirect) + * [token.prepareTokenParams](#tokenpreparetokenparams) + * [token.exchangeCodeForTokens](#tokenexchangecodefortokens) * [tokenManager](#tokenmanager) * [tokenManager.add](#tokenmanageraddkey-token) * [tokenManager.get](#tokenmanagergetkey) @@ -2210,6 +2212,15 @@ authClient.token.verify(idTokenObject, validationOptions) > :warning: Deprecated, this method will be removed in next major release, use [sdk.isLoginRedirect](#isloginredirect) instead. + +#### `token.prepareTokenParams` + +Returns a `TokenParams` object. If `PKCE` is enabled, this object will contain values for `codeVerifier`, `codeChallenge` and `codeChallengeMethod`. + +#### `token.exchangeCodeForTokens` + +Used internally to perform the final step of the `PKCE` authorization code flow. Accepts a `TokenParams` object which should contain a `codeVerifier` and an `authorizationCode`. + ### `tokenManager` #### `tokenManager.add(key, token)` diff --git a/jest.browser.js b/jest.browser.js index fc7ac7910..10ba72dce 100644 --- a/jest.browser.js +++ b/jest.browser.js @@ -22,7 +22,8 @@ module.exports = { '^@okta/okta-auth-js$': OktaAuth }, 'setupFiles': [ - '/test/support/nodeExceptions.js' + '/test/support/nodeExceptions.js', + '/jest.setup.js' ], 'testMatch': [ '**/test/spec/*.{js,ts}' diff --git a/jest.server.js b/jest.server.js index d10b36b65..9aa305a0c 100644 --- a/jest.server.js +++ b/jest.server.js @@ -14,6 +14,9 @@ module.exports = { 'moduleNameMapper': { '^@okta/okta-auth-js$': OktaAuth }, + 'setupFiles': [ + '/jest.setup.js' + ], 'testMatch': [ '**/test/spec/*.js' ], diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 000000000..1d4f52c42 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,8 @@ +// crypto to mimic browser environment +const Crypto = require('@peculiar/webcrypto').Crypto; +global.crypto = new Crypto(); + +// TextEncoder +const TextEncoder = require('util').TextEncoder; +// eslint-disable-next-line node/no-unsupported-features/node-builtins +global.TextEncoder = TextEncoder; diff --git a/lib/OktaAuthBase.ts b/lib/OktaAuthBase.ts index 55f1bc67c..f552898ab 100644 --- a/lib/OktaAuthBase.ts +++ b/lib/OktaAuthBase.ts @@ -19,6 +19,7 @@ import { postToTransaction, AuthTransaction } from './tx'; +import PKCE from './pkce'; import { OktaAuth, OktaAuthOptions, @@ -27,7 +28,8 @@ import { VerifyRecoveryTokenOptions, TransactionAPI, SessionAPI, - SigninAPI + SigninAPI, + PkceAPI, } from './types'; export default class OktaAuthBase implements OktaAuth, SigninAPI { @@ -35,18 +37,26 @@ export default class OktaAuthBase implements OktaAuth, SigninAPI { tx: TransactionAPI; userAgent: string; session: SessionAPI; + pkce: PkceAPI; constructor(args: OktaAuthOptions) { assertValidConfig(args); this.options = { issuer: removeTrailingSlash(args.issuer), + tokenUrl: removeTrailingSlash(args.tokenUrl), httpRequestClient: args.httpRequestClient, transformErrorXHR: args.transformErrorXHR, storageUtil: args.storageUtil, headers: args.headers, - devMode: args.devMode || false + devMode: args.devMode || false, + clientId: args.clientId, + redirectUri: args.redirectUri, + pkce: args.pkce }; + // Give the developer the ability to disable token signature validation. + this.options.ignoreSignature = !!args.ignoreSignature; + this.tx = { status: transactionStatus.bind(null, this), resume: resumeTransaction.bind(null, this), @@ -58,7 +68,12 @@ export default class OktaAuthBase implements OktaAuth, SigninAPI { }), introspect: introspect.bind(null, this) }; - + + this.pkce = { + DEFAULT_CODE_CHALLENGE_METHOD: PKCE.DEFAULT_CODE_CHALLENGE_METHOD, + generateVerifier: PKCE.generateVerifier, + computeChallenge: PKCE.computeChallenge + }; } // { username, password, (relayState), (context) } diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index bb7ba7ead..457f7ddbb 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -48,7 +48,9 @@ import { renewToken, renewTokens, getUserInfo, - verifyToken + verifyToken, + prepareTokenParams, + exchangeCodeForTokens } from '../token'; import { TokenManager } from '../TokenManager'; import { @@ -132,7 +134,6 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { clientId: args.clientId, authorizeUrl: removeTrailingSlash(args.authorizeUrl), userinfoUrl: removeTrailingSlash(args.userinfoUrl), - tokenUrl: removeTrailingSlash(args.tokenUrl), revokeUrl: removeTrailingSlash(args.revokeUrl), logoutUrl: removeTrailingSlash(args.logoutUrl), pkce: args.pkce === false ? false : true, @@ -161,10 +162,6 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { } else { this.options.maxClockSkew = args.maxClockSkew; } - - // Give the developer the ability to disable token signature - // validation. - this.options.ignoreSignature = !!args.ignoreSignature; this.session = { close: closeSession.bind(null, this), @@ -176,6 +173,8 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { this._tokenQueue = new PromiseQueue(); this.token = { + prepareTokenParams: prepareTokenParams.bind(null, this), + exchangeCodeForTokens: exchangeCodeForTokens.bind(null, this), getWithoutPrompt: getWithoutPrompt.bind(null, this), getWithPopup: getWithPopup.bind(null, this), getWithRedirect: getWithRedirect.bind(null, this), diff --git a/lib/constants.ts b/lib/constants.ts index f914aa444..31fd7dd8d 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -24,3 +24,9 @@ export const ACCESS_TOKEN_STORAGE_KEY = 'accessToken'; export const ID_TOKEN_STORAGE_KEY = 'idToken'; export const REFRESH_TOKEN_STORAGE_KEY = 'refreshToken'; export const REFERRER_PATH_STORAGE_KEY = 'referrerPath'; + +// Code verifier: Random URL-safe string with a minimum length of 43 characters. +// Code challenge: Base64 URL-encoded SHA-256 hash of the code verifier. +export const MIN_VERIFIER_LENGTH = 43; +export const MAX_VERIFIER_LENGTH = 128; +export const DEFAULT_CODE_CHALLENGE_METHOD = 'S256'; \ No newline at end of file diff --git a/lib/pkce.ts b/lib/pkce.ts index 872cf477c..39ad417b8 100644 --- a/lib/pkce.ts +++ b/lib/pkce.ts @@ -15,13 +15,8 @@ import AuthSdkError from './errors/AuthSdkError'; import http from './http'; import { warn, stringToBase64Url, removeNils, toQueryString } from './util'; -import { TokenParams, CustomUrls, PKCEMeta, OAuthResponse } from './types'; - -// Code verifier: Random URL-safe string with a minimum length of 43 characters. -// Code challenge: Base64 URL-encoded SHA-256 hash of the code verifier. -var MIN_VERIFIER_LENGTH = 43; -var MAX_VERIFIER_LENGTH = 128; -var DEFAULT_CODE_CHALLENGE_METHOD = 'S256'; +import { TokenParams, CustomUrls, PKCEMeta, OAuthResponse, OAuthParams } from './types'; +import { MIN_VERIFIER_LENGTH, MAX_VERIFIER_LENGTH, DEFAULT_CODE_CHALLENGE_METHOD } from './constants'; function dec2hex (dec) { return ('0' + dec.toString(16)).substr(-2); @@ -34,7 +29,7 @@ function getRandomString(length) { return str.slice(0, length); } -function generateVerifier(prefix) { +function generateVerifier(prefix?: string): string { var verifier = prefix || ''; if (verifier.length < MIN_VERIFIER_LENGTH) { verifier = verifier + getRandomString(MIN_VERIFIER_LENGTH - verifier.length); @@ -99,7 +94,7 @@ function clearMeta(sdk) { storage.clearStorage(); } -function computeChallenge(str) { +function computeChallenge(str: string): PromiseLike { var buffer = new TextEncoder().encode(str); return crypto.subtle.digest('SHA-256', buffer).then(function(arrayBuffer) { var hash = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)); @@ -119,7 +114,7 @@ function validateOptions(options: TokenParams) { throw new AuthSdkError('The redirectUri passed to /authorize must also be passed to /token'); } - if (!options.authorizationCode) { + if (!options.authorizationCode && !options.interactionCode) { throw new AuthSdkError('An authorization code (returned from /authorize) must be passed to /token'); } @@ -129,20 +124,26 @@ function validateOptions(options: TokenParams) { } function getPostData(options: TokenParams): string { - // Convert options to OAuth params - var params = removeNils({ + // Convert Token params to OAuth params, sent to the /token endpoint + var params: OAuthParams = removeNils({ 'client_id': options.clientId, 'redirect_uri': options.redirectUri, - 'grant_type': 'authorization_code', - 'code': options.authorizationCode, + 'grant_type': options.interactionCode ? 'interaction_code' : 'authorization_code', 'code_verifier': options.codeVerifier }); + + if (options.interactionCode) { + params['interaction_code'] = options.interactionCode; + } else if (options.authorizationCode) { + params.code = options.authorizationCode; + } + // Encode as URL string return toQueryString(params).slice(1); } // exchange authorization code for an access token -function getToken(sdk, options: TokenParams, urls: CustomUrls): Promise { +function exchangeCodeForTokens(sdk, options: TokenParams, urls: CustomUrls): Promise { validateOptions(options); var data = getPostData(options); @@ -158,11 +159,11 @@ function getToken(sdk, options: TokenParams, urls: CustomUrls): Promise { + if (!sdk.options.pkce) { + return Promise.reject( + new AuthSdkError('"pkce.exchangeCodeForTokens" method requires "pkce" SDK option to be true.') + ); + } + urls = urls || getOAuthUrls(sdk, tokenParams); + // build params using defaults + options + tokenParams = Object.assign({}, getDefaultTokenParams(sdk), clone(tokenParams)); + + const { + authorizationCode, + interactionCode, + codeVerifier, + clientId, + redirectUri, + scopes, + ignoreSignature, + } = tokenParams; + + var getTokenOptions = { + clientId, + redirectUri, + authorizationCode, + interactionCode, + codeVerifier, }; - return PKCE.getToken(sdk, getTokenParams, urls) - .then(function (res) { - validateResponse(res, getTokenParams); - return res; - }) - .finally(function () { - PKCE.clearMeta(sdk); + return PKCE.exchangeCodeForTokens(sdk, getTokenOptions, urls) + .then((response: OAuthResponse) => { + // `handleOAuthResponse` hanadles responses from both `/authorize` and `/token` endpoints + // Here we modify the response from `/token` so that it more closely matches a response from `/authorize` + // `responseType` is used to validate that the expected tokens were returned + const responseType = ['token']; // an accessToken will always be returned + if (scopes.indexOf('openid') !== -1) { + responseType.push('id_token'); // an idToken will be returned if "openid" is in the scopes + } + const handleResponseOptions: TokenParams = { + clientId, + redirectUri, + scopes, + responseType, + ignoreSignature, + }; + return handleOAuthResponse(sdk, handleResponseOptions, response, urls) + .then((response: TokenResponse) => { + // For compatibility, "code" is returned in the TokenResponse. OKTA-326091 + response.code = authorizationCode; + return response; + }); }); } @@ -233,8 +267,18 @@ function validateResponse(res: OAuthResponse, oauthParams: TokenParams) { // eslint-disable-next-line max-len function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuthResponse, urls: CustomUrls): Promise { - urls = urls || {}; - tokenParams = tokenParams || {}; + var 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 (res.code && pkce) { + return exchangeCodeForTokens(sdk, Object.assign({}, tokenParams, { + authorizationCode: res.code + }), urls); + } + + tokenParams = tokenParams || getDefaultTokenParams(sdk); + urls = urls || getOAuthUrls(sdk, tokenParams); var responseType = tokenParams.responseType; if (!Array.isArray(responseType)) { @@ -243,26 +287,12 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth var scopes = clone(tokenParams.scopes); var clientId = tokenParams.clientId || sdk.options.clientId; - var pkce = sdk.options.pkce !== false; + // Handling the result from implicit flow or PKCE token exchange return Promise.resolve() .then(function () { validateResponse(res, tokenParams); - - // PKCE flow - // We do not support "hybrid" scenarios where the response includes both a code and a token. - // If the response contains a code it is used immediately to obtain new tokens. - if (res.code && pkce) { - // responseType is not sent to the token endpoint. - // We populate this array to validate the response below - responseType = ['token']; // an accessToken will always be returned - if (scopes.indexOf('openid') !== -1) { - responseType.push('id_token'); // an idToken will be returned if "openid" is in the scopes - } - return exchangeCodeForToken(sdk, tokenParams, res.code, urls); - } - return res; - }).then(function (res: OAuthResponse) { + }).then(function () { var tokenDict = {} as Tokens; var expiresIn = res.expires_in; var tokenType = res.token_type; @@ -330,7 +360,7 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth return tokenDict; }) - .then(function (tokenDict) { + .then(function (tokenDict): TokenResponse { // Validate received tokens against requested response types if (responseType.indexOf('token') !== -1 && !tokenDict.accessToken) { // eslint-disable-next-line max-len @@ -579,12 +609,6 @@ function getToken(sdk: OktaAuth, options: TokenParams) { default: throw new AuthSdkError('The full page redirect flow is not supported'); } - }) - .catch(e => { - if (sdk.options.pkce) { - PKCE.clearMeta(sdk); - } - throw e; }); } @@ -592,6 +616,12 @@ function getWithoutPrompt(sdk: OktaAuth, options: TokenParams): Promise 2) { return Promise.reject(new AuthSdkError('As of version 3.0, "getWithoutPrompt" takes only a single set of options')); } + if (isLoginRedirect(sdk)) { + return Promise.reject(new AuthSdkError( + 'The app should not attempt to call getToken on callback. ' + + 'Authorize flow is already in process. Use parseFromUrl() to receive tokens.' + )); + } options = clone(options) || {}; Object.assign(options, { prompt: 'none', @@ -605,30 +635,27 @@ function getWithPopup(sdk: OktaAuth, options: TokenParams): Promise 2) { return Promise.reject(new AuthSdkError('As of version 3.0, "getWithPopup" takes only a single set of options')); } - options = clone(options) || {}; - Object.assign(options, { - display: 'popup', - responseMode: 'okta_post_message' - }); - return getToken(sdk, options); -} - -function prepareTokenParams(sdk: OktaAuth, options: TokenParams): Promise { if (isLoginRedirect(sdk)) { return Promise.reject(new AuthSdkError( 'The app should not attempt to call getToken on callback. ' + 'Authorize flow is already in process. Use parseFromUrl() to receive tokens.' )); } - - // clone and prepare options options = clone(options) || {}; + Object.assign(options, { + display: 'popup', + responseMode: 'okta_post_message' + }); + return getToken(sdk, options); +} +// Prepares params for a call to /authorize or /token +function prepareTokenParams(sdk: OktaAuth, tokenParams: TokenParams): Promise { // build params using defaults + options - var tokenParams: TokenParams = getDefaultTokenParams(sdk); - Object.assign(tokenParams, options); + tokenParams = Object.assign({}, getDefaultTokenParams(sdk), clone(tokenParams)); if (tokenParams.pkce === false) { + // Implicit flow or authorization_code without PKCE return Promise.resolve(tokenParams); } @@ -648,7 +675,7 @@ function prepareTokenParams(sdk: OktaAuth, options: TokenParams): Promise { if (arguments.length > 2) { return Promise.reject(new AuthSdkError('As of version 3.0, "getWithRedirect" takes only a single set of options')); } + if (isLoginRedirect(sdk)) { + return Promise.reject(new AuthSdkError( + 'The app should not attempt to call getToken on callback. ' + + 'Authorize flow is already in process. Use parseFromUrl() to receive tokens.' + )); + } options = clone(options) || {}; return prepareTokenParams(sdk, options) @@ -722,6 +747,14 @@ function getWithRedirect(sdk: OktaAuth, options: TokenParams): Promise { // Set state cookie for servers to validate state cookies.set(REDIRECT_STATE_COOKIE_NAME, tokenParams.state, null, sdk.options.cookies); + if (sdk.options.pkce) { + // We will need these values after redirect when we call /token + var meta: PKCEMeta = { + codeVerifier: tokenParams.codeVerifier, + redirectUri: tokenParams.redirectUri + }; + PKCE.saveMeta(sdk, meta); + } sdk.token.getWithRedirect._setLocation(requestUrl); }); } @@ -910,7 +943,20 @@ function parseFromUrl(sdk, options: string | ParseFromUrlOptions): Promise { + if (sdk.options.pkce) { + PKCE.clearMeta(sdk); + } + }); }); } @@ -978,7 +1024,9 @@ export { getUserInfo, verifyToken, handleOAuthResponse, + getDefaultTokenParams, prepareTokenParams, + exchangeCodeForTokens, _addOAuthParamsToStorage, // export for testing purpose _getOAuthParamsStrFromStorage, // export for testing purpose }; diff --git a/lib/types/OAuth.ts b/lib/types/OAuth.ts index a35ef790e..02a17b891 100644 --- a/lib/types/OAuth.ts +++ b/lib/types/OAuth.ts @@ -12,22 +12,25 @@ */ export interface OAuthParams { - client_id: string; - code_challenge: string; - code_challenge_method: string; - display: string; - idp: string; - idp_scope: string | string[]; - login_hint: string; - max_age: string | number; - nonce: string; - prompt: string; - redirect_uri: string; - response_mode: string; - response_type: string | string[]; + client_id?: string; + code_challenge?: string; + code_challenge_method?: string; + display?: string; + idp?: string; + idp_scope?: string | string[]; + login_hint?: string; + max_age?: string | number; + nonce?: string; + prompt?: string; + redirect_uri?: string; + response_mode?: string; + response_type?: string | string[]; scope?: string; - sessionToken: string; - state: string; + sessionToken?: string; + state?: string; + grant_type?: string; + code?: string; + interaction_code?: string; } export interface OAuthResponse { diff --git a/lib/types/api.ts b/lib/types/api.ts index 26be2bf45..6bdcf1c78 100644 --- a/lib/types/api.ts +++ b/lib/types/api.ts @@ -85,6 +85,8 @@ export interface TokenParams extends CustomUrls { codeVerifier?: string; authorizationCode?: string; codeChallenge?: string; + grantType?: string; + interactionCode?: string; idp?: string; idpScope?: string | string[]; loginHint?: string; @@ -104,6 +106,7 @@ export interface Tokens { export interface TokenResponse { tokens: Tokens; state: string; + code?: string; } export interface ParseFromUrlOptions { @@ -125,13 +128,18 @@ export interface GetWithRedirectAPI extends GetWithRedirectFunction { _setLocation: (loc: string) => void; } -export interface TokenAPI { +export interface BaseTokenAPI { + decode(token: string): JWTObject; + prepareTokenParams(params: TokenParams): Promise; + exchangeCodeForTokens(params: TokenParams, urls?: CustomUrls): Promise; +} + +export interface TokenAPI extends BaseTokenAPI { getUserInfo(accessToken?: AccessToken, idToken?: IDToken): Promise; getWithRedirect: GetWithRedirectAPI; parseFromUrl: ParseFromUrlInterface; getWithoutPrompt(params?: TokenParams): Promise; getWithPopup(params?: TokenParams): Promise; - decode(token: string): JWTObject; revoke(token: RevocableToken): Promise; renew(token: Token): Promise; renewTokens(): Promise; @@ -202,3 +210,9 @@ export interface ForgotPasswordOptions { export interface VerifyRecoveryTokenOptions { recoveryToken: string; } + +export interface PkceAPI { + DEFAULT_CODE_CHALLENGE_METHOD: string; + generateVerifier(prefix: string): string; + computeChallenge(str: string): PromiseLike; +} \ No newline at end of file diff --git a/package.json b/package.json index c001672d2..e44fc90d3 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "@babel/plugin-transform-typescript": "^7.10.5", "@babel/preset-env": "^7.8.2", "@babel/preset-typescript": "^7.10.4", + "@peculiar/webcrypto": "^1.1.4", "@types/jest": "^25.2.3", "@types/node": "^14.0.3", "@typescript-eslint/eslint-plugin": "^2.34.0", @@ -139,4 +140,4 @@ "samples/test", "samples/generated/*" ] -} \ No newline at end of file +} diff --git a/samples/generated/static-spa/public/index.html b/samples/generated/static-spa/public/index.html index 775e040ab..dc50d1164 100644 --- a/samples/generated/static-spa/public/index.html +++ b/samples/generated/static-spa/public/index.html @@ -1,8 +1,8 @@ - - + + diff --git a/samples/test/package.json b/samples/test/package.json index 59efe73b1..d5752ab85 100644 --- a/samples/test/package.json +++ b/samples/test/package.json @@ -20,7 +20,7 @@ "@wdio/mocha-framework": "^6.4.0", "@wdio/sauce-service": "^6.4.0", "@wdio/spec-reporter": "^5.15.1", - "chromedriver": "^85.0.0", + "chromedriver": "^87.0.0", "wait-on": "^3.3.0", "wdio-chromedriver-service": "^6.0.3", "webpack": "^3.0.0" diff --git a/scripts/lint.sh b/scripts/lint.sh index 6d6398783..1d6e60824 100644 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,8 +5,8 @@ source ${OKTA_HOME}/${REPO}/scripts/setup.sh export TEST_SUITE_TYPE="checkstyle" export TEST_RESULT_FILE_DIR="${REPO}/build2" -# CJS code must be built for lint to pass -yarn build:server +# build modules and samples +yarn build if ! yarn lint:report; then echo "lint failed! Exiting..." diff --git a/test/e2e/package.json b/test/e2e/package.json index 8857f86fc..0662a81a0 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -22,7 +22,7 @@ "@wdio/sauce-service": "^6.6.0", "@wdio/selenium-standalone-service": "^6.6.5", "@wdio/spec-reporter": "^6.6.0", - "chromedriver": "^85.0.0", + "chromedriver": "^87.0.0", "wait-on": "^3.3.0", "wdio-chromedriver-service": "^6.0.4", "webpack": "^3.0.0" diff --git a/test/karma/spec/loginFlow.js b/test/karma/spec/loginFlow.js index 9c91d2649..d9efd1f0b 100644 --- a/test/karma/spec/loginFlow.js +++ b/test/karma/spec/loginFlow.js @@ -19,7 +19,10 @@ describe('Complete login flow', function() { clientId: CLIENT_ID, redirectUri: REDIRECT_URI, scopes: ['openid', 'email'], - responseType: ['id_token', 'token'] + responseType: ['id_token', 'token'], + tokenManager: { + autoRenew: false + } }; const ACCESS_TOKEN = tokens.standardAccessToken; diff --git a/test/karma/spec/renewToken.js b/test/karma/spec/renewToken.js index c741e11f3..fd1b36205 100644 --- a/test/karma/spec/renewToken.js +++ b/test/karma/spec/renewToken.js @@ -17,6 +17,9 @@ describe('Renew token', function() { issuer: ISSUER, clientId: CLIENT_ID, redirectUri: REDIRECT_URI, + tokenManager: { + autoRenew: false + } }; const ACCESS_TOKEN_STR = tokens.standardAccessToken; diff --git a/test/spec/api-base-token.js b/test/spec/api-base-token.js new file mode 100644 index 000000000..125c65e09 --- /dev/null +++ b/test/spec/api-base-token.js @@ -0,0 +1,167 @@ +/* global USER_AGENT */ +jest.mock('cross-fetch'); + +import fetch from 'cross-fetch'; +import util from '@okta/test.support/util'; +import { OktaAuth } from '@okta/okta-auth-js'; +import tokens from '@okta/test.support/tokens'; +import oauthUtil from '@okta/test.support/oauthUtil'; + +const _ = require('lodash'); + +describe('base token API', function() { + + function createOktaAuth(options = {}) { + return new OktaAuth(Object.assign({ + issuer: tokens.ISSUER + }, options)); + } + describe('prepareTokenParams', function() { + let oktaAuth; + + beforeEach(() => { + util.warpToUnixTime(oauthUtil.getTime()); + }); + + describe('pkce', () => { + beforeEach(() => { + oktaAuth = createOktaAuth({ pkce: true }); + oauthUtil.loadWellKnownAndKeysCache(oktaAuth); + }); + it('can be called with no parameters', async () => { + const params = await oktaAuth.token.prepareTokenParams(); + expect(params).toEqual({ + 'pkce': true, + 'redirectUri': 'http://localhost/', + 'responseType': 'code', + 'state': expect.any(String), + 'nonce': expect.any(String), + 'scopes': [ + 'openid', + 'email' + ], + 'codeChallengeMethod': 'S256', + 'codeVerifier': expect.any(String), + 'codeChallenge': expect.any(String), + 'ignoreSignature': false + }); + }); + }); + describe('not pkce', () => { + beforeEach(() => { + oktaAuth = createOktaAuth({ pkce: false }); + oauthUtil.loadWellKnownAndKeysCache(oktaAuth); + }); + it('can be called with no parameters', async () => { + const params = await oktaAuth.token.prepareTokenParams(); + expect(params).toEqual({ + 'ignoreSignature': false, + 'pkce': false, + 'redirectUri': 'http://localhost/', + 'responseType': [ + 'token', + 'id_token' + ], + 'state': expect.any(String), + 'nonce': expect.any(String), + 'scopes': [ + 'openid', + 'email' + ] + }); + }); + }); + + }); + + describe('exchangeCodeForTokens', function() { + var ISSUER = tokens.ISSUER; + var REDIRECT_URI = 'http://fake.local'; + var CLIENT_ID = tokens.standardIdTokenParsed.clientId; + var endpoint = '/oauth2/v1/token'; + var codeVerifier = 'superfake'; + var authorizationCode = 'notreal'; + var interactionCode = 'definitelynotreal'; + var setup = { + issuer: ISSUER, + clientId: CLIENT_ID, + redirectUri: REDIRECT_URI, + pkce: true, + calls: [ + { + request: { + method: 'post', + uri: endpoint, + withCredentials: false, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Okta-User-Agent-Extended': USER_AGENT + } + }, + response: 'pkce-token-success', + responseVars: { + scope: 'ignored in this test', + accessToken: tokens.standardAccessToken, + idToken: tokens.standardIdToken + } + } + ] + }; + + beforeEach(() => { + util.warpToUnixTime(oauthUtil.getTime()); + }); + + afterEach(() => { + fetch.mockReset(); + }); + + util.itMakesCorrectRequestResponse({ + title: 'requests a token using authorizationCode', + setup: _.cloneDeep(setup), + execute: function (test) { + return test.oa.token.exchangeCodeForTokens({ + authorizationCode, + codeVerifier, + }); + }, + expectations: function () { + expect(fetch).toHaveBeenCalledTimes(1); + const args = fetch.mock.calls[0][1]; + const params = util.parseQueryParams(args.body); // decode form body + expect(params).toEqual( { + 'client_id': CLIENT_ID, + 'redirect_uri': REDIRECT_URI, + 'grant_type': 'authorization_code', + 'code_verifier': 'superfake', + 'code': 'notreal' + }); + } + }); + + util.itMakesCorrectRequestResponse({ + title: 'requests a token using interactionCode', + setup: _.cloneDeep(setup), + execute: function (test) { + oauthUtil.loadWellKnownAndKeysCache(test.oa); + return test.oa.token.exchangeCodeForTokens({ + interactionCode, + codeVerifier, + }); + }, + expectations: function () { + expect(fetch).toHaveBeenCalledTimes(1); + const args = fetch.mock.calls[0][1]; + const params = util.parseQueryParams(args.body); // decode form body + expect(params).toEqual( { + 'client_id': CLIENT_ID, + 'redirect_uri': REDIRECT_URI, + 'grant_type': 'interaction_code', + 'code_verifier': 'superfake', + 'interaction_code': 'definitelynotreal' + }); + } + }); + }); +}); \ No newline at end of file diff --git a/test/spec/api-pkce.js b/test/spec/api-pkce.js new file mode 100644 index 000000000..5815205d4 --- /dev/null +++ b/test/spec/api-pkce.js @@ -0,0 +1,30 @@ +import { DEFAULT_CODE_CHALLENGE_METHOD, OktaAuth } from '@okta/okta-auth-js'; +import pkce from '../../lib/pkce'; + +describe('pkce API', function() { + let oktaAuth; + beforeEach(() => { + oktaAuth = new OktaAuth({ + issuer: 'http://fakey' + }); + }); + + describe('DEFAULT_CODE_CHALLENGE_METHOD', () => { + it('has DEFAULT_CODE_CHALLENGE_METHOD defined', () => { + expect(oktaAuth.pkce.DEFAULT_CODE_CHALLENGE_METHOD).toBe(DEFAULT_CODE_CHALLENGE_METHOD); + }); + }); + describe('generateVerifier', () => { + it('method exists and calls pkce.generateVerifier', () => { + expect(typeof oktaAuth.pkce.generateVerifier).toBe('function'); + expect(oktaAuth.pkce.generateVerifier).toBe(pkce.generateVerifier); + }); + }); + + describe('computeChallenge', function() { + it('method exists and calls pkce.computeChallenge', async () => { + expect(typeof oktaAuth.pkce.computeChallenge).toBe('function'); + expect(oktaAuth.pkce.computeChallenge).toBe(pkce.computeChallenge); + }); + }); +}); \ No newline at end of file diff --git a/test/spec/oauthUtil.js b/test/spec/oauthUtil.js index f90679a66..52f578bbe 100644 --- a/test/spec/oauthUtil.js +++ b/test/spec/oauthUtil.js @@ -367,7 +367,7 @@ describe('getKey', function() { time: 1449699929 }, execute: function(test) { - oauthUtilHelpers.loadWellKnownAndKeysCache(); + oauthUtilHelpers.loadWellKnownAndKeysCache(test.oa); return oauthUtil.getKey(test.oa, null, 'U5R8cHbGw445Qbq8zVO1PcCpXL8yG6IcovVa3laCoxM'); }, expectations: function(test, key) { diff --git a/test/spec/pkce.js b/test/spec/pkce.js index 3e02bc0b8..6e2725789 100644 --- a/test/spec/pkce.js +++ b/test/spec/pkce.js @@ -97,8 +97,7 @@ describe('pkce', function() { expect(e.name).toEqual('AuthSdkError'); expect(e.errorSummary).toEqual( 'PKCE requires a modern browser with encryption support running in a secure context.\n' + - 'The current page is not being served with HTTPS protocol. PKCE requires secure HTTPS protocol.\n' + - '"TextEncoder" is not defined. To use PKCE, you may need to include a polyfill/shim for this browser.' + 'The current page is not being served with HTTPS protocol. PKCE requires secure HTTPS protocol.' ); }); }); @@ -164,7 +163,7 @@ describe('pkce', function() { }); - describe('getToken', function() { + describe('exchangeCodeForTokens', function() { var ISSUER = 'http://example.okta.com'; var REDIRECT_URI = 'http://fake.local'; var CLIENT_ID = 'fake'; @@ -207,7 +206,7 @@ describe('pkce', function() { ] }, execute: function (test) { - return pkce.getToken(test.oa, { + return pkce.exchangeCodeForTokens(test.oa, { clientId: CLIENT_ID, redirectUri: REDIRECT_URI, authorizationCode: authorizationCode, @@ -241,14 +240,14 @@ describe('pkce', function() { var urls = { tokenUrl: 'http://superfake' }; - pkce.getToken(authClient, oauthOptions, urls); + pkce.exchangeCodeForTokens(authClient, oauthOptions, urls); expect(httpRequst).toHaveBeenCalled(); }); it('Throws if no clientId', function() { oauthOptions.clientId = undefined; try { - pkce.getToken(authClient, oauthOptions); + pkce.exchangeCodeForTokens(authClient, oauthOptions); } catch(e) { expect(e instanceof AuthSdkError).toBe(true); expect(e.message).toBe('A clientId must be specified in the OktaAuth constructor to get a token'); @@ -258,7 +257,7 @@ describe('pkce', function() { it('Throws if no redirectUri', function() { oauthOptions.redirectUri = undefined; try { - pkce.getToken(authClient, oauthOptions); + pkce.exchangeCodeForTokens(authClient, oauthOptions); } catch(e) { expect(e instanceof AuthSdkError).toBe(true); expect(e.message).toBe('The redirectUri passed to /authorize must also be passed to /token'); @@ -268,7 +267,7 @@ describe('pkce', function() { it('Throws if no authorizationCode', function() { oauthOptions.authorizationCode = undefined; try { - pkce.getToken(authClient, oauthOptions); + pkce.exchangeCodeForTokens(authClient, oauthOptions); } catch(e) { expect(e instanceof AuthSdkError).toBe(true); expect(e.message).toBe('An authorization code (returned from /authorize) must be passed to /token'); @@ -278,7 +277,7 @@ describe('pkce', function() { it('Throws if no codeVerifier', function() { oauthOptions.codeVerifier = undefined; try { - pkce.getToken(authClient, oauthOptions); + pkce.exchangeCodeForTokens(authClient, oauthOptions); } catch(e) { expect(e instanceof AuthSdkError).toBe(true); expect(e.message).toBe('The "codeVerifier" (generated and saved by your app) must be passed to /token'); diff --git a/test/spec/token.ts b/test/spec/token.ts index 49d8613b7..278fc96b2 100644 --- a/test/spec/token.ts +++ b/test/spec/token.ts @@ -176,7 +176,7 @@ describe('token.getWithoutPrompt', function() { messageCallbacks = []; // Mock the well-known and keys request - oauthUtil.loadWellKnownAndKeysCache(); + oauthUtil.loadWellKnownAndKeysCache(authClient); oauthUtil.mockStateAndNonce(); util.warpToUnixTime(oauthUtil.getTime()); @@ -2252,7 +2252,7 @@ describe('token.parseFromUrl', function() { redirectUri }); spyOn(pkce, 'clearMeta'); - spyOn(pkce, 'getToken').and.returnValue(Promise.resolve(response)); + spyOn(pkce, 'exchangeCodeForTokens').and.returnValue(Promise.resolve(response)); } it('authorization_code: Will return code', function() { @@ -3339,7 +3339,7 @@ describe('token.verify', function() { it('verifies idToken at_hash claim against accessToken', () => { util.warpToUnixTime(1449699929); - oauthUtil.loadWellKnownAndKeysCache(); + oauthUtil.loadWellKnownAndKeysCache(client); validationParams.accessToken = tokens.standardAccessToken; return client.token.verify(idToken, validationParams) .then(function(res) { @@ -3350,7 +3350,7 @@ describe('token.verify', function() { it('throws if idToken at_hash claim does not match accessToken', () => { util.warpToUnixTime(1449699929); - oauthUtil.loadWellKnownAndKeysCache(); + oauthUtil.loadWellKnownAndKeysCache(client); validationParams.accessToken = tokens.standardAccessToken; idToken.claims.at_hash = 'other_hash'; return client.token.verify(idToken, validationParams) @@ -3364,7 +3364,7 @@ describe('token.verify', function() { it('skips verification if idToken does not have at_hash claim', () => { util.warpToUnixTime(1449699929); - oauthUtil.loadWellKnownAndKeysCache(); + oauthUtil.loadWellKnownAndKeysCache(client); validationParams.accessToken = tokens.standardAccessToken; delete idToken.claims.at_hash; return client.token.verify(idToken, validationParams) @@ -3377,7 +3377,7 @@ describe('token.verify', function() { it('verifies a valid idToken with nonce', function() { util.warpToUnixTime(1449699929); - oauthUtil.loadWellKnownAndKeysCache(); + oauthUtil.loadWellKnownAndKeysCache(client); validationParams.nonce = tokens.standardIdTokenParsed.nonce; return client.token.verify(tokens.standardIdTokenParsed, validationParams) .then(function(res) { @@ -3386,7 +3386,7 @@ describe('token.verify', function() { }); it('verifies a valid idToken without nonce or accessToken', function() { util.warpToUnixTime(1449699929); - oauthUtil.loadWellKnownAndKeysCache(); + oauthUtil.loadWellKnownAndKeysCache(client); return client.token.verify(tokens.standardIdTokenParsed, validationParams) .then(function(res) { expect(res).toEqual(tokens.standardIdTokenParsed); @@ -3394,11 +3394,11 @@ describe('token.verify', function() { }); it('validationParams are optional', () => { util.warpToUnixTime(1449699929); - oauthUtil.loadWellKnownAndKeysCache(); client = setupSync({ issuer: tokens.standardIdTokenParsed.issuer, clientId: tokens.standardIdTokenParsed.clientId, }); + oauthUtil.loadWellKnownAndKeysCache(client); return client.token.verify(tokens.standardIdTokenParsed, undefined) .then(function(res) { expect(res).toEqual(tokens.standardIdTokenParsed); diff --git a/test/support/oauthUtil.js b/test/support/oauthUtil.js index 5837144be..f117ecbb5 100644 --- a/test/support/oauthUtil.js +++ b/test/support/oauthUtil.js @@ -52,10 +52,12 @@ oauthUtil.loadWellKnownCache = function() { })); }; -oauthUtil.loadWellKnownAndKeysCache = function() { +oauthUtil.loadWellKnownAndKeysCache = function(authClient) { // add /.well-known/openid-configuration and /oauth2/v1/keys to cache // so we don't make unnecessary requests - localStorage.setItem('okta-cache-storage', JSON.stringify({ + + const httpCache = authClient.options.storageUtil.getHttpCache(); + httpCache.setStorage({ 'https://auth-js-test.okta.com/.well-known/openid-configuration': { expiresAt: 1449786329, response: wellKnown.response @@ -72,7 +74,7 @@ oauthUtil.loadWellKnownAndKeysCache = function() { expiresAt: 1449786329, response: keys.response } - })); + }); }; var defaultPostMessage = oauthUtil.defaultPostMessage = { @@ -188,7 +190,7 @@ oauthUtil.setup = function(opts) { util.warpToUnixTime(getTime(opts.time)); // Mock the well-known and keys request - oauthUtil.loadWellKnownAndKeysCache(); + oauthUtil.loadWellKnownAndKeysCache(authClient); if (opts.tokenManagerAddKeys) { for (var key in opts.tokenManagerAddKeys) { @@ -398,7 +400,7 @@ oauthUtil.setupRedirect = function(opts) { }, opts.oktaAuthArgs)); // Mock the well-known and keys request - oauthUtil.loadWellKnownAndKeysCache(); + oauthUtil.loadWellKnownAndKeysCache(client); oauthUtil.mockStateAndNonce(); var windowLocationMock = util.mockSetWindowLocation(client); @@ -434,7 +436,7 @@ oauthUtil.setupParseUrl = function(opts) { }, opts.oktaAuthArgs)); // Mock the well-known and keys request - oauthUtil.loadWellKnownAndKeysCache(); + oauthUtil.loadWellKnownAndKeysCache(client); util.warpToUnixTime(getTime(opts.time)); @@ -516,7 +518,7 @@ oauthUtil.setupSimultaneousPostMessage = function() { }); // Mock the well-known and keys request - oauthUtil.loadWellKnownAndKeysCache(); + oauthUtil.loadWellKnownAndKeysCache(client); var emitter = new EventEmitter(); jest.spyOn(window, 'addEventListener').mockImplementation(function(eventName, fn) { diff --git a/test/support/tokens.js b/test/support/tokens.js index 361505d10..f04576ed4 100644 --- a/test/support/tokens.js +++ b/test/support/tokens.js @@ -2,6 +2,8 @@ var tokens = {}; +tokens.ISSUER = 'https://auth-js-test.okta.com'; // must match what is in oauthUtil + tokens.unicodeToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + 'eyAibXNnX2VuIjogIkhlbGxvIiwKICAibXNnX2pwIjogIuOBk-OCk-OBq' + '-OBoeOBryIsCiAgIm1zZ19jbiI6ICLkvaDlpb0iLAogICJtc2dfa3IiOi' + diff --git a/test/support/util.js b/test/support/util.js index bf9fa9e95..4b13fe520 100644 --- a/test/support/util.js +++ b/test/support/util.js @@ -91,7 +91,7 @@ function mockAjax(pairs) { fetch.mockImplementation(function (url, args) { var pair = allPairs.shift(); if (!pair) { - throw new Error('We are making a request that we have not anticipated.'); + throw new Error('We are making a request that we have not anticipated: ' + url); } if (pair.request.withCredentials !== false) { @@ -193,6 +193,8 @@ function setup(options) { oa = new OktaAuth({ pkce: options.pkce, issuer: options.issuer, + clientId: options.clientId, + redirectUri: options.redirectUri, transformErrorXHR: options.transformErrorXHR, headers: options.headers, ignoreSignature: options.bypassCrypto === true, diff --git a/test/support/xhr/well-known.js b/test/support/xhr/well-known.js index fc0d9614f..5dbb39c92 100644 --- a/test/support/xhr/well-known.js +++ b/test/support/xhr/well-known.js @@ -6,6 +6,7 @@ module.exports = { "authorization_endpoint": "https://auth-js-test.okta.com/oauth2/v1/authorize", "userinfo_endpoint": "https://auth-js-test.okta.com/oauth2/v1/userinfo", "jwks_uri": "https://auth-js-test.okta.com/oauth2/v1/keys", + "code_challenge_methods_supported": ["S256"], "response_types_supported": [ "id_token", "id_token token" diff --git a/yarn.lock b/yarn.lock index 0ceb683d7..6bdd17990 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1567,6 +1567,34 @@ optionalDependencies: fsevents "*" +"@peculiar/asn1-schema@^2.0.12", "@peculiar/asn1-schema@^2.0.26": + version "2.0.27" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.27.tgz#1ee3b2b869ff3200bcc8ec60e6c87bd5a6f03fe0" + integrity sha512-1tIx7iL3Ma3HtnNS93nB7nhyI0soUJypElj9owd4tpMrRDmeJ8eZubsdq1sb0KSaCs5RqZNoABCP6m5WtnlVhQ== + dependencies: + "@types/asn1js" "^2.0.0" + asn1js "^2.0.26" + pvtsutils "^1.1.1" + tslib "^2.0.3" + +"@peculiar/json-schema@^1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339" + integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w== + dependencies: + tslib "^2.0.0" + +"@peculiar/webcrypto@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.1.4.tgz#cbbe2195e5e6f879780bdac9a66bcbaca75c483c" + integrity sha512-gEVxfbseFDV0Za3AmjTrRB+wigEMOejHDzoo571e8/YWD33Ejmk0XPF3+G+VaN8+5C5IWZx4CPvxQZ7mF2dvNA== + dependencies: + "@peculiar/asn1-schema" "^2.0.26" + "@peculiar/json-schema" "^1.1.12" + pvtsutils "^1.1.1" + tslib "^2.0.3" + webcrypto-core "^1.1.8" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -1622,6 +1650,11 @@ dependencies: "@types/glob" "*" +"@types/asn1js@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/asn1js/-/asn1js-2.0.0.tgz#10ca75692575744d0117098148a8dc84cbee6682" + integrity sha512-Jjzp5EqU0hNpADctc/UqhiFbY1y2MqIxBVa2S4dBlbnZHTLPMuggoL5q43X63LpsOIINRDirBjP56DUUKIUWIA== + "@types/atob@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@types/atob/-/atob-2.1.2.tgz#157eb0cc46264a8c55f2273a836c7a1a644fb820" @@ -2962,6 +2995,13 @@ asn1@~0.2.3: dependencies: safer-buffer "~2.1.0" +asn1js@^2.0.26: + version "2.0.26" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-2.0.26.tgz#0a6d435000f556a96c6012969d9704d981b71251" + integrity sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ== + dependencies: + pvutils latest + assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" @@ -3082,12 +3122,12 @@ axios@^0.18.0: follow-redirects "1.5.10" is-buffer "^2.0.2" -axios@^0.19.2: - version "0.19.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== +axios@^0.21.0: + version "0.21.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca" + integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw== dependencies: - follow-redirects "1.5.10" + follow-redirects "^1.10.0" babel-code-frame@^6.26.0: version "6.26.0" @@ -4107,17 +4147,18 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^85.0.0: - version "85.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-85.0.0.tgz#5b9b6a184569f5e2b22b45a928bbc66d1c4bb36f" - integrity sha512-Noinnkl9gRsfC1EYA5trcOVf9r/P6JJnWf+mU6KZS3xLjV9x/o71VZ+gqRl3oSI4PnTGnqYRISZFQk/teYVTRg== +chromedriver@^87.0.0: + version "87.0.4" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-87.0.4.tgz#749f69e9427880abff19c1838258c35238397e50" + integrity sha512-kD4N/L8c0nAzh1eEAiAbEIq6Pn5TvGvckODvP5dPqF90q5tPiAJZCoWWSOUV/mrPxiodjHPfmNeOfGERHugzug== dependencies: "@testim/chrome-version" "^1.0.7" - axios "^0.19.2" - del "^5.1.0" + axios "^0.21.0" + del "^6.0.0" extract-zip "^2.0.1" https-proxy-agent "^5.0.0" mkdirp "^1.0.4" + proxy-from-env "^1.1.0" tcp-port-used "^1.0.1" ci-info@^1.5.0: @@ -5293,18 +5334,18 @@ del@^4.1.1: pify "^4.0.1" rimraf "^2.6.3" -del@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/del/-/del-5.1.0.tgz#d9487c94e367410e6eff2925ee58c0c84a75b3a7" - integrity sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA== +del@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" + integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== dependencies: - globby "^10.0.1" - graceful-fs "^4.2.2" + globby "^11.0.1" + graceful-fs "^4.2.4" is-glob "^4.0.1" is-path-cwd "^2.2.0" - is-path-inside "^3.0.1" - p-map "^3.0.0" - rimraf "^3.0.0" + is-path-inside "^3.0.2" + p-map "^4.0.0" + rimraf "^3.0.2" slash "^3.0.0" delayed-stream@~1.0.0: @@ -6511,7 +6552,7 @@ fast-glob@^2.0.2, fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.0.3: +fast-glob@^3.1.1: version "3.2.4" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== @@ -6833,6 +6874,11 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== +follow-redirects@^1.10.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" + integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -7279,18 +7325,16 @@ globals@^9.18.0: resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== -globby@^10.0.1: - version "10.0.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" - integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg== +globby@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== dependencies: - "@types/glob" "^7.1.1" array-union "^2.1.0" dir-glob "^3.0.1" - fast-glob "^3.0.3" - glob "^7.1.3" - ignore "^5.1.1" - merge2 "^1.2.3" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" slash "^3.0.0" globby@^6.1.0: @@ -7438,7 +7482,7 @@ got@^8.3.1: url-parse-lax "^3.0.0" url-to-options "^1.0.1" -graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: +graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -7937,7 +7981,7 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1: +ignore@^5.1.1, ignore@^5.1.4: version "5.1.8" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== @@ -8390,7 +8434,7 @@ is-path-inside@^2.1.0: dependencies: path-is-inside "^1.0.2" -is-path-inside@^3.0.1: +is-path-inside@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== @@ -11544,10 +11588,10 @@ p-map@^2.0.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== -p-map@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d" - integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ== +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== dependencies: aggregate-error "^3.0.0" @@ -12096,7 +12140,7 @@ proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.1" -proxy-from-env@^1.0.0: +proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -12193,6 +12237,18 @@ puppeteer-core@^5.1.0: unbzip2-stream "^1.3.3" ws "^7.2.3" +pvtsutils@^1.0.11, pvtsutils@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.1.1.tgz#22c2d7689139d2c36d7ef3ac3d5e29bcd818d38a" + integrity sha512-Evbhe6L4Sxwu4SPLQ4LQZhgfWDQO3qa1lju9jM5cxsQp8vE10VipcSmo7hiJW48TmiHgVLgDtC2TL6/+ND+IVg== + dependencies: + tslib "^2.0.3" + +pvutils@latest: + version "1.0.17" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.0.17.tgz#ade3c74dfe7178944fe44806626bd2e249d996bf" + integrity sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ== + q@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" @@ -14471,6 +14527,11 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" + integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -15092,6 +15153,17 @@ wdio-chromedriver-service@^6.0.4: dependencies: fs-extra "^9.0.0" +webcrypto-core@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.1.8.tgz#91720c07f4f2edd181111b436647ea5a282af0a9" + integrity sha512-hKnFXsqh0VloojNeTfrwFoRM4MnaWzH6vtXcaFcGjPEu+8HmBdQZnps3/2ikOFqS8bJN1RYr6mI2P/FJzyZnXg== + dependencies: + "@peculiar/asn1-schema" "^2.0.12" + "@peculiar/json-schema" "^1.1.12" + asn1js "^2.0.26" + pvtsutils "^1.0.11" + tslib "^2.0.1" + webcrypto-shim@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/webcrypto-shim/-/webcrypto-shim-0.1.5.tgz#13e34a010ccc544edecfe8a2642204502841bcf0"