diff --git a/CHANGELOG.md b/CHANGELOG.md
index 395521fc..12ec9997 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/README.md b/README.md
index d0974cd4..3e4ab45f 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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'
}
@@ -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`
@@ -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`.
diff --git a/package.json b/package.json
index 39e759ad..bb00b8a9 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/okta/components/callback.component.ts b/src/okta/components/callback.component.ts
index ecf85ea3..87cd71b3 100644
--- a/src/okta/components/callback.component.ts
+++ b/src/okta/components/callback.component.ts
@@ -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: `
{{error}}
`
})
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 {
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();
}
}
diff --git a/src/okta/models/okta.config.ts b/src/okta/models/okta.config.ts
index 6aad5eca..1b388f44 100644
--- a/src/okta/models/okta.config.ts
+++ b/src/okta/models/okta.config.ts
@@ -23,6 +23,7 @@ export interface TestingObject {
export interface OktaConfig extends OktaAuthOptions {
onAuthRequired?: AuthRequiredFunction;
+ onAuthResume?: AuthRequiredFunction;
testing?: TestingObject;
isAuthenticated?: IsAuthenticatedFunction;
}
diff --git a/test/spec/callback.component.test.ts b/test/spec/callback.component.test.ts
index 9b267b62..e080670d 100644
--- a/test/spec/callback.component.test.ts
+++ b/test/spec/callback.component.test.ts
@@ -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';
@@ -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: [
@@ -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();
@@ -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');
+ });
+ }));
+ });
});
diff --git a/yarn.lock b/yarn.lock
index ab8beb5d..45561ae3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"
@@ -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"
@@ -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==
@@ -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==
@@ -7416,10 +7417,10 @@ jest@^26.4.2:
import-local "^3.0.2"
jest-cli "^26.6.3"
-js-cookie@2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.0.tgz#1b2c279a6eece380a12168b92485265b35b1effb"
- integrity sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s=
+js-cookie@2.2.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"
@@ -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"
@@ -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"