Skip to content
This repository has been archived by the owner on Oct 24, 2024. It is now read-only.

Commit

Permalink
feat: handle onSessionExpired, add isAuthenticated option, expose Tok…
Browse files Browse the repository at this point in the history
…enManager (#648)
  • Loading branch information
aarongranick-okta authored Jan 17, 2020
1 parent c27670c commit 40155af
Show file tree
Hide file tree
Showing 36 changed files with 1,301 additions and 1,235 deletions.
21 changes: 18 additions & 3 deletions packages/okta-angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,17 @@ An Angular InjectionToken used to configure the OktaAuthService. This value must
- `issuer` **(required)**: The OpenID Connect `issuer`
- `clientId` **(required)**: The OpenID Connect `client_id`
- `redirectUri` **(required)**: Where the callback is hosted
- `postLogoutRedirectUri` | Specify the url where the browser should be redirected after [logout](#oktaauthlogouturi). This url must be added to the list of `Logout redirect URIs` on the application's `General Settings` tab.
- `scope` *(deprecated in v1.2.2)*: Use `scopes` instead
- `scopes` *(optional)*: Reserved for custom claims to be returned in the tokens. Defaults to `['openid']`, which will only return the `sub` claim. To obtain more information about the user, use `openid profile`. For a list of scopes and claims, please see [Scope-dependent claims](https://developer.okta.com/standards/OIDC/index.html#scope-dependent-claims-not-always-returned) for more information.
- `responseType` *(optional)*: Desired token grant types. Default: `['id_token', 'token']`.
For PKCE flow, this should be left undefined or set to `['code']`.
- `pkce` *(optional)*: If `true`, PKCE flow will be used
- `onAuthRequired` *(optional)*: Accepts a callback to make a decision when authentication is required. If not supplied, `okta-angular` will redirect directly to Okta for authentication.

- `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:
1. [login](#oktaauthloginfromuri-additionalparams) is called
2. A route protected by `OktaAuthGuard` is accessed without authentication
- `onSessionExpired` *(optional)* - callback function. Called when the Okta SSO session has expired or was ended outside of the application. This SDK adds a default handler which will call [login](#oktaauthloginfromuri-additionalparams) to initiate a login flow. Passing a function here will disable the default handler.
- `isAuthenticated` *(optional)* - callback function. By default, `OktaAuthService.isAuthenticated` will return true if both `getIdToken()` and `getAccessToken()` return a value. Setting a `isAuthenticated` function on the config will skip the default logic and call the supplied function instead. The function should return a Promise and resolve to either true or false.
- `tokenManager` *(optional)*: An object containing additional properties used to configure the internal token manager. See [AuthJS TokenManager](https://github.com/okta/okta-auth-js#the-tokenmanager) for more detailed information.

- `autoRenew` *(optional)*:
Expand All @@ -118,6 +122,7 @@ For PKCE flow, this should be left undefined or set to `['code']`.
- [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)
- [`cookie`](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie)


### `OktaAuthModule`

The top-level Angular module which provides these components and services:
Expand Down Expand Up @@ -215,7 +220,7 @@ import {
...
} from '@okta/okta-angular';

export function onAuthRequired({oktaAuth, router}) {
export function onAuthRequired(oktaAuth, router) {
// Redirect the user to your custom login page
router.navigate(['/custom-login']);
}
Expand Down Expand Up @@ -281,6 +286,12 @@ export class MyComponent {
}
```

#### `oktaAuth.login(fromUri?, additionalParams?)`

Calls `onAuthRequired` function if it was set on the initial configuration. Otherwise, it will call `loginRedirect`. This method accepts a `fromUri` parameter to push the user to after successful authentication, and an optional `additionalParams` object.

For more information on `additionalParams`, see the [oktaAuth.loginRedirect](#oktaauthloginredirectfromuri-additionalparams) method below.

#### `oktaAuth.loginRedirect(fromUri?, additionalParams?)`

Performs a full page redirect to Okta based on the initial configuration. This method accepts a `fromUri` parameter to push the user to after successful authentication.
Expand Down Expand Up @@ -331,6 +342,10 @@ Used to capture the current URL state before a redirect occurs. Used primarily f

Returns the stored URI and query parameters stored when the `OktaAuthGuard` and/or `setFromUri` was used.

#### `oktaAuth.getTokenManager()`

Returns the internal [TokenManager](https://github.com/okta/okta-auth-js#tokenmanager).

## Contributing

We welcome contributions to all of our open-source packages. Please see the [contribution guide](https://github.com/okta/okta-oidc-js/blob/master/CONTRIBUTING.md) to understand how to structure a contribution.
Expand Down
3 changes: 2 additions & 1 deletion packages/okta-angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"license": "Apache-2.0",
"dependencies": {
"@okta/configuration-validation": "^0.4.1",
"@okta/okta-auth-js": "^2.11.0",
"@okta/okta-auth-js": "^2.11.2",
"tslib": "^1.9.0"
},
"devDependencies": {
Expand Down Expand Up @@ -92,6 +92,7 @@
"rxjs": ">=5.4.3"
},
"jest": {
"restoreMocks": true,
"transform": {
"^.+\\.(ts|html)$": "ts-jest"
},
Expand Down
15 changes: 0 additions & 15 deletions packages/okta-angular/src/@types/okta__okta-auth-js/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
declare module '@okta/okta-auth-js';

declare interface TokenClaims {
sub: string;
}

declare interface Token {
accessToken?: string;
idToken?: string;
claims: TokenClaims;
}

declare class TokenManager {
get(key: string):Token;
add(key: string, token: Token): void;
}

declare interface TokenAPI {
getUserInfo(accessToken: Token): Promise;
getWithRedirect(params: object): Promise;
Expand Down
17 changes: 0 additions & 17 deletions packages/okta-angular/src/okta/models/auth-required-function.ts

This file was deleted.

10 changes: 9 additions & 1 deletion packages/okta-angular/src/okta/models/okta.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@

import { InjectionToken } from '@angular/core';

import { AuthRequiredFunction } from './auth-required-function';
import { Router } from '@angular/router';

import { OktaAuthService } from '../services/okta.service';

export type AuthRequiredFunction = (oktaAuth: OktaAuthService, router: Router) => void;
export type IsAuthenticatedFunction = () => Promise<boolean>;
export type OnSessionExpiredFunction = () => void;

export interface TestingObject {
disableHttpsCheck: boolean;
Expand All @@ -36,6 +42,8 @@ export interface OktaConfig {
testing?: TestingObject;
tokenManager?: TokenManagerConfig;
postLogoutRedirectUri?: string;
isAuthenticated?: IsAuthenticatedFunction;
onSessionExpired?: OnSessionExpiredFunction;
}

export const OKTA_CONFIG = new InjectionToken<OktaConfig>('okta.config.angular');
17 changes: 17 additions & 0 deletions packages/okta-angular/src/okta/models/token-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { UserClaims } from './user-claims';

export interface AccessToken {
accessToken: string;
}

export interface IDToken {
idToken: string;
claims: UserClaims;
}

export interface TokenManager {
get(key: string): AccessToken | IDToken;
add(key: string, token: AccessToken | IDToken): void;
on(event: string, handler: Function): void;
off(event: string, handler: Function): void;
}
2 changes: 1 addition & 1 deletion packages/okta-angular/src/okta/okta.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@angular/router';

import { OktaAuthService } from './services/okta.service';
import { AuthRequiredFunction } from './models/auth-required-function';
import { AuthRequiredFunction } from './models/okta.config';

@Injectable()
export class OktaAuthGuard implements CanActivate {
Expand Down
29 changes: 27 additions & 2 deletions packages/okta-angular/src/okta/services/okta.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import {
buildConfigObject
} from '@okta/configuration-validation';

import { OKTA_CONFIG, OktaConfig } from '../models/okta.config';
import { OKTA_CONFIG, OktaConfig, AuthRequiredFunction } from '../models/okta.config';
import { UserClaims } from '../models/user-claims';
import { TokenManager } from '../models/token-manager';

import packageInfo from '../packageInfo';

Expand All @@ -46,6 +47,12 @@ export class OktaAuthService {
this.config = buildConfigObject(auth); // use normalized config object
this.config.scopes = this.config.scopes || [];

// Automatically enter login flow if session has expired or was ended outside the application
// The default behavior can be overriden by setting your own `onSessionExpired` function on the OktaConfig
if (!this.config.onSessionExpired) {
this.config.onSessionExpired = this.login.bind(this);
}

/**
* Scrub scopes to ensure 'openid' is included
*/
Expand All @@ -62,10 +69,28 @@ export class OktaAuthService {
this.$authenticationState = new Observable((observer: Observer<boolean>) => { this.observers.push(observer); });
}

login(fromUri?: string, additionalParams?: object) {
this.setFromUri(fromUri || this.router.url);
const onAuthRequired: AuthRequiredFunction | undefined = this.getOktaConfig().onAuthRequired;
if (onAuthRequired) {
return onAuthRequired(this, this.router);
}
return this.loginRedirect(undefined, additionalParams);
}

getTokenManager(): TokenManager {
return this.oktaAuth.tokenManager;
}

/**
* Checks if there is an access token and id token
*/
async isAuthenticated(): Promise<boolean> {
// Support a user-provided method to check authentication
if (this.config.isAuthenticated) {
return (this.config.isAuthenticated)();
}

const accessToken = await this.getAccessToken();
const idToken = await this.getIdToken();
return !!(accessToken || idToken);
Expand Down Expand Up @@ -147,7 +172,7 @@ export class OktaAuthService {
|| this.config.responseType
|| ['id_token', 'token'];

this.oktaAuth.token.getWithRedirect(params);
return this.oktaAuth.token.getWithRedirect(params); // can throw
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import { OktaAuthService } from '@okta/okta-angular';
selector: 'app-root',
template: `
<button id="home-button" routerLink="/"> Home </button>
<button id="login-button" *ngIf="!isAuthenticated" routerLink="/login"> Login </button>
<button id="login-button" *ngIf="!isAuthenticated" routerLink="/login" [queryParams]="{ fooParams: 'foo' }"> Login </button>
<button id="logout-button" *ngIf="isAuthenticated" (click)="logout()"> Logout </button>
<button id="protected-button" routerLink="/protected"> Protected </button>
<button id="protected-login-button" routerLink="/protected-with-data"> Protected Page w/ custom config </button>
<button id="protected-button" routerLink="/protected" [queryParams]="{ fooParams: 'foo' }"> Protected </button>
<button id="protected-login-button" routerLink="/protected-with-data"
[queryParams]="{ fooParams: 'foo' }"> Protected Page w/ custom config </button>
<router-outlet></router-outlet>
`,
Expand Down
131 changes: 131 additions & 0 deletions packages/okta-angular/test/spec/guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
jest.mock('@okta/okta-auth-js');

import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import OktaAuth from '@okta/okta-auth-js';

import {
OktaAuthModule,
OktaAuthService,
OktaAuthGuard,
} from '../../src/okta-angular';
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router, RouterState } from '@angular/router';

const VALID_CONFIG = {
clientId: 'foo',
issuer: 'https://foo',
redirectUri: 'https://foo'
};

function createService(options: any) {
options = options || {};

const oktaAuth = options.oktaAuth || {};
oktaAuth.tokenManager = oktaAuth.tokenManager || { on: jest.fn() };
OktaAuth.mockImplementation(() => oktaAuth);

TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([{ path: 'foo', redirectTo: '/foo' }]),
OktaAuthModule.initAuth(VALID_CONFIG)
],
providers: [OktaAuthService],
});
const service = TestBed.get(OktaAuthService);
service.getTokenManager = jest.fn().mockReturnValue({ on: jest.fn() });
service.isAuthenticated = jest.fn().mockReturnValue(Promise.resolve(options.isAuthenticated));
service.setFromUri = jest.fn();
service.loginRedirect = jest.fn();
return service;
}

describe('Angular auth guard', () => {

beforeEach(() => {
OktaAuth.mockClear();
});
afterEach(() => {
jest.restoreAllMocks();
});

describe('canActivate', () => {
describe('isAuthenticated() = true', () => {
it('returns true', async () => {
const service = createService({ isAuthenticated: true });
const router: unknown = undefined;
const guard = new OktaAuthGuard(service, router as Router);
const route: unknown = undefined;
const state: unknown = undefined;
const res = await guard.canActivate(route as ActivatedRouteSnapshot, state as RouterStateSnapshot);
expect(res).toBe(true);
});
});

describe('isAuthenticated() = false', () => {
let service: OktaAuthService;
let guard: OktaAuthGuard;
let state: RouterStateSnapshot;
let route: ActivatedRouteSnapshot;
let router: Router;
beforeEach(() => {
service = createService({ isAuthenticated: false });
router = TestBed.get(Router);
guard = new OktaAuthGuard(service, router);
const routerState: RouterState = router.routerState;
state = routerState.snapshot;
route = state.root;
});

it('returns false', async () => {
const config = service.getOktaConfig();
const res = await guard.canActivate(route, state);
expect(res).toBe(false);
});

it('by default, calls "loginRedirect()"', async () => {
const config = service.getOktaConfig();
const res = await guard.canActivate(route, state);
expect(service.loginRedirect).toHaveBeenCalled();
});

it('calls "setFromUri" with baseUrl and query object', async () => {
const baseUrl = 'http://fake.url/path';
const query = '?query=foo&bar=baz';
const hash = '#hash=foo';
state.url = `${baseUrl}${query}${hash}`;
const queryObj = { 'bar': 'baz' };
route.queryParams = queryObj;
const res = await guard.canActivate(route, state);
expect(service.setFromUri).toHaveBeenCalledWith(baseUrl, queryObj);
});

it('onAuthRequired can be set on route', async () => {
const fn = route.data['onAuthRequired'] = jest.fn();
const res = await guard.canActivate(route, state);
expect(fn).toHaveBeenCalledWith(service, router as Router);
});

it('onAuthRequired can be set on config', async () => {
const config = service.getOktaConfig();
const fn = config.onAuthRequired = jest.fn();

const res = await guard.canActivate(route, state);
expect(fn).toHaveBeenCalledWith(service, router as Router);
});
});
});

it('Can create the guard via angular injection', () => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([{ path: 'foo', redirectTo: '/foo' }]),
OktaAuthModule.initAuth(VALID_CONFIG)
],
providers: [OktaAuthService, OktaAuthGuard],
});
const guard = TestBed.get(OktaAuthGuard);
expect(guard.oktaAuth).toBeTruthy();
expect(guard.router).toBeTruthy();
expect(typeof guard.canActivate).toBe('function');
});
});
Loading

0 comments on commit 40155af

Please sign in to comment.