Skip to content

Commit

Permalink
feat: add onAuthResume option
Browse files Browse the repository at this point in the history
OKTA-360882
<<<Jenkins Check-In of Tested SHA: 177911f for [email protected]>>>
Artifact: okta-angular
Files changed count: 7
  • Loading branch information
aarongranick-okta authored and eng-prod-CI-bot-okta committed Apr 13, 2021
1 parent 521b0a3 commit c264fd5
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 34 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# 3.1.0

### Features

[#33](https://github.com/okta/okta-angular/pull/33) Adds option `onAuthResume` to resume authorization flow on custom login page.

# 3.0.1

### Bug Fixes

[#9](https://github.com/okta/okta-angular/pull/9) fix: handle --base-href option

# 3.0.0

[#5](https://github.com/okta/okta-angular/pull/5) Release 3.0.0 - `OktaAuthService` now inherits from an instance of `@okta/okta-auth-js` so all configuration options and public methods are available. See [MIGRATING](MIGRATING.md) for detailed information.
Expand Down
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[Dependency Injection]: https://angular.io/guide/dependency-injection
[OktaAuthService]: #oktaauthservice
[AuthState]: https://github.com/okta/okta-auth-js#authstatemanager
[external identity provider]: https://developer.okta.com/docs/concepts/identity-providers/

# Okta Angular SDK

Expand Down Expand Up @@ -75,7 +76,7 @@ import {
} from '@okta/okta-angular';

const oktaConfig = {
issuer: 'https://{yourOktaDomain}.com/oauth2/default',
issuer: 'https://{yourOktaDomain}/oauth2/default',
clientId: '{clientId}',
redirectUri: window.location.origin + '/login/callback'
}
Expand All @@ -102,7 +103,8 @@ An Angular InjectionToken used to configure the OktaAuthService. This value must

This SDK accepts all configuration options defined by [@okta/okta-auth-js][] and adds some additional options:

- `onAuthRequired` *(optional)*: - callback function. Called when authentication is required. If not supplied, `okta-angular` will redirect directly to Okta for authentication. This is triggered when a route protected by `OktaAuthGuard` is accessed without authentication.
- `onAuthRequired` *(optional)*: - callback function. Triggered when a route protected by `OktaAuthGuard` is accessed without authentication. Use this to present a [custom login page](#using-a-custom-login-page). If no `onAuthRequired` callback is defined, `okta-angular` will redirect directly to Okta for authentication.
- `onAuthResume` *(optional)*: - callback function. Only relevant if using a [custom login page](#using-a-custom-login-page). Called when the [authentication flow should be resumed by the application](#resuming-the-authentication-flow), typically as a result of redirect callback from an [external identity provider][]. If not defined, `onAuthRequired` will be called.
- `isAuthenticated` *(optional)* - callback function. By default, [OktaAuthService.isAuthenticated()](https://github.com/okta/okta-auth-js#isauthenticatedtimeout) will return true if **both** [getIdToken()](https://github.com/okta/okta-auth-js#getidtoken) **and** [getAccessToken()](https://github.com/okta/okta-auth-js#getaccesstoken) return a value. Setting an `isAuthenticated` function on the config allows you to customize this logic. The function receives an instance of `OktaAuthService` as a parameter and should return a Promise which resolves to either true or false.

### `OktaAuthModule`
Expand Down Expand Up @@ -258,6 +260,34 @@ const appRoutes: Routes = [
]
```

##### Resuming the authentication flow

When using a custom login page and an [external identity provider][] your app should be prepared to handle a redirect callback from Okta to resume the authentication flow. The `OktaCallbackComponent` has built-in logic for this scenario.

The `redirectUri` of your application will be requested with a special parameter (`?error=interaction_required`) to indicate that the authentication flow should be resumed by the application. In this case, the `OktaCallbackComponent` will call the `onAuthResume` function (if set on `OktaConfig`). If `onAuthResume` is not defined, then `onAuthRequired` will be called (if defined). If neither method is set in `OktaConfig`, then the `interaction_required` error will be displayed as a string.

If the authentication flow began on the custom login page using the [Okta SignIn Widget][], the transaction will automatically resume when the widget is rendered again on the custom login page.

Note that `onAuthResume` has the same signature as `onAuthRequired`. If you do not need any special logic for resuming an authorization flow, you can define only an `onAuthRequired` method and it will be called both to start or resume an auth flow.

```typescript
// myApp.module.ts

function onAuthResume(oktaAuth, injector) {
// Use injector to access any service available within your application
const router = injector.get(Router);

// Redirect the user to custom login page which renders the Okta SignIn Widget
router.navigate(['/custom-login']);
}

const oktaConfig = {
issuer: environment.ISSUER,
...
onAuthResume: onAuthResume
};
```

### `OktaAuthService`

In your components, your can take advantage of all of `okta-angular`'s features by importing the `OktaAuthService`. The `OktaAuthService` inherits from the `OktaAuth` service exported by [@okta/okta-auth-jks][] making the full [configuration](https://github.com/okta/okta-auth-js#configuration-reference) and [api](https://github.com/okta/okta-auth-js#api-reference) available on `OktaAuthService`.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
],
"license": "Apache-2.0",
"dependencies": {
"@okta/okta-auth-js": "^4.1.0",
"@okta/okta-auth-js": "^4.8.0",
"tslib": "^1.9.0"
},
"devDependencies": {
Expand Down
14 changes: 11 additions & 3 deletions src/okta/components/callback.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,31 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Optional, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { OktaAuthService } from '../services/okta.service';

@Component({
template: `<div>{{error}}</div>`
})
export class OktaCallbackComponent implements OnInit {
error: string;

constructor(private okta: OktaAuthService, private router: Router) {}
constructor(private okta: OktaAuthService, private router: Router, @Optional() private injector?: Injector) {}

async ngOnInit(): Promise<void> {
try {
// Parse code or tokens from the URL, store tokens in the TokenManager, and redirect back to the originalUri
await this.okta.handleLoginRedirect();
} catch (e) {
// Callback from social IDP. Show custom login page to continue.
if (this.okta.isInteractionRequiredError(e) && this.injector) {
const { onAuthResume, onAuthRequired } = this.okta.getOktaConfig();
const callbackFn = onAuthResume || onAuthRequired;
if (callbackFn) {
callbackFn(this.okta, this.injector);
return;
}
}
this.error = e.toString();
}
}
Expand Down
1 change: 1 addition & 0 deletions src/okta/models/okta.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface TestingObject {

export interface OktaConfig extends OktaAuthOptions {
onAuthRequired?: AuthRequiredFunction;
onAuthResume?: AuthRequiredFunction;
testing?: TestingObject;
isAuthenticated?: IsAuthenticatedFunction;
}
Expand Down
62 changes: 56 additions & 6 deletions test/spec/callback.component.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

Expand All @@ -22,11 +23,17 @@ describe('OktaCallbackComponent', () => {
href: 'https://foo',
search: '?code=fake'
};
const config = {
});
afterEach(() => {
window.location = originalLocation;
});

function bootstrap(config = {}) {
config = Object.assign({
clientId: 'foo',
issuer: 'https://foo',
redirectUri: 'https://foo'
};
}, config);

TestBed.configureTestingModule({
imports: [
Expand All @@ -46,21 +53,22 @@ describe('OktaCallbackComponent', () => {
service = TestBed.get(OktaAuthService);
fixture = TestBed.createComponent(OktaCallbackComponent);
component = fixture.componentInstance;
});
afterEach(() => {
window.location = originalLocation;
});
}

it('should create the component', async(() => {
bootstrap();
expect(component).toBeTruthy();
}));

it('should call handleLoginRedirect', async(() => {
bootstrap();
jest.spyOn(service, 'handleLoginRedirect').mockReturnValue(Promise.resolve());
fixture.detectChanges();
expect(service.handleLoginRedirect).toHaveBeenCalled();
}));

it('catches errors from handleLoginRedirect', async(() => {
bootstrap();
const error = new Error('test error');
jest.spyOn(service, 'handleLoginRedirect').mockReturnValue(Promise.reject(error));
fixture.detectChanges();
Expand All @@ -69,4 +77,46 @@ describe('OktaCallbackComponent', () => {
expect(component.error).toBe('Error: test error');
});
}));

describe('interaction code flow', () => {
it('will call `onAuthResume` function, if defined', async(() => {
const onAuthResume = jest.fn();
bootstrap({ onAuthResume });
const error = new Error('my fake error');
jest.spyOn(service, 'handleLoginRedirect').mockReturnValue(Promise.reject(error));
jest.spyOn(service, 'isInteractionRequiredError').mockReturnValue(true);
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(service.isInteractionRequiredError).toHaveBeenCalledWith(error);
expect(onAuthResume).toHaveBeenCalledWith(service, (component as any).injector);
expect(component.error).toBe(undefined);
});
}));

it('will call `onAuthRequired` function, if `onAuthResume` is not defined', async(() => {
const onAuthRequired = jest.fn();
bootstrap({ onAuthRequired });
const error = new Error('my fake error');
jest.spyOn(service, 'handleLoginRedirect').mockReturnValue(Promise.reject(error));
jest.spyOn(service, 'isInteractionRequiredError').mockReturnValue(true);
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(service.isInteractionRequiredError).toHaveBeenCalledWith(error);
expect(onAuthRequired).toHaveBeenCalledWith(service, (component as any).injector);
expect(component.error).toBe(undefined);
});
}));

it('if neither `onAuthRequired` or `onAuthResume` are defined, the error is displayed', async(() => {
bootstrap();
const error = new Error('my fake error');
jest.spyOn(service, 'handleLoginRedirect').mockReturnValue(Promise.reject(error));
jest.spyOn(service, 'isInteractionRequiredError').mockReturnValue(true);
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(service.isInteractionRequiredError).toHaveBeenCalledWith(error);
expect(component.error).toBe('Error: my fake error');
});
}));
});
});
56 changes: 34 additions & 22 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1405,17 +1405,18 @@
mkdirp "^1.0.4"
rimraf "^3.0.2"

"@okta/okta-auth-js@^4.1.0":
version "4.7.2"
resolved "https://registry.yarnpkg.com/@okta/okta-auth-js/-/okta-auth-js-4.7.2.tgz#528a0dce0dcc82d31af71d31e2a77215d3f43477"
integrity sha512-eYoE7kPKMZ5JkYAak5+AlOpX2ZVn1dGIjqVrBLdgIUxO0FX7F3ys1rqPmC+eZMfgHGiLvA9/xHMD8BdX0sck2g==
"@okta/okta-auth-js@^4.8.0":
version "4.8.0"
resolved "https://registry.yarnpkg.com/@okta/okta-auth-js/-/okta-auth-js-4.8.0.tgz#b745bc826b0df16f51f481a07a89f3387d48ab45"
integrity sha512-uaF2jS6avN0BNJ8EuJYM+KIwm3KZRo4QNAnEUDppysnxRtPlgExU2ndMEPZoSyYbCRdEd1x8UuSWTxrXhXThKQ==
dependencies:
"@babel/runtime" "^7.12.5"
Base64 "0.3.0"
Base64 "1.1.0"
core-js "^3.6.5"
cross-fetch "^3.0.6"
js-cookie "2.2.0"
node-cache "^4.2.0"
js-cookie "2.2.1"
karma-coverage "^2.0.3"
node-cache "^5.1.2"
p-cancelable "^2.0.0"
text-encoding "^0.7.0"
tiny-emitter "1.1.0"
Expand Down Expand Up @@ -2045,10 +2046,10 @@
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==

Base64@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/Base64/-/Base64-0.3.0.tgz#6da261a4e80d4fa0f5c684254e5bccd16bbdce9f"
integrity sha1-baJhpOgNT6D1xoQlTlvM0Wu9zp8=
Base64@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/Base64/-/Base64-1.1.0.tgz#810ef21afa8357df92ad7b5389188c446b9cb956"
integrity sha512-qeacf8dvGpf+XAT27ESHMh7z84uRzj/ua2pQdJg483m3bEXv/kVFtDnMgvf70BQGqzbZhR9t6BmASzKvqfJf3Q==

JSONStream@^1.3.4:
version "1.3.5"
Expand Down Expand Up @@ -6907,7 +6908,7 @@ istanbul-lib-instrument@^1.10.2:
istanbul-lib-coverage "^1.2.1"
semver "^5.3.0"

istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3:
istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.1, istanbul-lib-instrument@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d"
integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==
Expand Down Expand Up @@ -6963,7 +6964,7 @@ istanbul-reports@^1.5.1:
dependencies:
handlebars "^4.0.3"

istanbul-reports@^3.0.2:
istanbul-reports@^3.0.0, istanbul-reports@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b"
integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==
Expand Down Expand Up @@ -7416,10 +7417,10 @@ jest@^26.4.2:
import-local "^3.0.2"
jest-cli "^26.6.3"

[email protected].0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.0.tgz#1b2c279a6eece380a12168b92485265b35b1effb"
integrity sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s=
[email protected].1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==

"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
Expand Down Expand Up @@ -7619,6 +7620,18 @@ karma-coverage-istanbul-reporter@^1.2.1:
istanbul-api "^1.3.1"
minimatch "^3.0.4"

karma-coverage@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-2.0.3.tgz#c10f4711f4cf5caaaa668b1d6f642e7da122d973"
integrity sha512-atDvLQqvPcLxhED0cmXYdsPMCQuh6Asa9FMZW1bhNqlVEhJoB9qyZ2BY1gu7D/rr5GLGb5QzYO4siQskxaWP/g==
dependencies:
istanbul-lib-coverage "^3.0.0"
istanbul-lib-instrument "^4.0.1"
istanbul-lib-report "^3.0.0"
istanbul-lib-source-maps "^4.0.0"
istanbul-reports "^3.0.0"
minimatch "^3.0.4"

karma-jasmine-html-reporter@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz#48a8e5ef18807617ee2b5e33c1194c35b439524c"
Expand Down Expand Up @@ -8499,13 +8512,12 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==

node-cache@^4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-4.2.1.tgz#efd8474dee4edec4138cdded580f5516500f7334"
integrity sha512-BOb67bWg2dTyax5kdef5WfU3X8xu4wPg+zHzkvls0Q/QpYycIFRLEEIdAx9Wma43DxG6Qzn4illdZoYseKWa4A==
node-cache@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
dependencies:
clone "2.x"
lodash "^4.17.15"

node-fetch-npm@^2.0.2:
version "2.0.4"
Expand Down

0 comments on commit c264fd5

Please sign in to comment.