Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enroll Authenticator via /authorize #1324

Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- [#1333](https://github.com/okta/okta-auth-js/pull/1333) Adds support for MyAccount API password methods
- [#1324](https://github.com/okta/okta-auth-js/pull/1324) Adds `endpoints.authorize.enrollAuthenticator`. Adds `handleRedirect` and deprecates `handleLoginRedirect`.

### Fixes

Expand Down
129 changes: 93 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ var authClient = new OktaAuth(config);

### Running as a service

By default, creating a new instance of `OktaAuth` will not create any asynchronous side-effects. However, certain features such as [token auto renew](#autorenew), [token auto remove](#autoremove) and [cross-tab synchronization](#syncstorage) require `OktaAuth` to be running as a service. This means timeouts are set in the background which will continue working until the service is stopped. To start the `OktaAuth` service, simply call the `start` method right after creation and before calling other methods like [handleLoginRedirect](#handleloginredirecttokens). To terminate all background processes, call `stop`. See [Service Configuration](#services) for more info.
By default, creating a new instance of `OktaAuth` will not create any asynchronous side-effects. However, certain features such as [token auto renew](#autorenew), [token auto remove](#autoremove) and [cross-tab synchronization](#syncstorage) require `OktaAuth` to be running as a service. This means timeouts are set in the background which will continue working until the service is stopped. To start the `OktaAuth` service, simply call the `start` method right after creation and before calling other methods like [handleRedirect](#handleredirectoriginaluri). To terminate all background processes, call `stop`. See [Service Configuration](#services) for more info.

```javascript
var authClient = new OktaAuth(config);
Expand Down Expand Up @@ -536,7 +536,7 @@ oktaAuth.authStateManager.updateAuthState();

> :link: web browser only <br>

Callback function. When [sdk.handleLoginRedirect](#handleloginredirecttokens) is called, by default it uses `window.location.replace` to redirect back to the [originalUri](#setoriginaluriuri). This option overrides the default behavior.
Callback function. When [sdk.handleRedirect](#handleredirectoriginaluri) is called, by default it uses `window.location.replace` to redirect back to the [originalUri](#setoriginaluriuri). This option overrides the default behavior.

```javascript
const config = {
Expand All @@ -552,7 +552,7 @@ const config = {
const oktaAuth = new OktaAuth(config);
if (oktaAuth.isLoginRedirect()) {
try {
await oktaAuth.handleLoginRedirect();
await oktaAuth.handleRedirect();
} catch (e) {
// log or display error details
}
Expand Down Expand Up @@ -891,7 +891,8 @@ This is accomplished by selecting a single tab to handle the network requests to
* [getOriginalUri](#getoriginaluristate)
* [removeOriginalUri](#removeoriginaluri)
* [isLoginRedirect](#isloginredirect)
* [handleLoginRedirect](#handleloginredirecttokens)
* [handleLoginRedirect](#handleloginredirecttokens-originaluri)
* [handleRedirect](#handleredirectoriginaluri)
* [setHeaders](#setheaders)
* [tx.resume](#txresume)
* [tx.exists](#txexists)
Expand All @@ -903,6 +904,8 @@ This is accomplished by selecting a single tab to handle the network requests to
* [session.refresh](#sessionrefresh)
* [idx](#idx)
* [myaccount](#myaccount)
* [endpoints](#endpoints)
* [endpoints.autorize.enrollAuthenticator](#endpointsauthorizeenrollauthenticatoroptions)
* [token](#token)
* [token.getWithoutPrompt](#tokengetwithoutpromptoptions)
* [token.getWithPopup](#tokengetwithpopupoptions)
Expand Down Expand Up @@ -966,7 +969,7 @@ You can use [storeTokensFromRedirect](#storetokensfromredirect) to store tokens
```javascript
if (authClient.isLoginRedirect()) {
try {
await authClient.handleLoginRedirect();
await authClient.handleRedirect();
} catch (e) {
// log or display error details
}
Expand Down Expand Up @@ -1174,7 +1177,7 @@ Check `window.location` to verify if the app is in OAuth callback state or not.
if (authClient.isLoginRedirect()) {
// callback flow
try {
await authClient.handleLoginRedirect();
await authClient.handleRedirect();
} catch (e) {
// log or display error details
}
Expand All @@ -1186,12 +1189,23 @@ if (authClient.isLoginRedirect()) {
### `handleLoginRedirect(tokens?, originalUri?)`

> :link: web browser only <br>
> :hourglass: async
> :hourglass: async <br>
> :warning: Deprecated, this method could be removed in next major release, use [sdk.handleRedirect](#handleredirectoriginaluri) instead.

Stores passed in tokens or tokens from redirect url into storage, then redirect users back to the [originalUri](#setoriginaluriuri). When using `PKCE` authorization code flow, this method also exchanges authorization code for tokens. By default it calls `window.location.replace` for the redirection. The default behavior can be overrided by providing [options.restoreOriginalUri](#configuration-options). By default, [originalUri](#getoriginaluristate) will be retrieved from storage, but this can be overridden by passing a value fro `originalUri` to this function in the 2nd parameter.

> **Note:** `handleLoginRedirect` throws `OAuthError` or `AuthSdkError` in case there are errors during token retrieval.

### `handleRedirect(originalUri?)`

> :link: web browser only <br>
> :hourglass: async

Handle a redirect to the configured [redirectUri](#configuration-options) that happens on the end of [login](#signInWithRedirectoptions) flow, [enroll authenticator](#endpointsauthorizeenrollauthenticatoroptions) flow or on an error.
Stores tokens from redirect url into storage (for login flow), then redirect users back to the [originalUri](#setoriginaluriuri). When using `PKCE` authorization code flow, this method also exchanges authorization code for tokens. By default it calls `window.location.replace` for the redirection. The default behavior can be overrided by providing [options.restoreOriginalUri](#configuration-options). By default, [originalUri](#getoriginaluristate) will be retrieved from storage, but this can be overridden by specifying `originalUri` in the first parameter to this function.

> **Note:** `handleRedirect` throws `OAuthError` or `AuthSdkError` in case there are errors during token retrieval or authenticator enrollment.

### `setHeaders()`

Can set (or unset) request headers after construction.
Expand Down Expand Up @@ -1238,7 +1252,7 @@ See [authn API](docs/authn.md#sessionsetcookieandredirectsessiontoken-redirectur
#### `session.exists()`

> :link: web browser only <br>
> :warning: This method requires access to [third party cookies] <br>(#third-party-cookies)
> :warning: This method requires access to [third party cookies](#third-party-cookies) <br>
> :hourglass: async

Returns a promise that resolves with `true` if there is an existing Okta [session](https://developer.okta.com/docs/api/resources/sessions#example), or `false` if not.
Expand All @@ -1257,7 +1271,7 @@ authClient.session.exists()
#### `session.get()`

> :link: web browser only <br>
> :warning: This method requires access to [third party cookies] <br>(#third-party-cookies)
> :warning: This method requires access to [third party cookies](#third-party-cookies) <br>
> :hourglass: async

Gets the active [session](https://developer.okta.com/docs/api/resources/sessions#example).
Expand All @@ -1275,7 +1289,7 @@ authClient.session.get()
#### `session.refresh()`

> :link: web browser only <br>
> :warning: This method requires access to [third party cookies] <br>(#third-party-cookies)
> :warning: This method requires access to [third party cookies](#third-party-cookies) <br>
> :hourglass: async

Refresh the current session by extending its lifetime. This can be used as a keep-alive operation.
Expand All @@ -1298,8 +1312,7 @@ See detail in [IDX README](docs/idx.md)

See detail in [MyAccount API README](docs/myaccount/README.md)


### `token`
### `endpoints`

#### Authorize options

Expand All @@ -1315,39 +1328,71 @@ The following configuration options can be included in `token.getWithoutPrompt`,
| `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. |
| `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. Special value `enroll_authenticator` is used for [enrollAuthenticator](#endpointsauthorizeenrollauthenticatoroptions). |
| `maxAge` | Allowable elapsed time, in seconds, since the last time the end user was actively authenticated by Okta. |
| `acrValues` | [[EA][early-access]] Optional parameter to increase the level of user assurance. See [Predefined ACR values](https://developer.okta.com/docs/guides/step-up-authentication/main/#predefined-parameter-values) for more information. |
| `enrollAmrValues` | [[EA][early-access]] List of [authentication methods](https://self-issued.info/docs/draft-jones-oauth-amr-values-00.html) used to enroll authenticators with [enrollAuthenticator](#endpointsauthorizeenrollauthenticatoroptions). See [Parameter details](https://developer.okta.com/docs/reference/api/oidc/#parameter-details) for more information. |
| `loginHint` | A username to prepopulate if prompting for authentication. |

For more details, see Okta's [Authorize Request API](https://developer.okta.com/docs/api/resources/oidc#request-parameters).

#### `endpoints.authorize.enrollAuthenticator(options)`

> :link: web browser only <br>
> [Early Access][early-access]

Enroll authenticators using a redirect to [authorizeUrl](#authorizeurl) with special parameters. After a successful enrollment, the browser will be redirected to the configured [redirectUri](#configuration-options). You can use [sdk.handleRedirect](#handleredirectoriginaluri) to handle the redirect on successful enrollment or an error.

* `options` - See [Authorize options](#authorize-options)

Options that will be omitted: `scopes`, `nonce`.

Options that will be overridden: `responseType: 'none', prompt: 'enroll_authenticator'`.

Required options:

* `enrollAmrValues` - list of [authentication methods](https://self-issued.info/docs/draft-jones-oauth-amr-values-00.html) to allow the user to enroll in.

List of AMR values:
| AMR Value | Authenticator |
| ------------- | -------------------- |
| `pwd` | Okta Password |
| `kba` | Security question |
| `email` | Okta Email |
| `sms` | SMS |
| `tel` | Voice call |
| `duo` | DUO |
| `symantec` | Symantec VIP |
| `google_otp` | Google Authenticator |
| `okta_verify` | Okta Verify |
| `swk` | Custom App |
| `pop` | WebAuthn |
| `oath_otp` | On-Prem MFA |
| `rsa` | RSA SecurID |
| `yubikey` | Yubikey |
| `otp` | Custom HOTP |
| `fed` | External IdP |
| `sc` + `swk` | SmartCard/PIV |

See [enroll_amr_values parameter details](https://developer.okta.com/docs/reference/api/oidc/#request-parameters) for more information.

* `acrValues` - must be `urn:okta:2fa:any:ifpossible`, which means the user is prompted for at least one factor before enrollment.

##### Example

```javascript
authClient.token.getWithoutPrompt({
sessionToken: '00p8RhRDCh_8NxIin-wtF5M6ofFtRhfKWGBAbd2WmE',
scopes: [
'openid',
'email',
'profile'
],
state: '8rFzn3MH5q',
nonce: '51GePTswrm',
// Use a custom IdP for social authentication
idp: '0oa62b57p7c8PaGpU0h7'
})
.then(function(res) {
var tokens = res.tokens;

// Do something with tokens, such as
authClient.tokenManager.setTokens(tokens);
})
.catch(function(err) {
// handle OAuthError or AuthSdkError
});
try {
authClient.endpoints.authorize.enrollAuthenticator({
enrollAmrValues: ['okta_verify'],
acrValues: 'urn:okta:2fa:any:ifpossible'
})
} catch(err) {
// handle AuthSdkError
}
```

### `token`

#### `token.getWithoutPrompt(options)`

> :link: web browser only <br>
Expand All @@ -1358,11 +1403,22 @@ When you've obtained a sessionToken from the authorization flows, or a session a

* `options` - See [Authorize options](#authorize-options)

##### Example

```javascript
authClient.token.getWithoutPrompt({
responseType: 'id_token', // or array of types
sessionToken: 'testSessionToken' // optional if the user has an existing Okta session
})
scopes: [
'openid',
'email',
'profile'
],
state: '8rFzn3MH5q',
nonce: '51GePTswrm',
// Use a custom IdP for social authentication
idp: '0oa62b57p7c8PaGpU0h7'
})
.then(function(res) {
var tokens = res.tokens;

Expand Down Expand Up @@ -1492,7 +1548,7 @@ console.log(decodedToken.header, decodedToken.payload, decodedToken.signature);

#### `token.renew(tokenToRenew)`

> :warning: This method requires access to [third party cookies](#third-party-cookies)
> :warning: This method requires access to [third party cookies](#third-party-cookies) <br>
> :hourglass: async

Returns a new token if the Okta [session](https://developer.okta.com/docs/api/resources/sessions#example) is still valid.
Expand Down Expand Up @@ -1597,6 +1653,7 @@ Returns a `TokenParams` object. If `PKCE` is enabled, this object will contain v

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` API

#### `tokenManager.add(key, token)`
Expand Down
1 change: 1 addition & 0 deletions jest.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const config = Object.assign({}, baseConfig, {
'oidc/getWithoutPrompt',
'oidc/renewToken.ts',
'oidc/renewTokens.ts',
'oidc/enrollAuthenticator',
'TokenManager/browser',
'SyncStorageService',
'LeaderElectionService',
Expand Down
4 changes: 4 additions & 0 deletions lib/core/mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export function mixinCore
await this.serviceManager.stop();
}

async handleRedirect(originalUri?: string): Promise<void> {
await this.handleLoginRedirect(undefined, originalUri);
}

// eslint-disable-next-line complexity
async handleLoginRedirect(tokens?: Tokens, originalUri?: string): Promise<void> {
let state = this.options.state;
Expand Down
1 change: 1 addition & 0 deletions lib/core/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ extends OktaAuthOAuthInterface<M, S, O, TM>
start(): Promise<void>;
stop(): Promise<void>;
handleLoginRedirect(tokens?: Tokens, originalUri?: string): Promise<void>;
handleRedirect(originalUri?: string): Promise<void>;
}
5 changes: 3 additions & 2 deletions lib/oidc/endpoints/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ export function convertTokenParamsToOAuthParams(tokenParams: TokenParams) {
'sessionToken': tokenParams.sessionToken,
'state': tokenParams.state,
'acr_values': tokenParams.acrValues,
'enroll_amr_values': tokenParams.enrollAmrValues,
};
oauthParams = removeNils(oauthParams) as OAuthParams;

['idp_scope', 'response_type'].forEach(function (mayBeArray) {
['idp_scope', 'response_type', 'enroll_amr_values'].forEach(function (mayBeArray) {
if (Array.isArray(oauthParams[mayBeArray])) {
oauthParams[mayBeArray] = oauthParams[mayBeArray].join(' ');
}
Expand All @@ -56,7 +57,7 @@ export function convertTokenParamsToOAuthParams(tokenParams: TokenParams) {
if (tokenParams.responseType!.indexOf('id_token') !== -1 &&
tokenParams.scopes!.indexOf('openid') === -1) {
throw new AuthSdkError('openid scope must be specified in the scopes argument when requesting an id_token');
} else {
} else if (tokenParams.scopes) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? Does empty scope query parameter have a special meaning to the authorize endpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oauthParams.scope = tokenParams.scopes!.join(' ');
}

Expand Down
34 changes: 34 additions & 0 deletions lib/oidc/enrollAuthenticator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/*!
* Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*
*/
import { OktaAuthOAuthInterface, EnrollAuthenticatorOptions } from './types';
import { clone } from '../util';
import { prepareEnrollAuthenticatorParams, createEnrollAuthenticatorMeta } from './util';
import { buildAuthorizeParams } from './endpoints/authorize';

export function enrollAuthenticator(
sdk: OktaAuthOAuthInterface,
options: EnrollAuthenticatorOptions
): void {
options = clone(options) || {};

const params = prepareEnrollAuthenticatorParams(sdk, options);
const meta = createEnrollAuthenticatorMeta(sdk, params);
const requestUrl = meta.urls.authorizeUrl + buildAuthorizeParams(params);
sdk.transactionManager.save(meta);
if (sdk.options.setLocation) {
sdk.options.setLocation(requestUrl);
} else {
window.location.assign(requestUrl);
}
}
Loading