diff --git a/.eslintignore b/.eslintignore index bd07d4e1..4f30f921 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,4 @@ node_modules /dist +src/@types +src/okta/models diff --git a/README.md b/README.md index 67396170..e5212c23 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,9 @@ All of these features are supported by this SDK. Additionally, using this SDK, y This library currently supports: - [OAuth 2.0 Implicit Flow](https://tools.ietf.org/html/rfc6749#section-1.3.2) -- [OAuth 2.0 Authorization Code Flow](https://tools.ietf.org/html/rfc6749#section-1.3.1) with [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636) +- [OAuth 2.0 Authorization Code Flow](https://tools.ietf.org/html/rfc6749#section-1.3.1) with [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636) > This library has been tested for compatibility with the following Angular versions: 4, 5, 6, 7, 8, 9 -> :warning: Angular versions older than 7 may not be fully compatible with all dependencies of this library, due to an older Typescript version. You may be able to workaround this issue by setting `skipLibChecks: true` in your `tsconfig.json` file. ## Getting Started @@ -315,9 +314,7 @@ const oktaConfig = { ### `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-js](https://github.com/okta/okta-auth-js) 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`. - -The example below shows connecting two buttons to handle **login** and **logout**: +In your components, your can take advantage of all of `okta-angular`'s features by importing the `OktaAuthService`. The example below shows connecting two buttons to handle **login** and **logout**: ```typescript // sample.component.ts diff --git a/package.json b/package.json index 61935169..81361274 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@okta/okta-angular", "private": true, - "version": "3.0.0", + "version": "2.2.1", "description": "Angular support for Okta", "main": "./dist/bundles/okta-angular.umd.js", "module": "./dist/fesm5/okta-angular.js", @@ -42,8 +42,8 @@ ], "license": "Apache-2.0", "dependencies": { - "@okta/configuration-validation": "^1.0.0", - "@okta/okta-auth-js": "^4.0.0", + "@okta/configuration-validation": "^0.4.1", + "@okta/okta-auth-js": "^3.2.3", "tslib": "^1.9.0" }, "devDependencies": { @@ -109,7 +109,7 @@ "jest-junit" ], "moduleNameMapper": { - "@okta/okta-auth-js": "/node_modules/@okta/okta-auth-js/dist/okta-auth-js.umd.js" + "@okta/okta-auth-js": "/node_modules/@okta/okta-auth-js/dist/okta-auth-js.min.js" }, "restoreMocks": true, "transform": { diff --git a/rollup.config.js b/rollup.config.js index 14bc7a3b..cc0f6755 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -47,6 +47,12 @@ export default { }), commonjs({ namedExports: { + '@okta/configuration-validation': [ + 'assertIssuer', + 'assertClientId', + 'assertRedirectUri', + 'buildConfigObject' + ] } }), sourcemaps() diff --git a/src/@types/okta__configuration-validation/index.d.ts b/src/@types/okta__configuration-validation/index.d.ts new file mode 100644 index 00000000..a1b7f889 --- /dev/null +++ b/src/@types/okta__configuration-validation/index.d.ts @@ -0,0 +1 @@ +declare module '@okta/configuration-validation'; \ No newline at end of file diff --git a/src/@types/okta__okta-auth-js/index.d.ts b/src/@types/okta__okta-auth-js/index.d.ts new file mode 100644 index 00000000..3afb49de --- /dev/null +++ b/src/@types/okta__okta-auth-js/index.d.ts @@ -0,0 +1,22 @@ +declare module '@okta/okta-auth-js'; + +declare interface TokenHash { + [key: string] : Token; +} +declare interface ParseFromUrlResponse { + tokens: TokenHash; + state: string; +} + +declare interface TokenAPI { + getUserInfo(accessToken?: AccessToken, idToken?: IDToken): Promise; + getWithRedirect(params?: object): Promise; + parseFromUrl(): ParseFromUrlResponse; +} + +declare class OktaAuth { + userAgent: string; + tokenManager: TokenManager; + token: TokenAPI; + signOut(options: object): Promise; +} \ No newline at end of file diff --git a/src/okta-angular.ts b/src/okta-angular.ts index 44c88903..93070264 100644 --- a/src/okta-angular.ts +++ b/src/okta-angular.ts @@ -14,6 +14,7 @@ export { OktaAuthModule } from './okta/okta.module'; export { OktaAuthGuard } from './okta/okta.guard'; export { OktaAuthService } from './okta/services/okta.service'; export { OktaConfig, OKTA_CONFIG } from './okta/models/okta.config'; +export { UserClaims } from './okta/models/user-claims'; // Okta View Components export { OktaCallbackComponent } from './okta/components/callback.component'; diff --git a/src/okta/models/okta.config.ts b/src/okta/models/okta.config.ts index 6aad5eca..c7fd9548 100644 --- a/src/okta/models/okta.config.ts +++ b/src/okta/models/okta.config.ts @@ -12,19 +12,35 @@ import { InjectionToken, Injector } from '@angular/core'; import { OktaAuthService } from '../services/okta.service'; -import { OktaAuthOptions } from '@okta/okta-auth-js'; export type AuthRequiredFunction = (oktaAuth: OktaAuthService, injector: Injector) => void; export type IsAuthenticatedFunction = (oktaAuth: OktaAuthService) => Promise; +export type OnSessionExpiredFunction = () => void; export interface TestingObject { disableHttpsCheck: boolean; } -export interface OktaConfig extends OktaAuthOptions { +export interface TokenManagerConfig { + autoRenew?: boolean; + secure?: boolean; + storage?: string; +} + +export interface OktaConfig { + issuer?: string; + redirectUri?: string; + clientId?: string; + scope?: string; + scopes?: string[]; + responseType?: any; // can be string or array + pkce?: boolean; onAuthRequired?: AuthRequiredFunction; testing?: TestingObject; + tokenManager?: TokenManagerConfig; + postLogoutRedirectUri?: string; isAuthenticated?: IsAuthenticatedFunction; + onSessionExpired?: OnSessionExpiredFunction; } export const OKTA_CONFIG = new InjectionToken('okta.config.angular'); diff --git a/src/okta/models/token-manager.ts b/src/okta/models/token-manager.ts new file mode 100644 index 00000000..ab8c447d --- /dev/null +++ b/src/okta/models/token-manager.ts @@ -0,0 +1,19 @@ +import { UserClaims } from './user-claims'; + +export interface AccessToken { + accessToken: string; +} + +export interface IDToken { + idToken: string; + claims: UserClaims; +} + +export type Token = AccessToken | IDToken; + +export interface TokenManager { + get(key: string): Token; + add(key: string, token: Token): void; + on(event: string, handler: Function): void; + off(event: string, handler: Function): void; +} diff --git a/src/okta/models/user-claims.ts b/src/okta/models/user-claims.ts new file mode 100644 index 00000000..569efbfd --- /dev/null +++ b/src/okta/models/user-claims.ts @@ -0,0 +1,27 @@ +/** + * + * This interface represents the union of possible known claims that are in an + * ID Token or returned from the /userinfo response and depend on the + * response_type and scope parameters in the authorize request + */ +export interface UserClaims { + auth_time?: Number; + aud?: string; + email?: string; + email_verified?: Boolean; + exp?: Number; + family_name?: string; + given_name?: string; + iat?: Number; + iss?: string; + jti?: string; + locale?: string; + name?: string; + nonce?: string; + preferred_username?: string; + sub: string; + updated_at?: Number; + ver?: Number; + zoneinfo?: string; + [propName: string]: any; // For custom claims that may be configured by the org admin +} diff --git a/src/okta/services/okta.service.ts b/src/okta/services/okta.service.ts index 9244f9d8..81ece4b7 100644 --- a/src/okta/services/okta.service.ts +++ b/src/okta/services/okta.service.ts @@ -18,6 +18,8 @@ import { } from '@okta/configuration-validation'; import { OKTA_CONFIG, OktaConfig, AuthRequiredFunction } from '../models/okta.config'; +import { UserClaims } from '../models/user-claims'; +import { TokenManager, AccessToken, IDToken } from '../models/token-manager'; // eslint-disable-next-line node/no-unpublished-import import packageInfo from '../packageInfo'; @@ -25,53 +27,43 @@ import packageInfo from '../packageInfo'; /** * Import the okta-auth-js library */ -import { OktaAuth, TokenManager, AccessToken, IDToken, UserClaims, SignoutOptions } from '@okta/okta-auth-js'; +import OktaAuth from '@okta/okta-auth-js'; import { Observable, Observer } from 'rxjs'; -/** - * Scrub scopes to ensure 'openid' is included - * @param scopes - */ -function scrubScopes(scopes: string[]): void { - if (scopes.indexOf('openid') >= 0) { - return; - } - scopes.unshift('openid'); -} - @Injectable() -export class OktaAuthService extends OktaAuth { +export class OktaAuthService { + private oktaAuth: OktaAuth; private config: OktaConfig; private observers: Observer[]; - private injector: Injector; - $authenticationState: Observable; - constructor(@Inject(OKTA_CONFIG) config: OktaConfig, injector: Injector) { - config = Object.assign({}, config); - config.scopes = config.scopes || ['openid', 'email']; + constructor(@Inject(OKTA_CONFIG) config: OktaConfig, private injector: Injector) { + this.observers = []; - // Scrub scopes to ensure 'openid' is included - scrubScopes(config.scopes); + /** + * Cache the auth config. + */ + this.config = Object.assign({}, config); + this.config.scopes = this.config.scopes || ['openid', 'email']; - // Assert Configuration - assertIssuer(config.issuer, config.testing); - assertClientId(config.clientId); - assertRedirectUri(config.redirectUri); + /** + * Scrub scopes to ensure 'openid' is included + */ - super(config); - this.config = config; - this.injector = injector; + this.scrubScopes(this.config.scopes); - // Customize user agent - this.userAgent = `${packageInfo.name}/${packageInfo.version} ${this.userAgent}`; + // Assert Configuration + assertIssuer(this.config.issuer, this.config.testing); + assertClientId(this.config.clientId); + assertRedirectUri(this.config.redirectUri); - // Initialize observers - this.observers = []; + this.oktaAuth = new OktaAuth(this.config); + this.oktaAuth.userAgent = `${packageInfo.name}/${packageInfo.version} ${this.oktaAuth.userAgent}`; this.$authenticationState = new Observable((observer: Observer) => { this.observers.push(observer); }); } - async login(fromUri?: string, additionalParams?: Record): Promise { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + login(fromUri?: string, additionalParams?: Record) { this.setFromUri(fromUri); const onAuthRequired: AuthRequiredFunction | undefined = this.config.onAuthRequired; if (onAuthRequired) { @@ -81,7 +73,7 @@ export class OktaAuthService extends OktaAuth { } getTokenManager(): TokenManager { - return this.tokenManager; + return this.oktaAuth.tokenManager; } /** @@ -108,7 +100,7 @@ export class OktaAuthService extends OktaAuth { */ async getAccessToken(): Promise { try { - const accessToken: AccessToken = await this.tokenManager.get('accessToken') as AccessToken; + const accessToken: AccessToken = await this.oktaAuth.tokenManager.get('accessToken') as AccessToken; return accessToken.accessToken; } catch (err) { // The user no longer has an existing SSO session in the browser. @@ -123,7 +115,7 @@ export class OktaAuthService extends OktaAuth { */ async getIdToken(): Promise { try { - const idToken: IDToken = await this.tokenManager.get('idToken') as IDToken; + const idToken: IDToken = await this.oktaAuth.tokenManager.get('idToken') as IDToken; return idToken.idToken; } catch (err) { // The user no longer has an existing SSO session in the browser. @@ -137,7 +129,7 @@ export class OktaAuthService extends OktaAuth { * Returns user claims from the /userinfo endpoint. */ async getUser(): Promise { - return this.token.getUserInfo(); + return this.oktaAuth.token.getUserInfo(); } /** @@ -152,7 +144,8 @@ export class OktaAuthService extends OktaAuth { * @param fromUri * @param additionalParams */ - async loginRedirect(fromUri?: string, additionalParams?: Record): Promise { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + loginRedirect(fromUri?: string, additionalParams?: Record) { if (fromUri) { this.setFromUri(fromUri); } @@ -162,7 +155,7 @@ export class OktaAuthService extends OktaAuth { responseType: this.config.responseType }, additionalParams); - return this.token.getWithRedirect(params); // can throw + return this.oktaAuth.token.getWithRedirect(params); // can throw } /** @@ -193,13 +186,13 @@ export class OktaAuthService extends OktaAuth { * Parses the tokens from the callback URL. */ async handleAuthentication(): Promise { - const res = await this.token.parseFromUrl(); + const res = await this.oktaAuth.token.parseFromUrl(); const tokens = res.tokens; if (tokens.accessToken) { - this.tokenManager.add('accessToken', tokens.accessToken as AccessToken); + this.oktaAuth.tokenManager.add('accessToken', tokens.accessToken as AccessToken); } if (tokens.idToken) { - this.tokenManager.add('idToken', tokens.idToken as IDToken); + this.oktaAuth.tokenManager.add('idToken', tokens.idToken as IDToken); } if (await this.isAuthenticated()) { this.emitAuthenticationState(true); @@ -211,7 +204,8 @@ export class OktaAuthService extends OktaAuth { * tokens stored in the tokenManager. * @param options */ - async logout(options?: string | SignoutOptions): Promise { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any + async logout(options?: any): Promise { let redirectUri = null; options = options || {}; if (typeof options === 'string') { @@ -224,9 +218,18 @@ export class OktaAuthService extends OktaAuth { postLogoutRedirectUri: redirectUri }; } - await this.signOut(options); + await this.oktaAuth.signOut(options); this.emitAuthenticationState(false); } - + /** + * Scrub scopes to ensure 'openid' is included + * @param scopes + */ + scrubScopes(scopes: string[]): void { + if (scopes.indexOf('openid') >= 0) { + return; + } + scopes.unshift('openid'); + } } diff --git a/test/e2e/harness/src/app/sessionToken-login.component.ts b/test/e2e/harness/src/app/sessionToken-login.component.ts index 038290de..1d206517 100644 --- a/test/e2e/harness/src/app/sessionToken-login.component.ts +++ b/test/e2e/harness/src/app/sessionToken-login.component.ts @@ -13,7 +13,7 @@ import { Component } from '@angular/core'; import { OktaAuthService } from '@okta/okta-angular'; -import { OktaAuth } from '@okta/okta-auth-js'; +import OktaAuth from '@okta/okta-auth-js'; @Component({ selector: 'app-session-login', diff --git a/test/spec/guard.test.ts b/test/spec/guard.test.ts index a25b3315..02e463f6 100644 --- a/test/spec/guard.test.ts +++ b/test/spec/guard.test.ts @@ -1,5 +1,8 @@ +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, @@ -19,6 +22,10 @@ const VALID_CONFIG = { 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' }]), @@ -42,6 +49,9 @@ function createService(options: any) { describe('Angular auth guard', () => { + beforeEach(() => { + OktaAuth.mockClear(); + }); afterEach(() => { jest.restoreAllMocks(); }); diff --git a/test/spec/service.test.ts b/test/spec/service.test.ts index c5ab7d02..ae33c3d8 100644 --- a/test/spec/service.test.ts +++ b/test/spec/service.test.ts @@ -1,7 +1,10 @@ + +jest.mock('@okta/okta-auth-js'); + import { TestBed } from '@angular/core/testing'; -import { TokenResponse, AccessToken, IDToken } from '@okta/okta-auth-js'; +import OktaAuth from '@okta/okta-auth-js'; -const PACKAGE_JSON = require('../../package.json'); +import PACKAGE_JSON from '../../package.json'; import { OktaAuthModule, @@ -12,6 +15,7 @@ import { import { Injector } from '@angular/core'; describe('Angular service', () => { + let _mockAuthJS: any; let VALID_CONFIG: OktaConfig; beforeEach(() => { @@ -20,17 +24,34 @@ describe('Angular service', () => { issuer: 'https://foo', redirectUri: 'https://foo' }; + OktaAuth.mockClear(); + _mockAuthJS = { + tokenManager: { + on: jest.fn() + } + }; }); afterEach(() => { jest.restoreAllMocks(); }); + function extendMockAuthJS(mockAuthJS: any) { + mockAuthJS = mockAuthJS || {}; + mockAuthJS.tokenManager = Object.assign({}, mockAuthJS.tokenManager, { + on: jest.fn() + }); + mockAuthJS.token = Object.assign({}, mockAuthJS.token, { + getWithRedirect: jest.fn() + }); + return mockAuthJS; + } function extendConfig(config: object) { return Object.assign({}, VALID_CONFIG, config); } const createInstance = (params = {}) => { + OktaAuth.mockImplementation(() => _mockAuthJS); const injector: unknown = undefined; return () => new OktaAuthService(params, injector as Injector); }; @@ -115,7 +136,7 @@ describe('Angular service', () => { it('Adds a user agent on internal oktaAuth instance', () => { const service = createInstance(VALID_CONFIG)(); - expect(service.userAgent.indexOf(`@okta/okta-angular/${PACKAGE_JSON.version}`)).toBeGreaterThan(-1); + expect(service['oktaAuth'].userAgent.indexOf(`@okta/okta-angular/${PACKAGE_JSON.version}`)).toBeGreaterThan(-1); }); it('Can create the service via angular injection', () => { @@ -147,6 +168,7 @@ describe('Angular service', () => { describe('service methods', () => { function createService(config?: object, mockAuthJS = null) { + OktaAuth.mockImplementation(() => extendMockAuthJS(mockAuthJS)); config = extendConfig(config || {}); TestBed.configureTestingModule({ imports: [ @@ -160,10 +182,7 @@ describe('Angular service', () => { }, ], }); - const service = TestBed.get(OktaAuthService); - jest.spyOn(service.token, 'getWithRedirect').mockImplementation(() => {}); - jest.spyOn(service.tokenManager, 'on').mockReturnValue(undefined); - return service; + return TestBed.get(OktaAuthService); } describe('TokenManager', () => { @@ -171,24 +190,27 @@ describe('Angular service', () => { const service = createService(); const val = service.getTokenManager(); expect(val).toBeTruthy(); - expect(val).toBe(service.tokenManager); + expect(val).toBe(service.oktaAuth.tokenManager); }); }); describe('onSessionExpired', () => { it('By default, "onSessionExpired" is undefined', () => { + jest.spyOn(OktaAuthService.prototype, 'login').mockReturnValue(undefined); const service = createService(); const config = service.getOktaConfig(); expect(config.onSessionExpired).toBeUndefined(); }); it('Accepts custom function "onSessionExpired" via config which disables default handler', () => { + jest.spyOn(OktaAuthService.prototype, 'login').mockReturnValue(undefined); const onSessionExpired = jest.fn(); const service = createService({ onSessionExpired }); const config = service.getOktaConfig(); expect(config.onSessionExpired).toBe(onSessionExpired); config.onSessionExpired(); expect(onSessionExpired).toHaveBeenCalled(); + expect(OktaAuthService.prototype.login).not.toHaveBeenCalled(); }); }); @@ -238,21 +260,29 @@ describe('Angular service', () => { const mockToken = { accessToken: 'foo' }; - const service = createService(undefined); - jest.spyOn(service.tokenManager, 'get').mockImplementation(key => { - expect(key).toBe('accessToken'); - return Promise.resolve(mockToken); + const mockAuthJS = extendMockAuthJS({ + tokenManager: { + get: jest.fn().mockImplementation(key => { + expect(key).toBe('accessToken'); + return Promise.resolve(mockToken); + }) + } }); + const service = createService(undefined, mockAuthJS); const retVal = await service.getAccessToken(); expect(retVal).toBe(mockToken.accessToken); }); it('catches exceptions', async () => { - const service = createService(undefined); - jest.spyOn(service.tokenManager, 'get').mockImplementation(key => { - expect(key).toBe('accessToken'); - throw new Error('expected test error'); + const mockAuthJS = extendMockAuthJS({ + tokenManager: { + get: jest.fn().mockImplementation(key => { + expect(key).toBe('accessToken'); + throw new Error('expected test error'); + }) + } }); + const service = createService(undefined, mockAuthJS); const retVal = await service.getAccessToken(); expect(retVal).toBe(undefined); }); @@ -263,21 +293,29 @@ describe('Angular service', () => { const mockToken = { idToken: 'foo' }; - const service = createService(undefined); - jest.spyOn(service.tokenManager, 'get').mockImplementation(key => { - expect(key).toBe('idToken'); - return Promise.resolve(mockToken); + const mockAuthJS = extendMockAuthJS({ + tokenManager: { + get: jest.fn().mockImplementation(key => { + expect(key).toBe('idToken'); + return Promise.resolve(mockToken); + }) + } }); + const service = createService(undefined, mockAuthJS); const retVal = await service.getIdToken(); expect(retVal).toBe(mockToken.idToken); }); it('catches exceptions', async () => { - const service = createService(undefined); - jest.spyOn(service.tokenManager, 'get').mockImplementation(key => { - expect(key).toBe('idToken'); - throw new Error('expected test error'); + const mockAuthJS = extendMockAuthJS({ + tokenManager: { + get: jest.fn().mockImplementation(key => { + expect(key).toBe('idToken'); + throw new Error('expected test error'); + }) + } }); + const service = createService(undefined, mockAuthJS); const retVal = await service.getIdToken(); expect(retVal).toBe(undefined); }); @@ -288,15 +326,23 @@ describe('Angular service', () => { const userInfo = { sub: 'test-sub', }; - const service = createService(undefined); - jest.spyOn(service.token, 'getUserInfo').mockResolvedValueOnce(userInfo); + const mockAuthJS = extendMockAuthJS({ + token: { + getUserInfo: jest.fn().mockResolvedValueOnce(userInfo), + } + }); + const service = createService(undefined, mockAuthJS); const retVal = await service.getUser(); expect(retVal).toBe(userInfo); }); it('should throw error when AuthJS getUserInfo cannot resolve userInfo', async () => { - const service = createService(undefined); - jest.spyOn(service.token, 'getUserInfo').mockRejectedValueOnce(new Error('mock error')); + const mockAuthJS = extendMockAuthJS({ + token: { + getUserInfo: jest.fn().mockRejectedValueOnce(new Error('mock error')), + } + }); + const service = createService(undefined, mockAuthJS); try { await service.getUser(); } catch (err) { @@ -363,12 +409,13 @@ describe('Angular service', () => { describe('login', () => { const expectedRes = 'sometestresult'; beforeEach(() => { - jest.spyOn(OktaAuthService.prototype, 'loginRedirect').mockReturnValue(Promise.resolve()); + jest.spyOn(OktaAuthService.prototype, 'loginRedirect').mockReturnValue(expectedRes); jest.spyOn(OktaAuthService.prototype, 'setFromUri').mockReturnValue(undefined); }); - it('calls loginRedirect by default', async () => { + it('calls loginRedirect by default', () => { const service = createService(); - await service.login(); + const res = service.login(); + expect(res).toBe(expectedRes); expect(OktaAuthService.prototype.loginRedirect).toHaveBeenCalled(); }); @@ -400,12 +447,12 @@ describe('Angular service', () => { expect(OktaAuthService.prototype.setFromUri).toHaveBeenCalledWith(undefined); }); - it('Passes "additionalParams" to loginRedirect', async () => { - jest.spyOn(OktaAuthService.prototype, 'loginRedirect').mockReturnValue(Promise.resolve()); + it('Passes "additionalParams" to loginRedirect', () => { + jest.spyOn(OktaAuthService.prototype, 'loginRedirect').mockReturnValue(null); const service = createService(); const fromUri = 'https://foo.random'; const additionalParams = { foo: 'bar', baz: 'biz' }; - await service.login(fromUri, additionalParams); + service.login(fromUri, additionalParams); expect(OktaAuthService.prototype.loginRedirect).toHaveBeenCalledWith(undefined, additionalParams); expect(OktaAuthService.prototype.setFromUri).toHaveBeenCalledWith(fromUri); }); @@ -433,7 +480,7 @@ describe('Angular service', () => { const service = createService({ responseType: ['fake'], scopes: ['openid', 'fake']}); const uri = 'https://foo.random'; await service.loginRedirect(uri); - expect(service.token.getWithRedirect).toHaveBeenCalledWith({ + expect(service.oktaAuth.token.getWithRedirect).toHaveBeenCalledWith({ responseType: ['fake'], scopes: ['openid', 'fake'], }); @@ -451,7 +498,7 @@ describe('Angular service', () => { } }; await service.loginRedirect(uri, params); - expect(service.token.getWithRedirect).toHaveBeenCalledWith(params); + expect(service.oktaAuth.token.getWithRedirect).toHaveBeenCalledWith(params); }); @@ -464,7 +511,7 @@ describe('Angular service', () => { const uri = 'https://foo.random'; await service.loginRedirect(uri); - expect(service.token.getWithRedirect).toHaveBeenCalledWith(params); + expect(service.oktaAuth.token.getWithRedirect).toHaveBeenCalledWith(params); }); @@ -480,13 +527,13 @@ describe('Angular service', () => { responseType: ['also', 'different'], }; await service.loginRedirect(uri, params2); - expect(service.token.getWithRedirect).toHaveBeenCalledWith(params2); + expect(service.oktaAuth.token.getWithRedirect).toHaveBeenCalledWith(params2); }); }); describe('handleAuthentication', () => { let service: OktaAuthService; - let response: TokenResponse; + let response: ParseFromUrlResponse; let isAuthenticated: boolean; beforeEach(() => { response = { @@ -494,25 +541,31 @@ describe('Angular service', () => { state: 'abc' }; isAuthenticated = false; - service = createService(undefined); + const mockAuthJS = extendMockAuthJS({ + token: { + parseFromUrl: jest.fn().mockImplementation(() => response) + }, + tokenManager: { + add: jest.fn() + } + }); + service = createService(undefined, mockAuthJS); jest.spyOn(service, 'isAuthenticated').mockImplementation(() => Promise.resolve(isAuthenticated)); jest.spyOn(service as any, 'emitAuthenticationState'); - jest.spyOn(service.token, 'parseFromUrl').mockImplementation(() => Promise.resolve(response)); - jest.spyOn(service.tokenManager, 'add'); }); it('calls parseFromUrl', async () => { await service.handleAuthentication(); - expect((service as any).token.parseFromUrl).toHaveBeenCalled(); + expect((service as any).oktaAuth.token.parseFromUrl).toHaveBeenCalled(); }); it('stores tokens', async () => { - const accessToken = { accessToken: 'foo', scopes: ['foo'], expiresAt: 1 } as AccessToken; - const idToken = { idToken: 'bar', scopes: ['foo'], expiresAt: 1 } as IDToken; + const accessToken = { accessToken: 'foo' }; + const idToken = { idToken: 'bar' }; response.tokens = { accessToken, idToken }; await service.handleAuthentication(); - expect((service as any).tokenManager.add).toHaveBeenNthCalledWith(1, 'accessToken', accessToken); - expect((service as any).tokenManager.add).toHaveBeenNthCalledWith(2, 'idToken', idToken); + expect((service as any).oktaAuth.tokenManager.add).toHaveBeenNthCalledWith(1, 'accessToken', accessToken); + expect((service as any).oktaAuth.tokenManager.add).toHaveBeenNthCalledWith(2, 'idToken', idToken); }); it('isAuthenticated (false): does not authenticated state', async () => { @@ -533,16 +586,20 @@ describe('Angular service', () => { let mockAuthJS: any; function bootstrap(config?: any) { + mockAuthJS = extendMockAuthJS({ + signOut: jest.fn().mockReturnValue(Promise.resolve()), + tokenManager: { + clear: jest.fn(), + } + }); service = createService(config, mockAuthJS); jest.spyOn(service as any, 'emitAuthenticationState'); - jest.spyOn(service, 'signOut').mockReturnValue(Promise.resolve()); - jest.spyOn(service.tokenManager, 'clear'); } - it('calls signOut', async () => { + it('calls oktaAuth.signOut', async () => { bootstrap(); await service.logout(); - expect((service as any).signOut).toHaveBeenCalled(); + expect((service as any).oktaAuth.signOut).toHaveBeenCalled(); }); it('emits authentication state', async () => { @@ -554,14 +611,14 @@ describe('Angular service', () => { it('can be called with no options', async () => { bootstrap(); await service.logout(); - expect((service as any).signOut).toHaveBeenCalledWith({}); + expect((service as any).oktaAuth.signOut).toHaveBeenCalledWith({}); }); it('accepts an argument for uri to redirect to', async () => { bootstrap(); const uri = 'https://my.custom.uri'; await service.logout(uri); - expect((service as any).signOut).toHaveBeenCalledWith({ + expect((service as any).oktaAuth.signOut).toHaveBeenCalledWith({ postLogoutRedirectUri: uri }); }); @@ -570,7 +627,7 @@ describe('Angular service', () => { bootstrap(); const uri = '/relative-path'; await service.logout(uri); - expect((service as any).signOut).toHaveBeenCalledWith({ + expect((service as any).oktaAuth.signOut).toHaveBeenCalledWith({ postLogoutRedirectUri: window.location.origin + uri }); }); @@ -580,7 +637,7 @@ describe('Angular service', () => { bootstrap(); const options = { postLogoutRedirectUri: 'bar' }; await service.logout(options); - expect((service as any).signOut).toHaveBeenCalledWith(options); + expect((service as any).oktaAuth.signOut).toHaveBeenCalledWith(options); }); it('Returns a promise', async () => { @@ -594,7 +651,7 @@ describe('Angular service', () => { it('Can throw', async () => { bootstrap(); const testError = new Error('test error'); - (service.signOut as any).mockReturnValue(Promise.reject(testError)); + mockAuthJS.signOut.mockReturnValue(Promise.reject(testError)); return service.logout() .catch(e => { expect(e).toBe(testError); diff --git a/test/spec/tsconfig.json b/test/spec/tsconfig.json index 9398fdcd..dc55bc1d 100644 --- a/test/spec/tsconfig.json +++ b/test/spec/tsconfig.json @@ -4,7 +4,7 @@ "./", "../support/tsconfig.spec.json" ], "compilerOptions": { - "types": ["node", "jest"], + "types": ["node", "jest", "okta__okta-auth-js", "okta__configuration-validation"], "resolveJsonModule": true } } \ No newline at end of file diff --git a/tsconfig-build.json b/tsconfig-build.json index fb0665b3..41aada8f 100644 --- a/tsconfig-build.json +++ b/tsconfig-build.json @@ -21,10 +21,11 @@ "es2015", "dom" ], + "skipLibCheck": true, "typeRoots": [ - "node_modules/@types/" + "node_modules/@types/", + "src/@types/" ], - "types": ["node"], "esModuleInterop": false, "experimentalDecorators": true, "emitDecoratorMetadata": true, diff --git a/tsconfig.json b/tsconfig.json index 43407993..287ebe84 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "baseUrl": ".", "emitDecoratorMetadata": true, - "esModuleInterop": false, + "esModuleInterop": true, "experimentalDecorators": true, "strict": true, "strictPropertyInitialization": false, @@ -18,7 +18,8 @@ "dom" ], "typeRoots": [ - "node_modules/@types/" + "node_modules/@types/", + "src/@types/" ] }, "exclude": [ diff --git a/yarn.lock b/yarn.lock index bf29eb6f..b2b46898 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1527,27 +1527,23 @@ dependencies: mkdirp "^1.0.4" -"@okta/configuration-validation@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@okta/configuration-validation/-/configuration-validation-1.0.0.tgz#18d6220d139e7f5f3d203e11849e1ee61ca3cea1" - integrity sha512-CQaBdqP9fyYd2KwIgwwhWDivWx+Lwj7rtyXUbTlAHywKRwnEyNln7zUG6UaBBSRNK63I+W3A3GgPWO0gCpo1FA== +"@okta/configuration-validation@^0.4.1": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@okta/configuration-validation/-/configuration-validation-0.4.3.tgz#5756695979c54f9058dd36fb5a87c1c8f463df7e" + integrity sha512-dn/1EMGhwajQwV/jNIrj6zvYDdDpFIQnrdqRAww/SvzIz9cuPbw1vYBRpBYX2RQpPhJJuy6iZf0XB6yWVDUavw== dependencies: - "@okta/okta-auth-js" "^4.0.0" lodash "^4.17.15" -"@okta/okta-auth-js@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@okta/okta-auth-js/-/okta-auth-js-4.0.0.tgz#1c9b5518678e29e1e799c59556881bf6d61c01d6" - integrity sha512-Iy0iIuZHFJMcxYDuW7ZbQ0TgSPmGZzvNrMoTKAWseW77v0ez1iBLqxU71Ymuzpvr1Cw2SWnUltcs+vZQtpHFug== +"@okta/okta-auth-js@^3.2.3": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@okta/okta-auth-js/-/okta-auth-js-3.2.5.tgz#46ab9f35be37fc58e970997342799fe0a72c3e5e" + integrity sha512-QbyRhDrBI9CdKBFpikPFjR2zmwOIQSndx+/g9YVuQ25QrxUnVhq/nAg/EXMQtrOQVtcb99Vx5AIfm1DWnKV31A== dependencies: Base64 "0.3.0" - core-js "^3.6.5" cross-fetch "^3.0.0" js-cookie "2.2.0" node-cache "^4.2.0" - text-encoding "^0.7.0" tiny-emitter "1.1.0" - webcrypto-shim "^0.1.5" xhr2 "0.1.3" "@rollup/plugin-commonjs@11.0.1": @@ -3879,11 +3875,6 @@ core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1: version "2.6.11" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" -core-js@^3.6.5: - version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" - integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -11104,11 +11095,6 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -text-encoding@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.7.0.tgz#f895e836e45990624086601798ea98e8f36ee643" - integrity sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA== - text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -11823,11 +11809,6 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -webcrypto-shim@^0.1.5: - version "0.1.6" - resolved "https://registry.yarnpkg.com/webcrypto-shim/-/webcrypto-shim-0.1.6.tgz#b4554d95c0a63637226c9732440dc674bf96f5cb" - integrity sha512-0o612s3S5z3IkDSRghIwd3Ul4X8NRmmZDpt6PWGI9gSM+nygVvrfzGjhIh4vwzlOJxYxS0fcFD1wh3yznuVzFg== - webdriver-js-extender@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz#57d7a93c00db4cc8d556e4d3db4b5db0a80c3bb7"