Skip to content

Commit

Permalink
feat: Enroll Authenticator via /authorize (#1324)
Browse files Browse the repository at this point in the history
OKTA-539548 feat: Enroll Authenticator via /authorize
  • Loading branch information
denysoblohin-okta authored Dec 14, 2022
1 parent 3719459 commit 2c92f04
Show file tree
Hide file tree
Showing 43 changed files with 1,056 additions and 105 deletions.
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) {
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

0 comments on commit 2c92f04

Please sign in to comment.