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

Commit

Permalink
feat: Expose TokenManager, handle token error events
Browse files Browse the repository at this point in the history
  • Loading branch information
aarongranick-okta committed Oct 23, 2019
1 parent e731e7f commit 43517ea
Show file tree
Hide file tree
Showing 21 changed files with 1,203 additions and 256 deletions.
77 changes: 77 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"protocol": "auto",
"name": "okta-react Jest",
"cwd": "${workspaceFolder}/packages/okta-react",
"args": [
"node_modules/.bin/jest",
"--runInBand",
"--no-cache",
"${workspaceFolder}/${relativeFile}"
],
"sourceMaps": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {

},
"sourceMapPathOverrides": {
"webpack:///*": "/*",
},
"disableOptimisticBPs": true
},
{
"type": "node",
"request": "launch",
"protocol": "auto",
"name": "okta-angular Jest",
"cwd": "${workspaceFolder}/packages/okta-angular",
"args": [
"node_modules/.bin/jest",
"--runInBand",
"--no-cache",
"${workspaceFolder}/${relativeFile}"
],
"sourceMaps": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {

},
"sourceMapPathOverrides": {
"webpack:///*": "/*",
},
"disableOptimisticBPs": true
},
{
"type": "node",
"request": "launch",
"protocol": "auto",
"name": "okta-vue Jest",
"cwd": "${workspaceFolder}/packages/okta-vue",
"args": [
"node_modules/.bin/jest",
"--runInBand",
"--no-cache",
"${workspaceFolder}/${relativeFile}"
],
"sourceMaps": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {

},
"sourceMapPathOverrides": {
"webpack:///*": "/*",
},
"disableOptimisticBPs": true
},
]
}
9 changes: 9 additions & 0 deletions packages/okta-angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ For PKCE flow, this should be left undefined or set to `['code']`.
- `autoRenew` *(optional)*:
By default, the library will attempt to renew expired tokens. When an expired token is requested by the library, a renewal request is executed to update the token. If you wish to to disable auto renewal of tokens, set autoRenew to false.

- `onTokenError` *(optional)* - callback function. If there is an error while renewing a token, the error will be passed to a handler function. The default handler calls `loginRedirect()` to initiate a login flow. Passing a function here will override 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.

### `OktaAuthModule`

The top-level Angular module which provides these components and services:
Expand Down Expand Up @@ -307,6 +311,11 @@ Returns the stored URI and query parameters stored when the `OktaAuthGuard` and/

## 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.
#### `oktaAuth.getTokenManager()`

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

## Development

### Installing dependencies for contributions
We use [yarn](https://yarnpkg.com) for dependency management when developing this package:
Expand Down
2 changes: 1 addition & 1 deletion packages/okta-angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"start": "yarn --cwd test/e2e/harness/ start",
"test": "yarn lint && yarn test:e2e && yarn test:unit",
"test:e2e": "yarn --cwd test/e2e/harness/ e2e",
"test:unit": "yarn --cwd test/e2e/harness/ test"
"test:unit": "jest"
},
"repository": "https://github.com/okta/okta-oidc-js",
"homepage": "https://github.com/okta/okta-oidc-js/tree/master/packages/okta-angular",
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 onTokenErrorFunction = (error: Error) => void;

export interface TestingObject {
disableHttpsCheck: boolean;
Expand All @@ -28,6 +34,8 @@ export interface OktaConfig {
pkce?: boolean;
onAuthRequired?: AuthRequiredFunction;
testing?: TestingObject;
isAuthenticated?: IsAuthenticatedFunction;
onTokenError?: onTokenErrorFunction;
}

export const OKTA_CONFIG = new InjectionToken<OktaConfig>('okta.config.angular');
3 changes: 3 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,3 @@
export interface TokenManager {
on: Function;
}
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
21 changes: 21 additions & 0 deletions packages/okta-angular/src/okta/services/okta.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import packageInfo from '../packageInfo';
*/
import OktaAuth from '@okta/okta-auth-js';
import { Observable, Observer } from 'rxjs';
import { TokenManager } from '../models/token-manager';

@Injectable()
export class OktaAuthService {
Expand Down Expand Up @@ -60,12 +61,32 @@ export class OktaAuthService {
this.oktaAuth = new OktaAuth(this.config);
this.oktaAuth.userAgent = `${packageInfo.name}/${packageInfo.version} ${this.oktaAuth.userAgent}`;
this.$authenticationState = new Observable((observer: Observer<boolean>) => { this.observers.push(observer); });

// Automatically enters login flow if token renew fails.
// The default behavior can be overriden by passing a function via config: `config.onTokenError`
this.getTokenManager().on('error', this.config.onTokenError || this._onTokenError.bind(this));
}

// Handle token manager errors: Default implementation
_onTokenError(error) {
if (error.errorCode === 'login_required') {
this.loginRedirect();
}
}

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
128 changes: 128 additions & 0 deletions packages/okta-angular/test/spec/guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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';
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router, RouterState } from '@angular/router';

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

function createService(options) {
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 guard = new OktaAuthGuard(service, null);
const res = await guard.canActivate(null, null);
expect(res).toBe(true);
});
});

describe('isAuthenticated() = false', () => {
let service: OktaAuthService;
let guard: OktaAuthGuard;
let state: RouterStateSnapshot;
let route: ActivatedRouteSnapshot;
let 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);
});

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);
});
});
});

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 43517ea

Please sign in to comment.