diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b6847d8c..b4e0347dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ - [#317](https://github.com/okta/okta-auth-js/pull/317) - `pkce` option is now `true` by default. `grantType` option is removed. +- [#320](https://github.com/okta/okta-auth-js/pull/320) - `getWithRedirect`, `getWithPopup`, and `getWithoutPrompt` previously took 2 sets of option objects as parameters, a set of "oauthOptions" and additional options. These methods now take a single options object which can hold all available options. Passing a second options object will cause an exception to be thrown. + - [#321](https://github.com/okta/okta-auth-js/pull/321) - Default responseType when using implicit flow is now ['token', 'id_token']. - When both access token and id token are returned, the id token's at_hash claim will be validated against the access token diff --git a/README.md b/README.md index b372580ee..bc9f77710 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,10 @@ * [Node JS Usage](#node-js-usage) * [Contributing](#contributing) -The Okta Auth JavaScript SDK builds on top of our [Authentication API](https://developer.okta.com/docs/api/resources/authn) and [OAuth 2.0 API](https://developer.okta.com/docs/api/resources/oidc) to enable you to create a fully branded sign-in experience using JavaScript. +The Okta Auth JavaScript SDK builds on top of our [Authentication API](https://developer.okta.com/docs/api/resources/authn) and [OpenID Connect & OAuth 2.0 API](https://developer.okta.com/docs/api/resources/oidc) to enable you to create a fully branded sign-in experience using JavaScript. You can learn more on the [Okta + JavaScript][lang-landing] page in our documentation. -## Release status - This library uses semantic versioning and follows Okta's [library version policy](https://developer.okta.com/code/library-versions/). :heavy_check_mark: The current stable major version series is: `2.x` @@ -155,7 +153,7 @@ var config = { var authClient = new OktaAuth(config); ``` -### [OpenID Connect](https://developer.okta.com/docs/api/resources/oidc) options +### Configuration options These configuration options can be included when instantiating Okta Auth JS (`new OktaAuth(config)`) or in `token.getWithoutPrompt`, `token.getWithPopup`, or `token.getWithRedirect` (unless noted otherwise). If included in both, the value passed in the method takes priority. @@ -377,8 +375,8 @@ var config = { * [session.get](#sessionget) * [session.refresh](#sessionrefresh) * [token](#token) - * [token.getWithoutPrompt](#tokengetwithoutpromptoauthoptions) - * [token.getWithPopup](#tokengetwithpopupoauthoptions) + * [token.getWithoutPrompt](#tokengetwithoutpromptoptions) + * [token.getWithPopup](#tokengetwithpopupoptions) * [token.getWithRedirect](#tokengetwithredirectoptions) * [token.parseFromUrl](#tokenparsefromurloptions) * [token.decode](#tokendecodeidtokenstring) @@ -1518,7 +1516,7 @@ authClient.session.refresh() ### `token` -#### Extended OpenID Connect options +#### Authorize options The following configuration options can **only** be included in `token.getWithoutPrompt`, `token.getWithPopup`, or `token.getWithRedirect`. @@ -1530,8 +1528,14 @@ The following configuration options can **only** be included in `token.getWithou | `scopes` | Specify what information to make available in the returned `id_token` or `access_token`. For OIDC, you must include `openid` as one of the scopes. Defaults to `['openid', 'email']`. For a list of available scopes, see [Scopes and Claims](https://developer.okta.com/docs/api/resources/oidc#access-token-scopes-and-claims). | | `state` | A string that will be passed to `/authorize` endpoint and returned in the OAuth response. The value is used to validate the OAuth response and prevent cross-site request forgery (CSRF). The `state` value passed to [getWithRedirect](#tokengetwithredirectoptions) will be returned along with any requested tokens from [parseFromUrl](#tokenparsefromurloptions). Your app can use this string to perform additional validation and/or pass information from the login page. Defaults to a random string. | | `nonce` | Specify a nonce that will be validated in an `id_token`. This is usually only provided during redirect flows to obtain an authorization code that will be exchanged for an `id_token`. Defaults to a random string. | +| `idp` | Identity provider to use if there is no Okta Session. | +| `idpScope` | A space delimited list of scopes to be provided to the Social Identity Provider when performing [Social Login](social-login) These scopes are used in addition to the scopes already configured on the Identity Provider. | +| `display` | The display parameter to be passed to the Social Identity Provider when performing [Social Login](social-login). | +| `prompt` | Determines whether the Okta login will be displayed on failure. Use `none` to prevent this behavior. Valid values: `none`, `consent`, `login`, or `consent login`. See [Parameter details](https://developer.okta.com/docs/reference/api/oidc/#parameter-details) for more information. | +| `maxAge` | Allowable elapsed time, in seconds, since the last time the end user was actively authenticated by Okta. | +| `loginHint` | A username to prepopulate if prompting for authentication. | -For a list of all available parameters that can be passed to the `/authorize` endpoint, see Okta's [Authorize Request API](https://developer.okta.com/docs/api/resources/oidc#request-parameters). +For more details, see Okta's [Authorize Request API](https://developer.okta.com/docs/api/resources/oidc#request-parameters). ##### Example @@ -1559,11 +1563,11 @@ authClient.token.getWithoutPrompt({ }); ``` -#### `token.getWithoutPrompt(oauthOptions)` +#### `token.getWithoutPrompt(options)` When you've obtained a sessionToken from the authorization flows, or a session already exists, you can obtain a token or tokens without prompting the user to log in. -* `oauthOptions` - See [Extended OpenID Connect options](#extended-openid-connect-options) +* `options` - See [Authorize options](#authorize-options) ```javascript authClient.token.getWithoutPrompt({ @@ -1581,14 +1585,14 @@ authClient.token.getWithoutPrompt({ }); ``` -#### `token.getWithPopup(oauthOptions)` +#### `token.getWithPopup(options)` Create token with a popup. -* `oauthOptions` - See [Extended OpenID Connect options](#extended-openid-connect-options) +* `options` - See [Authorize options](#authorize-options) ```javascript -authClient.token.getWithPopup(oauthOptions) +authClient.token.getWithPopup(options) .then(function(res) { var tokens = res.tokens; @@ -1604,7 +1608,7 @@ authClient.token.getWithPopup(oauthOptions) Create token using a redirect. After a successful authentication, the browser will be redirected to the configured [redirectUri](#additional-options). The authorization code, access, or ID Tokens will be available as parameters appended to this URL. By default, values will be in the hash fragment of the URL (for SPA applications) or in the search query (for Web applications). SPA Applications using the PKCE flow can opt to receive the authorization code in the search query by setting the [responseMode](#additional-options) option to "query". -* `oauthOptions` - See [Extended OpenID Connect options](#extended-openid-connect-options) +* `options` - See [Authorize options](#authorize-options) ```javascript authClient.token.getWithRedirect({ @@ -1964,3 +1968,4 @@ We're happy to accept contributions and PRs! Please see the [contribution guide] [lang-landing]: https://developer.okta.com/code/javascript [github-issues]: https://github.com/okta/okta-auth-js/issues [github-releases]: https://github.com/okta/okta-auth-js/releases +[social-login]: https://developer.okta.com/docs/concepts/social-login/ diff --git a/packages/okta-auth-js/lib/oauthUtil.js b/packages/okta-auth-js/lib/oauthUtil.js index 589d79303..7c20adf8a 100644 --- a/packages/okta-auth-js/lib/oauthUtil.js +++ b/packages/okta-auth-js/lib/oauthUtil.js @@ -164,7 +164,10 @@ function validateClaims(sdk, claims, validationParams) { } } -function getOAuthUrls(sdk, oauthParams, options) { +function getOAuthUrls(sdk, options) { + if (arguments.length > 2) { + throw new AuthSdkError('As of version 3.0, "getOAuthUrls" takes only a single set of options'); + } options = options || {}; // Get user-supplied arguments diff --git a/packages/okta-auth-js/lib/token.js b/packages/okta-auth-js/lib/token.js index 8718dc214..e85c6c3f2 100644 --- a/packages/okta-auth-js/lib/token.js +++ b/packages/okta-auth-js/lib/token.js @@ -426,11 +426,13 @@ function buildAuthorizeParams(oauthParams) { * @param {String} [options.popupTitle] Title dispayed in the popup. * Defaults to 'External Identity Provider User Authentication' */ -function getToken(sdk, oauthOptions, options) { - oauthOptions = oauthOptions || {}; +function getToken(sdk, options) { + if (arguments.length > 2) { + return Promise.reject(new AuthSdkError('As of version 3.0, "getToken" takes only a single set of options')); + } options = options || {}; - return prepareOauthParams(sdk, oauthOptions) + return prepareOauthParams(sdk, options) .then(function(oauthParams) { // Start overriding any options that don't make sense @@ -444,9 +446,9 @@ function getToken(sdk, oauthOptions, options) { display: 'popup' }; - if (oauthOptions.sessionToken) { + if (options.sessionToken) { util.extend(oauthParams, sessionTokenOverrides); - } else if (oauthOptions.idp) { + } else if (options.idp) { util.extend(oauthParams, idpOverrides); } @@ -456,8 +458,8 @@ function getToken(sdk, oauthOptions, options) { urls; // Get authorizeUrl and issuer - urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options); - endpoint = oauthOptions.codeVerifier ? urls.tokenUrl : urls.authorizeUrl; + urls = oauthUtil.getOAuthUrls(sdk, oauthParams); + endpoint = options.codeVerifier ? urls.tokenUrl : urls.authorizeUrl; requestUrl = endpoint + buildAuthorizeParams(oauthParams); // Determine the flow type @@ -557,32 +559,38 @@ function getToken(sdk, oauthOptions, options) { }); } -function getWithoutPrompt(sdk, oauthOptions, options) { - var oauthParams = util.clone(oauthOptions) || {}; - util.extend(oauthParams, { +function getWithoutPrompt(sdk, options) { + if (arguments.length > 2) { + return Promise.reject(new AuthSdkError('As of version 3.0, "getWithoutPrompt" takes only a single set of options')); + } + options = util.clone(options) || {}; + util.extend(options, { prompt: 'none', responseMode: 'okta_post_message', display: null }); - return getToken(sdk, oauthParams, options); + return getToken(sdk, options); } -function getWithPopup(sdk, oauthOptions, options) { - var oauthParams = util.clone(oauthOptions) || {}; - util.extend(oauthParams, { +function getWithPopup(sdk, options) { + if (arguments.length > 2) { + return Promise.reject(new AuthSdkError('As of version 3.0, "getWithPopup" takes only a single set of options')); + } + options = util.clone(options) || {}; + util.extend(options, { display: 'popup', responseMode: 'okta_post_message' }); - return getToken(sdk, oauthParams, options); + return getToken(sdk, options); } -function prepareOauthParams(sdk, oauthOptions) { +function prepareOauthParams(sdk, options) { // clone and prepare options - oauthOptions = util.clone(oauthOptions) || {}; + options = util.clone(options) || {}; // build params using defaults + options var oauthParams = getDefaultOAuthParams(sdk); - util.extend(oauthParams, oauthOptions); + util.extend(oauthParams, options); if (oauthParams.pkce === false) { return Promise.resolve(oauthParams); @@ -632,15 +640,18 @@ function prepareOauthParams(sdk, oauthOptions) { }); } -function getWithRedirect(sdk, oauthOptions, options) { - oauthOptions = util.clone(oauthOptions) || {}; +function getWithRedirect(sdk, options) { + if (arguments.length > 2) { + return Promise.reject(new AuthSdkError('As of version 3.0, "getWithRedirect" takes only a single set of options')); + } + options = util.clone(options) || {}; - return prepareOauthParams(sdk, oauthOptions) + return prepareOauthParams(sdk, options) .then(function(oauthParams) { - // Dynamically set the responseMode unless the user has provided one + // Dynamically set the responseMode unless the user has explicitly provided one // Server-side flow requires query. Client-side apps usually prefer fragment. - if (!oauthOptions.responseMode) { + if (!options.responseMode) { if (oauthParams.responseType.includes('code') && !oauthParams.pkce) { // server-side flows using authorization_code oauthParams.responseMode = 'query'; @@ -650,7 +661,7 @@ function getWithRedirect(sdk, oauthOptions, options) { } } - var urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options); + var urls = oauthUtil.getOAuthUrls(sdk, options); var requestUrl = urls.authorizeUrl + buildAuthorizeParams(oauthParams); // Set session cookie to store the oauthParams @@ -700,8 +711,7 @@ function renewToken(sdk, token) { return sdk.token.getWithoutPrompt({ responseType: responseType, - scopes: token.scopes - }, { + scopes: token.scopes, authorizeUrl: token.authorizeUrl, userinfoUrl: token.userinfoUrl, issuer: token.issuer diff --git a/packages/okta-auth-js/test/spec/oauthUtil.js b/packages/okta-auth-js/test/spec/oauthUtil.js index cef576352..7155c900e 100644 --- a/packages/okta-auth-js/test/spec/oauthUtil.js +++ b/packages/okta-auth-js/test/spec/oauthUtil.js @@ -416,13 +416,9 @@ describe('getOAuthUrls', function() { issuer: 'https://auth-js-test.okta.com' }); - var oauthParams = options.oauthParams || { - responseType: 'id_token' - }; - var result, error; try { - result = oauthUtil.getOAuthUrls(sdk, oauthParams, options.options); + result = oauthUtil.getOAuthUrls(sdk, options.options); } catch(e) { error = e; } @@ -442,6 +438,18 @@ describe('getOAuthUrls', function() { } } + it('throws if an extra options object is passed', () => { + const sdk = new OktaAuth({ + pkce: false, + issuer: 'https://auth-js-test.okta.com' + }); + + const f = function () { + oauthUtil.getOAuthUrls(sdk, {}, {}); + }; + expect(f).toThrowError('As of version 3.0, "getOAuthUrls" takes only a single set of options'); + }); + it('defaults all urls using global defaults', function() { setupOAuthUrls({ expectedResult: { diff --git a/packages/okta-auth-js/test/spec/token.js b/packages/okta-auth-js/test/spec/token.js index 008f2cbe2..84fbabd2e 100644 --- a/packages/okta-auth-js/test/spec/token.js +++ b/packages/okta-auth-js/test/spec/token.js @@ -209,6 +209,35 @@ describe('token.getWithoutPrompt', function() { }); }); + it('If extra options are passed, promise will reject', function() { + return oauthUtil.setupFrame({ + willFail: true, + oktaAuthArgs: { + pkce: false, + issuer: 'https://auth-js-test.okta.com', + }, + getWithoutPromptArgs: [{ + /* expected options */ + }, { + /* extra options */ + }] + }) + .then(function() { + expect(true).toEqual(false); + }) + .catch(function(err) { + util.expectErrorToEqual(err, { + name: 'AuthSdkError', + message: 'As of version 3.0, "getWithoutPrompt" takes only a single set of options', + errorCode: 'INTERNAL', + errorSummary: 'As of version 3.0, "getWithoutPrompt" takes only a single set of options', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + }); + }); + }); + it('If authorizeUrl does not match configured issuer, promise will reject', function() { return oauthUtil.setupFrame({ willFail: true, @@ -219,8 +248,7 @@ describe('token.getWithoutPrompt', function() { redirectUri: 'https://example.com/redirect' }, getWithoutPromptArgs: [{ - sessionToken: 'testSessionToken' - }, { + sessionToken: 'testSessionToken', authorizeUrl: 'https://bogus', }], postMessageSrc: { @@ -358,8 +386,7 @@ describe('token.getWithoutPrompt', function() { redirectUri: 'https://example.com/redirect' }, getWithoutPromptArgs: [{ - sessionToken: 'testSessionToken' - }, { + sessionToken: 'testSessionToken', issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7' }], postMessageSrc: { @@ -787,6 +814,35 @@ describe('token.getWithPopup', function() { jest.useRealTimers(); }); + it('promise will reject if extra options object is passed', function() { + return oauthUtil.setup({ + willFail: true, + oktaAuthArgs: { + pkce: false, + issuer: 'https://auth-js-test.okta.com', + }, + getWithPopupArgs: [{ + /* expected options */ + }, { + /* extra options */ + }] + }) + .then(function() { + expect(true).toEqual(false); + }) + .catch(function(err) { + util.expectErrorToEqual(err, { + name: 'AuthSdkError', + message: 'As of version 3.0, "getWithPopup" takes only a single set of options', + errorCode: 'INTERNAL', + errorSummary: 'As of version 3.0, "getWithPopup" takes only a single set of options', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + }); + }); + }); + it('promise will reject if fails due to timeout', function() { var timeoutMs = 120000; var mockWindow = { @@ -948,8 +1004,7 @@ describe('token.getWithPopup', function() { redirectUri: 'https://example.com/redirect' }, getWithPopupArgs: [{ - idp: 'testIdp' - }, { + idp: 'testIdp', issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7' }], postMessageSrc: { @@ -1287,6 +1342,35 @@ describe('token.getWithRedirect', function() { spyOn(pkce, 'computeChallenge').and.returnValue(Promise.resolve(codeChallenge)); } + it('If extra options are passed, promise will reject', function() { + return oauthUtil.setupRedirect({ + willFail: true, + oktaAuthArgs: { + pkce: false, + issuer: 'https://auth-js-test.okta.com', + }, + getWithRedirectArgs: [{ + /* expected options */ + }, { + /* extra options */ + }] + }) + .then(function() { + expect(true).toEqual(false); + }) + .catch(function(err) { + util.expectErrorToEqual(err, { + name: 'AuthSdkError', + message: 'As of version 3.0, "getWithRedirect" takes only a single set of options', + errorCode: 'INTERNAL', + errorSummary: 'As of version 3.0, "getWithRedirect" takes only a single set of options', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + }); + }); + }); + it('Can pass responseMode=query', function() { return oauthUtil.setupRedirect({ getWithRedirectArgs: { @@ -1452,8 +1536,7 @@ describe('token.getWithRedirect', function() { getWithRedirectArgs: [{ responseType: 'token', scopes: ['email'], - sessionToken: 'testToken' - }, { + sessionToken: 'testToken', issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7' }], expectedCookies: [