diff --git a/e2e/apps/express-oidc/strategy.mjs b/e2e/apps/express-oidc/strategy.mjs index 423561c..1f04cbd 100644 --- a/e2e/apps/express-oidc/strategy.mjs +++ b/e2e/apps/express-oidc/strategy.mjs @@ -1,4 +1,5 @@ import { Strategy} from 'passport'; +import { OAuth2Client } from '@okta/auth-foundation'; import { AuthorizationCodeFlow, SessionLogoutFlow, AuthTransaction } from '@okta/oauth2-flows'; @@ -20,8 +21,8 @@ export class OIDCStrategy extends Strategy { } async authenticate (req) { - const flow = new AuthorizationCodeFlow({ - ...authParams, + const client = new OAuth2Client(authParams); + const flow = new AuthorizationCodeFlow(client, { redirectUri: 'http://localhost:8080/login/callback' }); @@ -61,8 +62,8 @@ export class OIDCStrategy extends Strategy { } static async logout (idToken) { - const flow = new SessionLogoutFlow({ - ...authParams, + const client = new OAuth2Client(authParams); + const flow = new SessionLogoutFlow(client, { logoutRedirectUri: 'http://localhost:8080/' }); diff --git a/e2e/apps/redirect-model/src/apps/orchestrators/pages/ProxyHost.tsx b/e2e/apps/redirect-model/src/apps/orchestrators/pages/ProxyHost.tsx index 3c7f600..43ee860 100644 --- a/e2e/apps/redirect-model/src/apps/orchestrators/pages/ProxyHost.tsx +++ b/e2e/apps/redirect-model/src/apps/orchestrators/pages/ProxyHost.tsx @@ -1,6 +1,9 @@ -import { AuthorizationCodeFlow } from '@okta/oauth2-flows'; -import { AuthorizationCodeFlowOrchestrator, HostOrchestrator } from '@okta/spa-platform'; -import { FetchClient } from '@okta/spa-platform/fetch'; +import { + AuthorizationCodeFlow, + AuthorizationCodeFlowOrchestrator, + HostOrchestrator, + FetchClient +} from '@okta/spa-platform'; import { client } from '@/auth'; import { createMessageComponent } from '../createMessageComponent'; diff --git a/e2e/apps/redirect-model/src/apps/orchestrators/pages/Redirect.tsx b/e2e/apps/redirect-model/src/apps/orchestrators/pages/Redirect.tsx index cc49dac..b3f59af 100644 --- a/e2e/apps/redirect-model/src/apps/orchestrators/pages/Redirect.tsx +++ b/e2e/apps/redirect-model/src/apps/orchestrators/pages/Redirect.tsx @@ -1,8 +1,10 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; -import { AuthorizationCodeFlow } from '@okta/oauth2-flows'; -import { AuthorizationCodeFlowOrchestrator } from '@okta/spa-platform'; -import { FetchClient } from '@okta/spa-platform/fetch'; +import { + AuthorizationCodeFlow, + AuthorizationCodeFlowOrchestrator, + FetchClient +} from '@okta/spa-platform'; import { client } from '@/auth'; import { Loading } from '@/component/Loading'; import { createMessageComponent } from '../createMessageComponent'; diff --git a/e2e/apps/redirect-model/src/apps/orchestrators/pages/Silent.tsx b/e2e/apps/redirect-model/src/apps/orchestrators/pages/Silent.tsx index 8dfb442..09c9002 100644 --- a/e2e/apps/redirect-model/src/apps/orchestrators/pages/Silent.tsx +++ b/e2e/apps/redirect-model/src/apps/orchestrators/pages/Silent.tsx @@ -1,6 +1,8 @@ -import { AuthorizationCodeFlow } from '@okta/oauth2-flows'; -import { AuthorizationCodeFlowOrchestrator } from '@okta/spa-platform'; -import { FetchClient } from '@okta/spa-platform/fetch'; +import { + AuthorizationCodeFlow, + AuthorizationCodeFlowOrchestrator, + FetchClient +} from '@okta/spa-platform'; import { client } from '@/auth'; import { createMessageComponent } from '../createMessageComponent'; diff --git a/e2e/apps/redirect-model/src/auth.tsx b/e2e/apps/redirect-model/src/auth.tsx index d93fd53..d644afd 100644 --- a/e2e/apps/redirect-model/src/auth.tsx +++ b/e2e/apps/redirect-model/src/auth.tsx @@ -1,6 +1,10 @@ -import { Credential, OAuth2Client, clearDPoPKeyPairs } from '@okta/spa-platform'; -import { AuthorizationCodeFlow, SessionLogoutFlow } from '@okta/spa-platform/flows'; -import { AuthorizationCodeFlowOrchestrator } from '@okta/spa-platform/orchestrator'; +import { + Credential, + OAuth2Client, + clearDPoPKeyPairs, + AuthorizationCodeFlow, + SessionLogoutFlow +} from '@okta/spa-platform'; const USE_DPOP = __USE_DPOP__ === "true"; diff --git a/e2e/apps/redirect-model/src/component/LogoutCallback.tsx b/e2e/apps/redirect-model/src/component/LogoutCallback.tsx index 0727180..b9112ca 100644 --- a/e2e/apps/redirect-model/src/component/LogoutCallback.tsx +++ b/e2e/apps/redirect-model/src/component/LogoutCallback.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router'; import { useSearchParams } from 'react-router-dom'; -import { getSearchParam } from '@okta/auth-foundation'; +import { getSearchParam } from '@okta/spa-platform'; import { Loading } from './Loading'; diff --git a/e2e/apps/redirect-model/src/router.tsx b/e2e/apps/redirect-model/src/router.tsx index e1dbdba..7427485 100644 --- a/e2e/apps/redirect-model/src/router.tsx +++ b/e2e/apps/redirect-model/src/router.tsx @@ -1,7 +1,5 @@ import { createBrowserRouter } from 'react-router-dom'; import { Credential } from '@okta/spa-platform'; -import { AuthorizationCodeFlow } from '@okta/spa-platform/flows'; -import { signInFlow } from '@/auth'; // import Page components import { App } from './App'; diff --git a/e2e/apps/token-broker/resource-server/middleware.ts b/e2e/apps/token-broker/resource-server/middleware.ts index 6dcf617..f7a96b8 100644 --- a/e2e/apps/token-broker/resource-server/middleware.ts +++ b/e2e/apps/token-broker/resource-server/middleware.ts @@ -1,5 +1,5 @@ import { MockLayer } from 'vite-plugin-mock-server'; -import { JWT, shortID } from '@okta/auth-foundation'; +import { JWT, shortID } from '@okta/auth-foundation/core'; const dpopNonceError = diff --git a/e2e/apps/token-broker/src/auth.tsx b/e2e/apps/token-broker/src/auth.tsx index 2c0a421..7f7e35d 100644 --- a/e2e/apps/token-broker/src/auth.tsx +++ b/e2e/apps/token-broker/src/auth.tsx @@ -1,6 +1,13 @@ -import { Credential, OAuth2Client, clearDPoPKeyPairs } from '@okta/spa-platform'; -import { AuthorizationCodeFlow, SessionLogoutFlow } from '@okta/spa-platform/flows'; -import { AcrValues, JsonRecord, isOAuth2ErrorResponse } from '@okta/auth-foundation'; +import { + Credential, + OAuth2Client, + clearDPoPKeyPairs, + AuthorizationCodeFlow, + SessionLogoutFlow, + type AcrValues, + type JsonRecord, + isOAuth2ErrorResponse, +} from '@okta/spa-platform'; const ADMIN_SPA_REFRESH_TOKEN_TAG = 'admin-spa:mordor-token'; @@ -24,7 +31,6 @@ oauthConfig.baseURL = oauthConfig.issuer; export const client = new OAuth2Client(oauthConfig); - // ############# OAuth Flow Instances ############# // export const signInFlow = new AuthorizationCodeFlow(client, { redirectUri: `${window.location.origin}/login/callback`, diff --git a/e2e/apps/token-broker/src/broker.tsx b/e2e/apps/token-broker/src/broker.tsx index 13c36fc..8f85aca 100644 --- a/e2e/apps/token-broker/src/broker.tsx +++ b/e2e/apps/token-broker/src/broker.tsx @@ -1,10 +1,12 @@ import { + Credential, + Token, + HostOrchestrator, OAuth2ErrorResponse, isOAuth2ErrorResponse, hasSameValues, AcrValues -} from '@okta/auth-foundation'; -import { Credential, Token, HostOrchestrator } from '@okta/spa-platform'; +} from '@okta/spa-platform'; import { signIn, signOutFlow, getMordorToken, handleAcrStepUp } from './auth'; diff --git a/e2e/apps/token-broker/src/component/LogoutCallback.tsx b/e2e/apps/token-broker/src/component/LogoutCallback.tsx index 0727180..b9112ca 100644 --- a/e2e/apps/token-broker/src/component/LogoutCallback.tsx +++ b/e2e/apps/token-broker/src/component/LogoutCallback.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router'; import { useSearchParams } from 'react-router-dom'; -import { getSearchParam } from '@okta/auth-foundation'; +import { getSearchParam } from '@okta/spa-platform'; import { Loading } from './Loading'; diff --git a/e2e/apps/token-broker/src/resourceClient.ts b/e2e/apps/token-broker/src/resourceClient.ts index 1ff00c3..f233da6 100644 --- a/e2e/apps/token-broker/src/resourceClient.ts +++ b/e2e/apps/token-broker/src/resourceClient.ts @@ -1,5 +1,4 @@ -import { FetchClient } from '@okta/spa-platform/fetch'; -import { HostOrchestrator } from '@okta/spa-platform/orchestrator'; +import { FetchClient, HostOrchestrator, type APIRequest } from '@okta/spa-platform'; import { customScopes } from '@/auth'; @@ -11,11 +10,11 @@ orchestrator.defaultTimeout = 15000; export const fetchClient = new FetchClient(orchestrator); // testing APIClient request interceptors -const interceptor1 = (req: Request) => { +const interceptor1 = (req: APIRequest) => { req.headers.append('foo', '1'); return req; }; -const interceptor2 = (req: Request) => { +const interceptor2 = (req: APIRequest) => { req.headers.append('bar', '1'); return req; }; diff --git a/packages/auth-foundation/jest.browser.config.js b/packages/auth-foundation/jest.browser.config.js index 4b739a2..d74b450 100644 --- a/packages/auth-foundation/jest.browser.config.js +++ b/packages/auth-foundation/jest.browser.config.js @@ -8,6 +8,9 @@ const config = { __PKG_NAME__: pkg.name, __PKG_VERSION__: pkg.version, }, + setupFilesAfterEnv: [ + '/test/jest.setupAfterEnv.ts' + ] }; export default config; diff --git a/packages/auth-foundation/jest.node.config.js b/packages/auth-foundation/jest.node.config.js index 8510470..85a54b5 100644 --- a/packages/auth-foundation/jest.node.config.js +++ b/packages/auth-foundation/jest.node.config.js @@ -7,7 +7,10 @@ const config = { globals: { __PKG_NAME__: pkg.name, __PKG_VERSION__: pkg.version, - } + }, + setupFilesAfterEnv: [ + '/test/jest.setupAfterEnv.ts' + ] }; export default config; diff --git a/packages/auth-foundation/package.json b/packages/auth-foundation/package.json index 3e01842..2503bb8 100644 --- a/packages/auth-foundation/package.json +++ b/packages/auth-foundation/package.json @@ -22,6 +22,10 @@ "types": "./dist/types/index.d.ts", "import": "./dist/esm/index.js" }, + "./core" : { + "types": "./dist/types/core.d.ts", + "import": "./dist/esm/core.js" + }, "./client": { "types": "./dist/types/client.d.ts", "import": "./dist/esm/client.js" diff --git a/packages/auth-foundation/rollup.config.mjs b/packages/auth-foundation/rollup.config.mjs index 76c9f5e..3195b02 100644 --- a/packages/auth-foundation/rollup.config.mjs +++ b/packages/auth-foundation/rollup.config.mjs @@ -7,6 +7,6 @@ const base = baseConfig(ts, pkg); export default { ...base, - input: [base.input, 'src/client.ts', 'src/internal.ts'], + input: [base.input, 'src/core.ts', 'src/internal.ts'], external: [...Object.keys(pkg.dependencies)], }; diff --git a/packages/auth-foundation/src/Token.ts b/packages/auth-foundation/src/Token.ts index 966f68c..2434fba 100644 --- a/packages/auth-foundation/src/Token.ts +++ b/packages/auth-foundation/src/Token.ts @@ -18,9 +18,8 @@ import { validateURL } from './internals/validators.ts'; import { shortID } from './crypto/index.ts'; import { JWT } from './jwt/index.ts'; import { OAuth2Request } from './http/index.ts'; -import { DefaultDPoPSigningAuthority, DPoPSigningAuthority } from './oauth2/dpop/index.ts'; import { Timestamp } from './utils/TimeCoordinator.ts'; -import TimeCoordinator from './utils/TimeCoordinator.ts'; +import { Platform } from './platform/Platform.ts'; /** * @module Token @@ -70,8 +69,6 @@ export type TokenPrimitiveInit = TokenResponse; * - Okta Documentation: {@link https://developer.okta.com/docs/reference/api/oidc/#response-properties-4 | OIDC } */ export class Token implements JSONSerializable, Expires, RequestAuthorizer { - public readonly dpopSigningAuthority: DPoPSigningAuthority = DefaultDPoPSigningAuthority; - /** @internal */ public static expiryTimeouts: {[key: string]: ReturnType} = {}; @@ -114,7 +111,7 @@ export class Token implements JSONSerializable, Expires, RequestAuthorizer { constructor (obj: TokenInit) { const id = obj?.id ?? shortID(); this.id = id; - this.issuedAt = obj?.issuedAt ? new Date(obj?.issuedAt) : TimeCoordinator.now().asDate; + this.issuedAt = obj?.issuedAt ? new Date(obj?.issuedAt) : Platform.TimeCoordinator.now().asDate; this.accessToken = obj.accessToken; if (obj.idToken) { @@ -200,7 +197,7 @@ export class Token implements JSONSerializable, Expires, RequestAuthorizer { */ get isExpired (): boolean { // TODO: revisit - const now = TimeCoordinator.now().asDate; + const now = Platform.TimeCoordinator.now().asDate; return +this.expiresAt - +now <= 0; } @@ -219,7 +216,7 @@ export class Token implements JSONSerializable, Expires, RequestAuthorizer { * @see {@link Token.willBeValidIn} */ willBeExpiredIn (duration: Seconds) { - const ts = Timestamp.from(TimeCoordinator.now().value + duration); + const ts = Timestamp.from(Platform.TimeCoordinator.now().value + duration); return ts.isAfter(this.expiresAt); } @@ -289,7 +286,7 @@ export class Token implements JSONSerializable, Expires, RequestAuthorizer { return Token.create({ id: this.id, - issuedAt: (this.issuedAt ?? token.issuedAt ?? TimeCoordinator.now().asDate).valueOf() / 1000, + issuedAt: (this.issuedAt ?? token.issuedAt ?? Platform.TimeCoordinator.now().asDate).valueOf() / 1000, tokenType: this.tokenType, expiresIn: this.expiresIn, accessToken: this.accessToken, @@ -320,7 +317,7 @@ export class Token implements JSONSerializable, Expires, RequestAuthorizer { if (this.tokenType === 'DPoP') { const keyPairId = this.context.dpopPairId; // .generateDPoPProof() will throw if dpopPairId is undefined - await this.dpopSigningAuthority.sign(request, { keyPairId, nonce: dpopNonce, accessToken: this.accessToken }); + await Platform.DPoPSigningAuthority.sign(request, { keyPairId, nonce: dpopNonce, accessToken: this.accessToken }); } request.headers.set('Authorization', `${this.tokenType} ${this.accessToken}`); diff --git a/packages/auth-foundation/src/client.ts b/packages/auth-foundation/src/client.ts deleted file mode 100644 index 876e208..0000000 --- a/packages/auth-foundation/src/client.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @packageDocumentation - * @internal - */ - -import { OAuth2Client } from './oauth2/client.ts'; -export default OAuth2Client; diff --git a/packages/auth-foundation/src/core.ts b/packages/auth-foundation/src/core.ts new file mode 100644 index 0000000..dd43bd1 --- /dev/null +++ b/packages/auth-foundation/src/core.ts @@ -0,0 +1,35 @@ +/** + * @module Core + */ + +// types +export * from './types/index.ts'; + +// common +export * from './http/index.ts'; +export * from './errors/index.ts'; +export * from './utils/index.ts'; +export * from './utils/EventEmitter.ts'; +export * from './utils/TimeCoordinator.ts'; +export * from './utils/TaskBridge.ts'; + +// crypto / jwt +export { randomBytes, shortID } from './crypto/index.ts'; +export * from './jwt/index.ts'; + +// oauth2 +export * from './oauth2/pkce.ts'; +export * from './oauth2/dpop/index.ts'; +export * from './oauth2/client.ts'; + +// Credential & Token +export * from './Token.ts'; +export * from './Credential/index.ts'; +export * from './TokenOrchestrator.ts'; + +// FetchClient +export * from './FetchClient.ts'; + +export { addEnv } from './http/oktaUserAgent.ts'; + +export { Platform } from './platform/Platform.ts'; diff --git a/packages/auth-foundation/src/index.ts b/packages/auth-foundation/src/index.ts index 552fd67..e7bfc46 100644 --- a/packages/auth-foundation/src/index.ts +++ b/packages/auth-foundation/src/index.ts @@ -2,31 +2,17 @@ * @module Core */ -// types -export * from './types/index.ts'; +export * from './core.ts'; -// common -export * from './http/index.ts'; -export * from './errors/index.ts'; -export * from './utils/index.ts'; -export * from './utils/EventEmitter.ts'; -export * from './utils/TimeCoordinator.ts'; -export * from './utils/TaskBridge.ts'; +import { Platform } from './platform/Platform.ts'; -// crypto / jwt -export { randomBytes, shortID } from './crypto/index.ts'; -export * from './jwt/index.ts'; +// eslint-disable-next-line no-restricted-syntax +import { __internalTimeCoordinator } from './utils/TimeCoordinator.ts'; +// eslint-disable-next-line no-restricted-syntax +import { __internalDPoPSigningAuthority } from './oauth2/dpop/index.ts'; -// oauth2 -export * from './oauth2/pkce.ts'; -export * from './oauth2/dpop/index.ts'; - -// Credential & Token -export * from './Token.ts'; -export * from './Credential/index.ts'; -export * from './TokenOrchestrator.ts'; - -// FetchClient -export * from './FetchClient.ts'; - -export { addEnv } from './http/oktaUserAgent.ts'; +// NOTE: any singleton added to the Platform will need to be added to `test/jest.setupAfterEnv.ts` as well +Platform.registerDefaultsLoader(() => ({ + TimeCoordinator: __internalTimeCoordinator, + DPoPSigningAuthority: __internalDPoPSigningAuthority +})); diff --git a/packages/auth-foundation/src/internal.ts b/packages/auth-foundation/src/internal.ts index ed6a46b..dfca439 100644 --- a/packages/auth-foundation/src/internal.ts +++ b/packages/auth-foundation/src/internal.ts @@ -6,5 +6,5 @@ export * from './internals/index.ts'; export { addEnv } from './http/oktaUserAgent.ts'; - export { buf, b64u } from './crypto/index.ts'; +export { __internalTimeCoordinator as TimeCoordinator } from './utils/TimeCoordinator.ts'; diff --git a/packages/auth-foundation/src/jwt/IDTokenValidator.ts b/packages/auth-foundation/src/jwt/IDTokenValidator.ts index ec56bd9..7511727 100644 --- a/packages/auth-foundation/src/jwt/IDTokenValidator.ts +++ b/packages/auth-foundation/src/jwt/IDTokenValidator.ts @@ -8,7 +8,9 @@ import type { AcrValues } from '../types/index.ts'; import type { JWT } from './JWT.ts'; import { JWTError } from '../errors/index.ts'; -import TimeCoordinator, { Timestamp } from '../utils/TimeCoordinator.ts'; +import { Timestamp } from '../utils/TimeCoordinator.ts'; +import { Platform } from '../platform/Platform.ts'; + /** * @group JWT @@ -98,7 +100,7 @@ export const DefaultIDTokenValidator: IDTokenValidator = { break; case 'expirationTime': - const now = TimeCoordinator.now(); + const now = Platform.TimeCoordinator.now(); if (jwt.expirationTime && now.isBefore(jwt.expirationTime)) { break; } @@ -107,7 +109,7 @@ export const DefaultIDTokenValidator: IDTokenValidator = { case 'issuedAtTime': if (jwt.issuedAt) { const issuedAt: Date = jwt.issuedAt; - const now = TimeCoordinator.now(); + const now = Platform.TimeCoordinator.now(); if (Math.abs(now.timeSince(issuedAt)) <= DefaultIDTokenValidator.issuedAtGraceInterval) { break; } @@ -131,7 +133,7 @@ export const DefaultIDTokenValidator: IDTokenValidator = { // compare `auth_time` to a timestamp to determine how long ago authentication was completed // the timestamp can either be the issuedAt (iat) claim or a coordinated .now() - const issuedAt = Timestamp.from(jwt?.issuedAt ?? TimeCoordinator.now()); + const issuedAt = Timestamp.from(jwt?.issuedAt ?? Platform.TimeCoordinator.now()); const elapsedTime = issuedAt.timeSince(authTime); if (elapsedTime > context.maxAge) { throw new JWTError('exceeds maxAge'); diff --git a/packages/auth-foundation/src/jwt/JWT.ts b/packages/auth-foundation/src/jwt/JWT.ts index a81f7cb..e7f94d5 100644 --- a/packages/auth-foundation/src/jwt/JWT.ts +++ b/packages/auth-foundation/src/jwt/JWT.ts @@ -7,10 +7,11 @@ import type { JsonRecord, RawRepresentable, Expires, TimeInterval } from '../types/index.ts'; import { JWTError } from '../errors/index.ts'; import { validateString } from '../internals/validators.ts'; -import TimeCoordinator from '../utils/TimeCoordinator.ts'; import { JWK, JWKS } from './JWK.ts'; import { buf, b64u } from '../crypto/index.ts'; import { IDTokenValidator } from './IDTokenValidator.ts'; +import { Platform } from '../platform/Platform.ts'; + /** * @group JWT @@ -70,7 +71,7 @@ function validateBody (claims: JsonRecord) { throw new JWTError('Unexpected `nbf` claim type'); } - if (!TimeCoordinator.now().isAfter(claims.nbf)) { + if (!Platform.TimeCoordinator.now().isAfter(claims.nbf)) { throw new JWTError('`nbf` claim is unexpectedly in the past'); } } @@ -242,7 +243,7 @@ export class JWT implements RawRepresentable, Expires { if (!this.expirationTime) { return false; } - const now = TimeCoordinator.now(); + const now = Platform.TimeCoordinator.now(); return now.isBefore(this.expirationTime); } get isValid(): boolean { diff --git a/packages/auth-foundation/src/oauth2/client.ts b/packages/auth-foundation/src/oauth2/client.ts index f73f6bc..c72a0f5 100644 --- a/packages/auth-foundation/src/oauth2/client.ts +++ b/packages/auth-foundation/src/oauth2/client.ts @@ -21,14 +21,14 @@ import { TokenHashValidator } from '../jwt/index.ts'; import { APIClient, APIRequest } from '../http/index.ts'; -import { DefaultDPoPSigningAuthority, type DPoPSigningAuthority } from './dpop/index.ts'; import { Configuration as ConfigurationConstructor, type ConfigurationParams } from './configuration.ts'; import { TokenInit, Token } from '../Token.ts'; import { UserInfo } from './requests/UserInfo.ts'; import { PromiseQueue } from '../utils/PromiseQueue.ts'; import { EventEmitter } from '../utils/EventEmitter.ts'; import { hasSameValues } from '../utils/index.ts'; -import TimeCoordinator, { Timestamp } from '../utils/TimeCoordinator.ts'; +import { Timestamp } from '../utils/TimeCoordinator.ts'; +import { Platform } from '../platform/Platform.ts'; // ref: https://developer.okta.com/docs/reference/api/oidc/ @@ -38,10 +38,6 @@ import TimeCoordinator, { Timestamp } from '../utils/TimeCoordinator.ts'; * @noInheritDoc */ export class OAuth2Client extends APIClient { - /** - * @group Customizations - */ - public readonly dpopSigningAuthority: DPoPSigningAuthority = DefaultDPoPSigningAuthority; /** * @group Customizations */ @@ -67,11 +63,6 @@ export class OAuth2Client e this.configuration = configuration; } - /** @internal */ - protected createToken (init: TokenInit): Token { - return new Token(init); - } - /** @internal */ protected getDPoPNonceCacheKey (request: APIRequest): string { return `${this.configuration.clientId}.${super.getDPoPNonceCacheKey(request)}`; @@ -101,7 +92,8 @@ export class OAuth2Client e } /** @internal */ - protected async prepareDPoPNonceRetry (request: APIRequest): Promise { + protected async prepareDPoPNonceRetry (request: APIRequest, nonce: string): Promise { + request.context.dpopNonce = nonce; return this.signTokenRequestWithDPoP(request); } @@ -109,7 +101,7 @@ export class OAuth2Client e const { dpopPairId } = request.context; // dpop nonce may not be available for this request (undefined), this is expected const dpopNonce = nonce ?? await this.getDPoPNonceFromCache(request); - await this.dpopSigningAuthority.sign(request, { keyPairId: dpopPairId, nonce: dpopNonce }); + await Platform.DPoPSigningAuthority.sign(request, { keyPairId: dpopPairId, nonce: dpopNonce }); } protected async processResponse(response: Response, request: APIRequest): Promise { @@ -123,7 +115,7 @@ export class OAuth2Client e if (parsedDate.toString() !== 'Invalid Date') { const serverTime = Timestamp.from(parsedDate); const skew = Math.round(serverTime.timeSince(Date.now() / 1000)); - TimeCoordinator.clockSkew = skew; + Platform.TimeCoordinator.clockSkew = skew; } } } @@ -188,7 +180,7 @@ export class OAuth2Client e tokenRequest: Token.TokenRequest, requestContext: OAuth2Client.TokenRequestContext = {} ): Promise { - const { keyPairId: dpopPairId } = requestContext; + const { dpopPairId } = requestContext; const request = tokenRequest.prepare({ dpopPairId }); const { acrValues, maxAge } = tokenRequest; @@ -208,7 +200,7 @@ export class OAuth2Client e // request hasn't been retried too many times previously request.canRetry() && // (heuristic) the TimeCoordinator updated with a meaningful time difference (~2.5 mintues) - Math.abs(Date.now() - TimeCoordinator.clockSkew) >= 150 + Math.abs(Date.now() - Platform.TimeCoordinator.clockSkew) >= 150 ) { // If a JWT (DPoP Proof) clock skew error is returned we can retry the request. // The `Date` header of the /token response will be have been processed, hopefully @@ -255,7 +247,7 @@ export class OAuth2Client e result.id = tokenRequest.id; } - const token = this.createToken(result); + const token = new Token(result); return token; } @@ -320,8 +312,8 @@ export class OAuth2Client e public async exchange (request: Token.TokenRequest): Promise { const context: OAuth2Client.TokenRequestContext = {}; if (this.configuration.dpop) { - const keyPairId = await this.dpopSigningAuthority.createDPoPKeyPair(); - context.keyPairId = keyPairId; + const dpopPairId = await Platform.DPoPSigningAuthority.createDPoPKeyPair(); + context.dpopPairId = dpopPairId; } const [keySet, response] = await Promise.all([ @@ -405,7 +397,7 @@ export class OAuth2Client e const context: OAuth2Client.TokenRequestContext = {}; if (token.context?.dpopPairId) { - context.keyPairId = token.context.dpopPairId; + context.dpopPairId = token.context.dpopPairId; } const [keySet, response] = await Promise.all([ @@ -424,14 +416,14 @@ export class OAuth2Client e // 1. providing a sub-set of scopes // 2. providing no scopes (empty array) if (!hasSameValues(response.scopes, token.scopes) || scopes?.length === 0) { - refreshedToken = this.createToken({ + refreshedToken = new Token({ ...(token.toJSON() as TokenInit), id: token.id, refreshToken: response.refreshToken }); const tokenInit = { ...response.toJSON() } as TokenInit; - newToken = this.createToken({ + newToken = new Token({ ...tokenInit, // downscoped token should "inherit" context from "parent" token context: { ...refreshedToken.context, ...tokenInit.context }, @@ -582,7 +574,7 @@ export namespace OAuth2Client { /** @internal */ export type TokenRequestContext = { - keyPairId?: string; + dpopPairId?: string; }; /** @internal */ diff --git a/packages/auth-foundation/src/oauth2/dpop/index.ts b/packages/auth-foundation/src/oauth2/dpop/index.ts index e1b745e..aecd297 100644 --- a/packages/auth-foundation/src/oauth2/dpop/index.ts +++ b/packages/auth-foundation/src/oauth2/dpop/index.ts @@ -9,7 +9,7 @@ import { JWT } from '../../jwt/index.ts'; import { DPoPStorage } from './storage.ts'; import { DPoPNonceCache } from './nonceCache.ts'; import { DPoPError } from '../../errors/DPoPError.ts'; -import TimeCoordinator from '../../utils/TimeCoordinator.ts'; +import { Platform } from '../../platform/Platform.ts'; export { DPoPNonceCache }; export type { DPoPHeaders, DPoPClaims, DPoPProofParams, DPoPStorage }; @@ -62,11 +62,9 @@ export class DPoPSigningAuthorityImpl implements DPoPSigningAuthority { * @returns `id` representing the generated key pair */ async createDPoPKeyPair (): Promise { - // export async function createDPoPKeyPair (): Promise<{keyPair: CryptoKeyPair, keyPairId: string}> { const keyPairId = shortID(); const keyPair = await this.generateKeyPair(); await this.store.add(keyPairId, keyPair); - // return { keyPair, keyPairId }; return keyPairId; } @@ -118,7 +116,7 @@ export class DPoPSigningAuthorityImpl implements DPoPSigningAuthority { const claims: DPoPClaims = { htm: request.method, htu: `${url.origin}${url.pathname}`, - iat: TimeCoordinator.now().value, + iat: Platform.TimeCoordinator.now().value, jti: randomBytes(), nonce }; @@ -145,5 +143,5 @@ export class DPoPSigningAuthorityImpl implements DPoPSigningAuthority { } -/** @internal */ -export const DefaultDPoPSigningAuthority: DPoPSigningAuthority = new DPoPSigningAuthorityImpl(new DPoPStorage.MemoryStore()); +/** @internal - Use `Platform.DPoPSigninAuthority instead */ +export const __internalDPoPSigningAuthority: DPoPSigningAuthority = new DPoPSigningAuthorityImpl(new DPoPStorage.MemoryStore()); diff --git a/packages/auth-foundation/src/platform/Platform.ts b/packages/auth-foundation/src/platform/Platform.ts new file mode 100644 index 0000000..f893a77 --- /dev/null +++ b/packages/auth-foundation/src/platform/Platform.ts @@ -0,0 +1,145 @@ +import { AuthSdkError } from '../errors/AuthSdkError.ts'; +import type { DPoPSigningAuthority } from '../oauth2/dpop/index.ts'; +import type { TimeCoordinator } from '../utils/TimeCoordinator.ts'; + + +/** + * The required Platform dependencies + */ +export interface PlatformDependencies { + TimeCoordinator: TimeCoordinator; + DPoPSigningAuthority: DPoPSigningAuthority; +} + +export class PlatformRegistryError extends AuthSdkError {} + +/** + * A singleton registry of globally-available singleton dependencies which can + * provide platform-specific default implementations and enable overriding as needed + * + * For example, the {@link TimeCoordinator} should be globally available to be a + * centralized entity to perform all time calculations. Registering the {@link TimeCoordinator} + * as a {@link Platform} dependency enables consumers to access the {@link TimeCoordinator} via + * + * @example + * ``` + * import { Platform } from '@okta/auth-foundation'; + * const currentTime = Platform.TimeCoordinator.now(); + * ``` + * + * To enable tree-shaking and prevent including default implementations (bundle bloat) which + * will be instanceously overwritten, default implemenations can be selectively included. + * + * @remarks + * Use `import * from '@okta/auth-foundation'` for standard usage, including all default platform + * dependency implementations. + * + * Use `import * from '@okta/auth-foundation/core'` for deeper customizations of platform dependencies, + * this does not include any default implementations. {@link PlatformRegistryError} will be thrown if + * a dependency is used before an implementation is provided + */ +export class PlatformRegistry implements PlatformDependencies { + #deps: PlatformDependencies | null = null; + #defaultsLoader: (() => PlatformDependencies) | null = null; + + /** + * Override default platform dependencies globally + * + * This pattern will include the default implementations within the resulting bundle, + * causing essentially dead code to be bundled. This will likely be acceptable for + * most standard use cases. For scenarios where deeper customizations are required + * see {@link PlatformRegistry.registerDefaultsLoader} + * + * @remarks + * Call this once at application startup before using any SDK components. + * Partial updates are supported - only override what you need. + */ + public configure (dependencies: Partial): void { + this.#deps = { + ...this.getDefaults(), + ...dependencies + }; + } + + /** + * Registers a loader to provide the platform dependency default implementations + * + * When a deeper customization of platform dependencies is required, this method can + * be used to provide custom implementations of platform dependencies without including + * the provided default implementations in any resulting bundle. + * + * This pattern is not recommended for standard SDK usage and should only be used if deep + * customization is required (like providing support to an otherwise unsupported runtime environment) + * + * For standard usage, see {@link PlatformRegistry.configure} + * + * @remarks + * Call this once at application startup before using any SDK components. + * + * @example + * ``` + * // src/auth.ts + * import { Platform } from '@okta/auth-foundation/core'; // ensure "/core" is imported specifically + * + * Platform.registerDefaultsLoader(() => ({ + * TimeCoordinator: MyCustomTimeCoordinator + * })); + * + * // ensure this module is loaded before any other '@okta/*' dependencies + * ``` + */ + public registerDefaultsLoader(loader: () => PlatformDependencies): void { + this.#defaultsLoader = loader; + } + + /** + * @internal + * Resets loaded dependencies. For testing purposes mostly. + */ + public reset (): void { + this.#deps = null; + } + + /** + * @internal + * Get all current dependencies (configured or defaults) + */ + protected get resolved (): PlatformDependencies { + return this.#deps ?? this.getDefaults(); + } + + /** + * @internal + * Override in subclasses to provide platform-specific defaults + */ + protected getDefaults(): PlatformDependencies { + if (!this.#defaultsLoader) { + throw new PlatformRegistryError( + `No platform defaults available. Import from "@okta/auth-foundation" directly or call Platform.registerDefaultsLoader()` + ); + } + return this.#defaultsLoader(); + } + + /** + * Get the current TimeCoordinator instance + * + * @remarks + * Returns configured override or factory default + */ + public get TimeCoordinator(): TimeCoordinator { + return this.resolved.TimeCoordinator; + } + + /** + * Get the current DPoPSigningAuthority instance + * + * @remarks + * Returns configured override or factory default + */ + public get DPoPSigningAuthority(): DPoPSigningAuthority { + return this.resolved.DPoPSigningAuthority; + } +} + +export const Platform = new PlatformRegistry(); diff --git a/packages/auth-foundation/src/utils/TimeCoordinator.ts b/packages/auth-foundation/src/utils/TimeCoordinator.ts index 431e7b0..4337320 100644 --- a/packages/auth-foundation/src/utils/TimeCoordinator.ts +++ b/packages/auth-foundation/src/utils/TimeCoordinator.ts @@ -7,8 +7,8 @@ // CORS requests are limited to the specific headers exposed. This by default will block the `date` header import type { TimeInterval, EpochTimestamp, Seconds } from '../types/index.ts'; +import { Platform } from '../platform/Platform.ts'; -// TODO: DOC THIS /** * Utility class for parse timestamps and performing time/date calculations @@ -51,7 +51,7 @@ export class Timestamp { isBefore (t: EpochTimestamp | Date | Timestamp): boolean { t = Timestamp.from(t).value; // eslint-disable-next-line @typescript-eslint/no-use-before-define - return this.ts < t - TimeCoordinator.clockTolerance; + return this.ts < t - Platform.TimeCoordinator.clockTolerance; } isAfter (t: Timestamp): boolean; @@ -60,7 +60,7 @@ export class Timestamp { isAfter (t: EpochTimestamp | Date | Timestamp): boolean { t = Timestamp.from(t).value; // eslint-disable-next-line @typescript-eslint/no-use-before-define - return this.ts > t + TimeCoordinator.clockTolerance; + return this.ts > t + Platform.TimeCoordinator.clockTolerance; } timeSince (t: Timestamp): Seconds; @@ -73,7 +73,7 @@ export class Timestamp { timeSinceNow () { // eslint-disable-next-line @typescript-eslint/no-use-before-define - const now = timeCoordinator.now(); + const now = Platform.TimeCoordinator.now(); return this.ts - now.value; } } @@ -81,9 +81,18 @@ export class Timestamp { /** * @group TimeCoordinator */ -class TimeCoordinator { +export interface TimeCoordinator { + clockSkew: Seconds; + clockTolerance: Seconds; + now: () => Timestamp; +} + +/** + * @group TimeCoordinator + */ +export class DefaultTimeCoordinator implements TimeCoordinator { #skew = 0; - static #tolerance = 0; + #tolerance = 0; get clockSkew (): Seconds { return this.#skew; @@ -93,12 +102,12 @@ class TimeCoordinator { this.#skew = skew; } - static get clockTolerance (): Seconds { - return TimeCoordinator.#tolerance; + get clockTolerance (): Seconds { + return this.#tolerance; } - static set clockTolerance (tolerance: Seconds) { - TimeCoordinator.#tolerance = tolerance; + set clockTolerance (tolerance: Seconds) { + this.#tolerance = tolerance; } now (): Timestamp { @@ -107,7 +116,6 @@ class TimeCoordinator { } } -const timeCoordinator = new TimeCoordinator(); +/** @internal - Use `Platform.TimeCoordinator` instead */ +export const __internalTimeCoordinator = new DefaultTimeCoordinator(); -/** @internal */ -export default timeCoordinator; diff --git a/packages/auth-foundation/test/jest.setupAfterEnv.ts b/packages/auth-foundation/test/jest.setupAfterEnv.ts new file mode 100644 index 0000000..0ed6966 --- /dev/null +++ b/packages/auth-foundation/test/jest.setupAfterEnv.ts @@ -0,0 +1,10 @@ +import { Platform } from 'src/platform/Platform'; +// eslint-disable-next-line no-restricted-syntax +import { __internalTimeCoordinator } from 'src/utils/TimeCoordinator'; +// eslint-disable-next-line no-restricted-syntax +import { __internalDPoPSigningAuthority } from 'src/oauth2/dpop'; + +Platform.registerDefaultsLoader(() => ({ + TimeCoordinator: __internalTimeCoordinator, + DPoPSigningAuthority: __internalDPoPSigningAuthority +})); diff --git a/packages/auth-foundation/test/spec/FetchClient.spec.ts b/packages/auth-foundation/test/spec/FetchClient.spec.ts index 3b28e5d..271d8ae 100644 --- a/packages/auth-foundation/test/spec/FetchClient.spec.ts +++ b/packages/auth-foundation/test/spec/FetchClient.spec.ts @@ -1,6 +1,7 @@ import { FetchClient } from 'src/FetchClient'; import { TokenOrchestrator } from 'src/TokenOrchestrator'; import { APIClientError } from 'src/errors'; +import { Platform } from 'src/platform/Platform'; import { randStr } from '@repo/jest-helpers/browser/helpers'; import { makeTestToken } from '../helpers/makeTestResource'; @@ -52,7 +53,7 @@ describe('FetchClient', () => { // test with a (mocked) dpop-bound token const dpopToken = makeTestToken(null, { tokenType: 'DPoP' }); - dpopToken.dpopSigningAuthority.sign = jest.fn().mockImplementation(async (request) => { + jest.spyOn(Platform.DPoPSigningAuthority, 'sign').mockImplementation(async (request) => { request.headers.set('dpop', 'fakedpopvalue'); return request; }); diff --git a/packages/auth-foundation/test/spec/Platform.spec.ts b/packages/auth-foundation/test/spec/Platform.spec.ts new file mode 100644 index 0000000..a8ec5fc --- /dev/null +++ b/packages/auth-foundation/test/spec/Platform.spec.ts @@ -0,0 +1,97 @@ +import { PlatformRegistry } from 'src/platform/Platform'; +import { __internalTimeCoordinator, TimeCoordinator, Timestamp } from 'src/utils/TimeCoordinator'; +import { __internalDPoPSigningAuthority } from 'src/oauth2/dpop'; + + +describe('PlatformRegistry', () => { + let PlatformModule; + let Platform: PlatformRegistry; + + beforeEach(async () => { + PlatformModule = (await import('src/platform/Platform')); + Platform = PlatformModule.Platform; + }); + + afterEach(() => { + jest.resetModules(); + }); + + describe('PlatformRegistry', () => { + it('exports an instance of PlatformRegistry', () => { + expect(Platform).toBeInstanceOf(PlatformRegistry); + }); + + it('should throw if no default loader has been registered', () => { + expect(() => Platform.TimeCoordinator).toThrow(PlatformModule.PlatformRegistryError); + }); + }); + + describe('exports / entry files', () => { + describe('default export (index.ts)', () => { + it('exposes public API and defines default platform dependencies', async () => { + const module = (await import('src/index')); + expect(module).toMatchObject({ + Credential: expect.any(Function), + OAuth2Client: expect.any(Function), + TokenOrchestrator: expect.any(Function), + FetchClient: expect.any(Function), + }); + expect(() => module.Platform.TimeCoordinator).not.toThrow(); + expect(() => module.Platform.DPoPSigningAuthority).not.toThrow(); + }); + }); + + describe('core export (core.ts)', () => { + it('exposes public API and does NOT provide default platform dependencies', async () => { + const module = (await import('src/core')); + expect(module).toMatchObject({ + Credential: expect.any(Function), + OAuth2Client: expect.any(Function), + TokenOrchestrator: expect.any(Function), + FetchClient: expect.any(Function), + }); + expect(() => module.Platform.TimeCoordinator).toThrow(PlatformModule.PlatformRegistryError); + expect(() => module.Platform.DPoPSigningAuthority).toThrow(PlatformModule.PlatformRegistryError); + + module.Platform.registerDefaultsLoader(() => ({ + TimeCoordinator: __internalTimeCoordinator, + DPoPSigningAuthority: __internalDPoPSigningAuthority + })); + + expect(() => module.Platform.TimeCoordinator).not.toThrow(); + expect(() => module.Platform.DPoPSigningAuthority).not.toThrow(); + }); + }); + }); + + describe('Override capabilities', () => { + class CustomTimeCoordinator implements TimeCoordinator { + clockSkew: number = 100; + clockTolerance: number = 100; + now () { return Timestamp.from(1000); } + } + + it('enables the default dependency implementation to be overwritten', () => { + expect(() => Platform.TimeCoordinator).toThrow(PlatformModule.PlatformRegistryError); + expect(() => Platform.DPoPSigningAuthority).toThrow(PlatformModule.PlatformRegistryError); + + Platform.registerDefaultsLoader(() => ({ + TimeCoordinator: __internalTimeCoordinator, + DPoPSigningAuthority: __internalDPoPSigningAuthority + })); + + expect(Platform.TimeCoordinator).toEqual(__internalTimeCoordinator); + expect(Platform.DPoPSigningAuthority).toEqual(__internalDPoPSigningAuthority); + + const CustomizedTimeCoordinator = new CustomTimeCoordinator(); + Platform.configure({ + TimeCoordinator: CustomizedTimeCoordinator + }); + + expect(Platform.TimeCoordinator).toEqual(CustomizedTimeCoordinator); + expect(Platform.DPoPSigningAuthority).toEqual(__internalDPoPSigningAuthority); + + expect(__internalTimeCoordinator.now()).not.toEqual(CustomizedTimeCoordinator.now()); + }); + }); +}); \ No newline at end of file diff --git a/packages/auth-foundation/test/spec/Token.spec.ts b/packages/auth-foundation/test/spec/Token.spec.ts index 8bff2a9..c7b3b50 100644 --- a/packages/auth-foundation/test/spec/Token.spec.ts +++ b/packages/auth-foundation/test/spec/Token.spec.ts @@ -8,8 +8,10 @@ jest.mock('src/http/oktaUserAgent', () => { import { JWT } from 'src/jwt'; import { Token } from 'src/Token'; import { OAuth2Client } from 'src/oauth2/client'; -import { mockTokenResponse } from '@repo/jest-helpers/browser/helpers'; import { OAuth2Error } from 'src/errors'; +import { Platform } from 'src/platform/Platform'; + +import { mockTokenResponse } from '@repo/jest-helpers/browser/helpers'; interface TestContext { @@ -255,7 +257,7 @@ describe('Token', () => { tokenType: 'DPoP', context: { dpopPairId: 'dpopPairId' } })); - jest.spyOn(token.dpopSigningAuthority, 'sign').mockImplementation((request) => { + jest.spyOn(Platform.DPoPSigningAuthority, 'sign').mockImplementation((request) => { request.headers.set('dpop', 'dpopproof'); return Promise.resolve(request); }); @@ -268,8 +270,8 @@ describe('Token', () => { authorization: `DPoP ${token.accessToken}`, dpop: 'dpopproof' }); - expect(token.dpopSigningAuthority.sign).toHaveBeenCalledTimes(1); - expect(token.dpopSigningAuthority.sign).toHaveBeenLastCalledWith( + expect(Platform.DPoPSigningAuthority.sign).toHaveBeenCalledTimes(1); + expect(Platform.DPoPSigningAuthority.sign).toHaveBeenLastCalledWith( expect.any(Request), expect.objectContaining({ keyPairId: 'dpopPairId', accessToken: token.accessToken }) ); @@ -284,8 +286,8 @@ describe('Token', () => { authorization: `DPoP ${token.accessToken}`, dpop: 'dpopproof' }); - expect(token.dpopSigningAuthority.sign).toHaveBeenCalledTimes(2); - expect(token.dpopSigningAuthority.sign).toHaveBeenLastCalledWith( + expect(Platform.DPoPSigningAuthority.sign).toHaveBeenCalledTimes(2); + expect(Platform.DPoPSigningAuthority.sign).toHaveBeenLastCalledWith( expect.any(Request), expect.objectContaining({ keyPairId: 'dpopPairId', accessToken: token.accessToken }) ); @@ -298,8 +300,8 @@ describe('Token', () => { authorization: `DPoP ${token.accessToken}`, dpop: 'dpopproof' }); - expect(token.dpopSigningAuthority.sign).toHaveBeenCalledTimes(3); - expect(token.dpopSigningAuthority.sign).toHaveBeenLastCalledWith( + expect(Platform.DPoPSigningAuthority.sign).toHaveBeenCalledTimes(3); + expect(Platform.DPoPSigningAuthority.sign).toHaveBeenLastCalledWith( expect.any(Request), expect.objectContaining({ keyPairId: 'dpopPairId', diff --git a/packages/auth-foundation/test/spec/TokenOrchestrator.spec.ts b/packages/auth-foundation/test/spec/TokenOrchestrator.spec.ts index 2b1bdf7..87b446c 100644 --- a/packages/auth-foundation/test/spec/TokenOrchestrator.spec.ts +++ b/packages/auth-foundation/test/spec/TokenOrchestrator.spec.ts @@ -1,15 +1,11 @@ import { Token } from 'src/Token'; import { TokenOrchestrator } from 'src/TokenOrchestrator'; import { TokenOrchestratorError } from 'src/errors'; +import { Platform } from 'src/platform/Platform'; import { makeTestToken } from '../helpers/makeTestResource'; -// Mock DPoP token (and signingAuthority) -const testToken = makeTestToken(null, { tokenType: 'DPoP' }); -testToken.dpopSigningAuthority.sign = jest.fn().mockImplementation(async (request) => { - request.headers.set('dpop', 'fakedpopvalue'); - return request; -}); +let testToken: Token; // Extend and mock abstract methods to test default implementation // of non-abstract methods @@ -20,6 +16,15 @@ class TestOrchestrator extends TokenOrchestrator { } describe('TokenOrchestrator', () => { + beforeEach(() => { + // Mock DPoP token (and signingAuthority) + testToken = makeTestToken(null, { tokenType: 'DPoP' }); + jest.spyOn(Platform.DPoPSigningAuthority, 'sign').mockImplementation(async (request) => { + request.headers.set('dpop', 'fakedpopvalue'); + return request; + }); + }); + describe('authorize impl', () => { it('should sign request with dpop signature', async () => { const orch = new TestOrchestrator(); diff --git a/packages/auth-foundation/test/spec/oauth2/client.spec.ts b/packages/auth-foundation/test/spec/oauth2/client.spec.ts index 0ec8a7c..c67d096 100644 --- a/packages/auth-foundation/test/spec/oauth2/client.spec.ts +++ b/packages/auth-foundation/test/spec/oauth2/client.spec.ts @@ -8,6 +8,7 @@ jest.mock('src/http/oktaUserAgent', () => { import { Token, TokenInit } from 'src/Token'; import { OAuth2Client } from 'src/oauth2/client'; import { OAuth2Error, TokenError, JWTError } from 'src/errors'; +import { Platform } from 'src/platform/Platform'; import { mockTokenResponse } from '@repo/jest-helpers/browser/helpers'; const fetchSpy = global.fetch = jest.fn(); @@ -122,8 +123,8 @@ describe('OAuth2Client', () => { it('dpop nonce / cache', async () => { client.configuration.dpop = true; jest.spyOn(client, 'jwks').mockResolvedValue({}); - jest.spyOn(client.dpopSigningAuthority, 'createDPoPKeyPair').mockResolvedValue('dpopPairId'); - jest.spyOn(client.dpopSigningAuthority, 'sign').mockImplementation((request) => Promise.resolve(request)); + jest.spyOn(Platform.DPoPSigningAuthority, 'createDPoPKeyPair').mockResolvedValue('dpopPairId'); + jest.spyOn(Platform.DPoPSigningAuthority, 'sign').mockImplementation((request) => Promise.resolve(request)); fetchSpy.mockImplementation( () => Response.json({ token_type: 'DPoP', expires_in: 300, @@ -145,8 +146,8 @@ describe('OAuth2Client', () => { }); await client.exchange(tokenRequest1); expect(fetchSpy).toHaveBeenLastCalledWith(expect.any(Request)); - expect(client.dpopSigningAuthority.sign).toHaveBeenCalledTimes(1); - expect(client.dpopSigningAuthority.sign).toHaveBeenLastCalledWith( + expect(Platform.DPoPSigningAuthority.sign).toHaveBeenCalledTimes(1); + expect(Platform.DPoPSigningAuthority.sign).toHaveBeenLastCalledWith( expect.any(Request), expect.objectContaining({ keyPairId: 'dpopPairId' @@ -164,8 +165,8 @@ describe('OAuth2Client', () => { }); await client.exchange(tokenRequest2); expect(fetchSpy).toHaveBeenLastCalledWith(expect.any(Request)); - expect(client.dpopSigningAuthority.sign).toHaveBeenCalledTimes(2); - expect(client.dpopSigningAuthority.sign).toHaveBeenLastCalledWith( + expect(Platform.DPoPSigningAuthority.sign).toHaveBeenCalledTimes(2); + expect(Platform.DPoPSigningAuthority.sign).toHaveBeenLastCalledWith( expect.any(Request), expect.objectContaining({ keyPairId: 'dpopPairId', @@ -549,7 +550,7 @@ describe('OAuth2Client', () => { describe('DPoP proof clock skew recovery', () => { beforeEach(() => { client.configuration.dpop = true; - jest.spyOn(client.dpopSigningAuthority, 'sign').mockImplementation((request) => request); + jest.spyOn(Platform.DPoPSigningAuthority, 'sign').mockImplementation((request) => Promise.resolve(request)); }); test('isDPoPProofClockSkewError', () => { @@ -967,13 +968,15 @@ describe('OAuth2Client', () => { }); jest.spyOn((client as any), 'jwks').mockResolvedValue({ keys: [{ kid: 'foo', alg: 'bar'}]}); - const TimeCoordinator = (await import('src/utils/TimeCoordinator')).default; + const TimeCoordinator = (await import('src/utils/TimeCoordinator')).__internalTimeCoordinator; testContext = { client, original, tokenResponse, TimeCoordinator }; }); afterEach(() => { // needed to reset the dynamically imported `TimeCoordinator` singleton instance jest.resetModules(); + const { TimeCoordinator } = testContext; + TimeCoordinator.clockSkew = 0; }); it('should calculate clock skew when Date header is available', async () => { diff --git a/packages/auth-foundation/test/tsconfig.json b/packages/auth-foundation/test/tsconfig.json index cb145ae..86eca29 100644 --- a/packages/auth-foundation/test/tsconfig.json +++ b/packages/auth-foundation/test/tsconfig.json @@ -12,7 +12,8 @@ "apps/**/*.ts", "e2e/**/*.ts", "helpers/**/*.ts", - "spec/**/*.ts", + "spec/**/*.ts", + "jest.setupAfterEnv.ts", ], "exclude": [ "node_modules" diff --git a/packages/oauth2-flows/jest.browser.config.js b/packages/oauth2-flows/jest.browser.config.js index f0815db..b2239e9 100644 --- a/packages/oauth2-flows/jest.browser.config.js +++ b/packages/oauth2-flows/jest.browser.config.js @@ -9,7 +9,7 @@ const config = { __PKG_VERSION__: pkg.version, }, moduleNameMapper: { - '^@okta/auth-foundation/client$': '/../auth-foundation/src/client.ts', + '^@okta/auth-foundation/core$': '/../auth-foundation/src/core.ts', '^@okta/auth-foundation/internal$': '/../auth-foundation/src/internal.ts', '^@okta/auth-foundation$': '/../auth-foundation/src/index.ts' }, diff --git a/packages/oauth2-flows/jest.node.config.js b/packages/oauth2-flows/jest.node.config.js index 2bee184..172c544 100644 --- a/packages/oauth2-flows/jest.node.config.js +++ b/packages/oauth2-flows/jest.node.config.js @@ -9,7 +9,7 @@ const config = { __PKG_VERSION__: pkg.version, }, moduleNameMapper: { - '^@okta/auth-foundation/client$': '/../auth-foundation/src/client.ts', + '^@okta/auth-foundation/core$': '/../auth-foundation/src/core.ts', '^@okta/auth-foundation/internal$': '/../auth-foundation/src/internal.ts', '^@okta/auth-foundation$': '/../auth-foundation/src/index.ts', }, diff --git a/packages/oauth2-flows/rollup.config.mjs b/packages/oauth2-flows/rollup.config.mjs index 542a166..8757ec8 100644 --- a/packages/oauth2-flows/rollup.config.mjs +++ b/packages/oauth2-flows/rollup.config.mjs @@ -6,7 +6,7 @@ export default { ...baseConfig(ts, pkg), external: [ ...Object.keys(pkg.peerDependencies), - '@okta/auth-foundation/client', + '@okta/auth-foundation/core', '@okta/auth-foundation/internal' ], }; diff --git a/packages/oauth2-flows/src/AuthTransaction.ts b/packages/oauth2-flows/src/AuthTransaction.ts index f43e8ad..544a6a2 100644 --- a/packages/oauth2-flows/src/AuthTransaction.ts +++ b/packages/oauth2-flows/src/AuthTransaction.ts @@ -3,7 +3,7 @@ * @mergeModuleWith Core */ -import { randomBytes, type JsonRecord } from '@okta/auth-foundation'; +import { randomBytes, type JsonRecord } from '@okta/auth-foundation/core'; import { AuthContext } from './types.ts'; diff --git a/packages/oauth2-flows/src/AuthenticationFlow.ts b/packages/oauth2-flows/src/AuthenticationFlow.ts index ec9175c..b67f962 100644 --- a/packages/oauth2-flows/src/AuthenticationFlow.ts +++ b/packages/oauth2-flows/src/AuthenticationFlow.ts @@ -2,7 +2,7 @@ * @module Core */ -import { EventEmitter, Emitter, AuthSdkError } from '@okta/auth-foundation'; +import { EventEmitter, Emitter, AuthSdkError } from '@okta/auth-foundation/core'; /** diff --git a/packages/oauth2-flows/src/AuthorizationCodeFlow/index.ts b/packages/oauth2-flows/src/AuthorizationCodeFlow/index.ts index d6dbde7..a9b56b1 100644 --- a/packages/oauth2-flows/src/AuthorizationCodeFlow/index.ts +++ b/packages/oauth2-flows/src/AuthorizationCodeFlow/index.ts @@ -10,6 +10,7 @@ import { type TimeInterval, type AcrValues, type JsonRecord, + OAuth2Client, PKCE, OAuth2Error, isOAuth2ErrorResponse, @@ -18,8 +19,7 @@ import { mergeURLSearchParameters, Token, AuthSdkError, -} from '@okta/auth-foundation'; -import OAuth2Client from '@okta/auth-foundation/client'; +} from '@okta/auth-foundation/core'; import { AuthenticationFlow, AuthenticationFlowError diff --git a/packages/oauth2-flows/src/LogoutFlow.ts b/packages/oauth2-flows/src/LogoutFlow.ts index cb979fd..487a75e 100644 --- a/packages/oauth2-flows/src/LogoutFlow.ts +++ b/packages/oauth2-flows/src/LogoutFlow.ts @@ -2,7 +2,7 @@ * @module Core */ -import { AuthSdkError } from '@okta/auth-foundation'; +import { AuthSdkError } from '@okta/auth-foundation/core'; import { AuthenticationFlow } from './AuthenticationFlow.ts'; /** diff --git a/packages/oauth2-flows/src/SessionLogoutFlow/index.ts b/packages/oauth2-flows/src/SessionLogoutFlow/index.ts index 39f3fe0..04f7067 100644 --- a/packages/oauth2-flows/src/SessionLogoutFlow/index.ts +++ b/packages/oauth2-flows/src/SessionLogoutFlow/index.ts @@ -4,11 +4,11 @@ import type { AuthContext } from '../types.ts'; import { + OAuth2Client, randomBytes, OAuth2Error, mergeURLSearchParameters -} from '@okta/auth-foundation'; -import OAuth2Client from '@okta/auth-foundation/client'; +} from '@okta/auth-foundation/core'; import { LogoutFlow } from '../LogoutFlow.ts'; diff --git a/packages/oauth2-flows/test/spec/AuthTransaction.spec.ts b/packages/oauth2-flows/test/spec/AuthTransaction.spec.ts index 9d55090..8bbaf77 100644 --- a/packages/oauth2-flows/test/spec/AuthTransaction.spec.ts +++ b/packages/oauth2-flows/test/spec/AuthTransaction.spec.ts @@ -1,5 +1,6 @@ import { AuthTransaction, type DefaultTransactionStorage } from 'src/AuthTransaction'; + it('AuthTransaction', async () => { const storage = AuthTransaction.storage as DefaultTransactionStorage; diff --git a/packages/oauth2-flows/test/spec/AuthorizationCodeFlow.spec.ts b/packages/oauth2-flows/test/spec/AuthorizationCodeFlow.spec.ts index 27d9973..a5b7f7a 100644 --- a/packages/oauth2-flows/test/spec/AuthorizationCodeFlow.spec.ts +++ b/packages/oauth2-flows/test/spec/AuthorizationCodeFlow.spec.ts @@ -1,9 +1,9 @@ -import { OAuth2Error } from '@okta/auth-foundation'; -import OAuth2Client from '@okta/auth-foundation/client'; +import { OAuth2Client, OAuth2Error } from '@okta/auth-foundation'; import { AuthenticationFlowError } from 'src/AuthenticationFlow'; import { AuthorizationCodeFlow } from 'src/AuthorizationCodeFlow'; import { AuthTransaction } from 'src/AuthTransaction'; + describe('AuthorizationCodeFlow', () => { const authParams = { baseURL: 'https://fake.okta.com', diff --git a/packages/oauth2-flows/test/spec/SessionLogoutFlow.spec.ts b/packages/oauth2-flows/test/spec/SessionLogoutFlow.spec.ts index 2787a9a..999a852 100644 --- a/packages/oauth2-flows/test/spec/SessionLogoutFlow.spec.ts +++ b/packages/oauth2-flows/test/spec/SessionLogoutFlow.spec.ts @@ -1,5 +1,4 @@ -import { OAuth2Error } from '@okta/auth-foundation'; -import OAuth2Client from '@okta/auth-foundation/client'; +import { OAuth2Client, OAuth2Error } from '@okta/auth-foundation'; import { SessionLogoutFlow } from 'src/SessionLogoutFlow'; diff --git a/packages/spa-platform/jest.config.js b/packages/spa-platform/jest.config.js index b7722f8..39b548e 100644 --- a/packages/spa-platform/jest.config.js +++ b/packages/spa-platform/jest.config.js @@ -15,8 +15,8 @@ const config = { '/src/index.ts', ], moduleNameMapper: { - // TODO: why is this required? yarn workspace and jest don't seem to get along? - '^@okta/auth-foundation/client$': '/../auth-foundation/src/client.ts', + // NOTE: auth-foundation/core maps to src/index.ts so Default Dependencies (like TimeCoordinator) are loaded + '^@okta/auth-foundation/core$': '/../auth-foundation/src/index.ts', '^@okta/auth-foundation/internal$': '/../auth-foundation/src/internal.ts', '^@okta/auth-foundation$': '/../auth-foundation/src/index.ts', '^@okta/oauth2-flows$': '/../oauth2-flows/src/index.ts', diff --git a/packages/spa-platform/package.json b/packages/spa-platform/package.json index c403d2d..fb37364 100644 --- a/packages/spa-platform/package.json +++ b/packages/spa-platform/package.json @@ -18,18 +18,6 @@ "types": "./dist/types/index.d.ts", "import": "./dist/esm/index.js" }, - "./fetch": { - "types": "./dist/types/FetchClient/index.d.ts", - "import": "./dist/esm/FetchClient/index.js" - }, - "./orchestrator": { - "types": "./dist/types/orchestrators/index.d.ts", - "import": "./dist/esm/orchestrators/index.js" - }, - "./flows": { - "types": "./dist/types/flows/index.d.ts", - "import": "./dist/esm/flows/index.js" - }, "./package.json": "./package.json" }, "scripts": { diff --git a/packages/spa-platform/rollup.config.mjs b/packages/spa-platform/rollup.config.mjs index d63bb0f..fe4cce7 100644 --- a/packages/spa-platform/rollup.config.mjs +++ b/packages/spa-platform/rollup.config.mjs @@ -6,15 +6,9 @@ const base = baseConfig(ts, pkg); export default { ...base, - input: [ - base.input, - 'src/FetchClient/index.ts', - 'src/orchestrators/index.ts', - 'src/flows/index.ts' - ], external: [ ...Object.keys(pkg.peerDependencies), - '@okta/auth-foundation/client', + '@okta/auth-foundation/core', '@okta/auth-foundation/internal', ], }; diff --git a/packages/spa-platform/src/Credential/Credential.ts b/packages/spa-platform/src/Credential/Credential.ts index 45ce2ef..62861a5 100644 --- a/packages/spa-platform/src/Credential/Credential.ts +++ b/packages/spa-platform/src/Credential/Credential.ts @@ -7,7 +7,7 @@ import { Credential as CredentialBase, type RequestAuthorizer, type JSONSerializable, -} from '@okta/auth-foundation'; +} from '@okta/auth-foundation/core'; import { CredentialCoordinatorImpl } from './CredentialCoordinator.ts'; diff --git a/packages/spa-platform/src/Credential/CredentialCoordinator.ts b/packages/spa-platform/src/Credential/CredentialCoordinator.ts index a36ab7f..b3fc4d5 100644 --- a/packages/spa-platform/src/Credential/CredentialCoordinator.ts +++ b/packages/spa-platform/src/Credential/CredentialCoordinator.ts @@ -9,14 +9,14 @@ import type { TokenStorageEvents, JsonRecord, TokenInit, -} from '@okta/auth-foundation'; +} from '@okta/auth-foundation/core'; import { + Token, CredentialCoordinator, CredentialCoordinatorImpl as CredentialCoordinatorBase, shortID, pause, -} from '@okta/auth-foundation'; -import { Token } from '../platform/index.ts'; +} from '@okta/auth-foundation/core'; import { DefaultCredentialDataSource } from './CredentialDataSource.ts'; import { BrowserTokenStorage } from './TokenStorage.ts'; import { isFirefox } from '../utils/UserAgent.ts'; diff --git a/packages/spa-platform/src/Credential/CredentialDataSource.ts b/packages/spa-platform/src/Credential/CredentialDataSource.ts index 1d3b789..172fe00 100644 --- a/packages/spa-platform/src/Credential/CredentialDataSource.ts +++ b/packages/spa-platform/src/Credential/CredentialDataSource.ts @@ -7,7 +7,7 @@ import { type ConfigurationParams, DefaultCredentialDataSource as BaseCredentialDataSource, CredentialDataSource -} from '@okta/auth-foundation'; +} from '@okta/auth-foundation/core'; import { OAuth2Client } from '../platform/index.ts'; diff --git a/packages/spa-platform/src/Credential/TokenStorage.ts b/packages/spa-platform/src/Credential/TokenStorage.ts index 099cb1e..04206eb 100644 --- a/packages/spa-platform/src/Credential/TokenStorage.ts +++ b/packages/spa-platform/src/Credential/TokenStorage.ts @@ -4,14 +4,14 @@ */ import { - JsonRecord, - TokenStorage, - TokenStorageEvents, + Token, + type JsonRecord, + type TokenStorage, + type TokenStorageEvents, CredentialError, EventEmitter -} from '@okta/auth-foundation'; +} from '@okta/auth-foundation/core'; import { buf, b64u } from '@okta/auth-foundation/internal'; -import { Token } from '../platform/index.ts'; import { IndexedDBStore } from '../utils/IndexedDBStore.ts'; diff --git a/packages/spa-platform/src/FetchClient/index.ts b/packages/spa-platform/src/FetchClient/index.ts index f74c6d6..9375907 100644 --- a/packages/spa-platform/src/FetchClient/index.ts +++ b/packages/spa-platform/src/FetchClient/index.ts @@ -1,7 +1,7 @@ import { FetchClient as FetchClientBase, type DPoPNonceCache -} from '@okta/auth-foundation'; +} from '@okta/auth-foundation/core'; import { PersistentCache } from '../platform/dpop/index.ts'; /** diff --git a/packages/spa-platform/src/flows/AuthorizationCodeFlow.ts b/packages/spa-platform/src/flows/AuthorizationCodeFlow.ts index 187c386..37d86db 100644 --- a/packages/spa-platform/src/flows/AuthorizationCodeFlow.ts +++ b/packages/spa-platform/src/flows/AuthorizationCodeFlow.ts @@ -2,7 +2,7 @@ import { type OAuth2ErrorResponse, isOAuth2ErrorResponse, OAuth2Error -} from '@okta/auth-foundation'; +} from '@okta/auth-foundation/core'; import { AuthTransaction, AuthorizationCodeFlow as AuthorizationCodeFlowBase, diff --git a/packages/spa-platform/src/flows/TransactionStorage.ts b/packages/spa-platform/src/flows/TransactionStorage.ts index 3414732..6cfc538 100644 --- a/packages/spa-platform/src/flows/TransactionStorage.ts +++ b/packages/spa-platform/src/flows/TransactionStorage.ts @@ -1,4 +1,4 @@ -import type { JsonRecord } from '@okta/auth-foundation'; +import type { JsonRecord } from '@okta/auth-foundation/core'; import type { TransactionStorage } from '@okta/oauth2-flows'; import { LocalStorageCache } from '../utils/LocalStorageCache.ts'; diff --git a/packages/spa-platform/src/index.ts b/packages/spa-platform/src/index.ts index c95870c..3fbc355 100644 --- a/packages/spa-platform/src/index.ts +++ b/packages/spa-platform/src/index.ts @@ -9,12 +9,33 @@ import { addEnv } from '@okta/auth-foundation/internal'; declare const __PKG_NAME__: string; declare const __PKG_VERSION__: string; - addEnv(`${__PKG_NAME__}/${__PKG_VERSION__}`); -export * from './platform/index.ts'; -export * from './Credential/index.ts'; +import { Platform } from '@okta/auth-foundation/core'; +import { DefaultSigningAuthority } from './platform/dpop/authority.ts'; +import { TimeCoordinator } from '@okta/auth-foundation/internal'; + +Platform.registerDefaultsLoader(() => ({ + TimeCoordinator, + DPoPSigningAuthority: DefaultSigningAuthority +})); + +export * from '@okta/auth-foundation/core'; + +export { Credential } from './Credential/Credential.ts'; +export { CredentialCoordinatorImpl } from './Credential/CredentialCoordinator.ts'; +export { BrowserTokenStorage } from './Credential/TokenStorage.ts'; +export { DefaultCredentialDataSource } from './Credential/CredentialDataSource.ts'; + +export { FetchClient } from './FetchClient/index.ts'; + export * from './orchestrators/index.ts'; -export * from './FetchClient/index.ts'; + +export * from './flows/index.ts'; + +export { DefaultSigningAuthority } from './platform/dpop/authority.ts'; +export { clearDPoPKeyPairs } from './platform/index.ts'; +export { PersistentCache } from './platform/dpop/nonceCache.ts'; +export { OAuth2Client } from './platform/OAuth2Client.ts'; export * from './utils/isModernBrowser.ts'; diff --git a/packages/spa-platform/src/orchestrators/AuthorizationCodeFlowOrchestrator.ts b/packages/spa-platform/src/orchestrators/AuthorizationCodeFlowOrchestrator.ts index 8ec8981..4f4960a 100644 --- a/packages/spa-platform/src/orchestrators/AuthorizationCodeFlowOrchestrator.ts +++ b/packages/spa-platform/src/orchestrators/AuthorizationCodeFlowOrchestrator.ts @@ -4,14 +4,14 @@ */ import { + Token, hasSameValues, toRelativeUrl, TokenOrchestrator, TokenOrchestratorError, EventEmitter -} from '@okta/auth-foundation'; +} from '@okta/auth-foundation/core'; import { AuthorizationCodeFlow } from '../flows/index.ts'; -import { Token } from '../platform/index.ts'; import { Credential } from '../Credential/index.ts'; diff --git a/packages/spa-platform/src/orchestrators/HostOrchestrator/Host.ts b/packages/spa-platform/src/orchestrators/HostOrchestrator/Host.ts index e33eb33..c56ef87 100644 --- a/packages/spa-platform/src/orchestrators/HostOrchestrator/Host.ts +++ b/packages/spa-platform/src/orchestrators/HostOrchestrator/Host.ts @@ -1,12 +1,12 @@ import type { HostOrchestrator as HO } from './index.ts'; import { + Token, shortID, type TokenPrimitiveInit, EventEmitter, type Emitter, TokenOrchestrator -} from '@okta/auth-foundation'; -import { Token } from '../../platform/index.ts'; +} from '@okta/auth-foundation/core'; import { OrchestrationBridge } from './OrchestrationBridge.ts'; diff --git a/packages/spa-platform/src/orchestrators/HostOrchestrator/OrchestrationBridge.ts b/packages/spa-platform/src/orchestrators/HostOrchestrator/OrchestrationBridge.ts index 266f72a..ac5543d 100644 --- a/packages/spa-platform/src/orchestrators/HostOrchestrator/OrchestrationBridge.ts +++ b/packages/spa-platform/src/orchestrators/HostOrchestrator/OrchestrationBridge.ts @@ -1,5 +1,5 @@ import type { HostOrchestrator } from './index.ts'; -import { TaskBridge } from '@okta/auth-foundation'; +import { TaskBridge } from '@okta/auth-foundation/core'; import { LocalBroadcastChannel } from '../../utils/LocalBroadcastChannel.ts'; const defaultOptions = { diff --git a/packages/spa-platform/src/orchestrators/HostOrchestrator/SubApp.ts b/packages/spa-platform/src/orchestrators/HostOrchestrator/SubApp.ts index 9a363a3..2cb5cb1 100644 --- a/packages/spa-platform/src/orchestrators/HostOrchestrator/SubApp.ts +++ b/packages/spa-platform/src/orchestrators/HostOrchestrator/SubApp.ts @@ -1,5 +1,6 @@ import type { HostOrchestrator as HO } from './index.ts'; import { + Token, shortID, type SubSet, ignoreUndefineds, @@ -7,9 +8,8 @@ import { TokenOrchestratorError, EventEmitter, hashObject -} from '@okta/auth-foundation'; +} from '@okta/auth-foundation/core'; import { validateString } from '@okta/auth-foundation/internal'; -import { Token } from '../../platform/index.ts'; import { OrchestrationBridge } from './OrchestrationBridge.ts'; diff --git a/packages/spa-platform/src/orchestrators/HostOrchestrator/index.ts b/packages/spa-platform/src/orchestrators/HostOrchestrator/index.ts index a3d445f..3c23f6c 100644 --- a/packages/spa-platform/src/orchestrators/HostOrchestrator/index.ts +++ b/packages/spa-platform/src/orchestrators/HostOrchestrator/index.ts @@ -9,7 +9,7 @@ import { type JsonRecord, type TokenPrimitiveInit, Token -} from '@okta/auth-foundation'; +} from '@okta/auth-foundation/core'; import { HostOrchestrator as HostApp } from './Host.ts'; import { SubAppOrchestrator } from './SubApp.ts'; diff --git a/packages/spa-platform/src/platform/OAuth2Client.ts b/packages/spa-platform/src/platform/OAuth2Client.ts index 5a4a8e8..d9a952b 100644 --- a/packages/spa-platform/src/platform/OAuth2Client.ts +++ b/packages/spa-platform/src/platform/OAuth2Client.ts @@ -4,18 +4,16 @@ */ import { - TokenInit, + Token, + OAuth2Client as OAuth2ClientBase, + type TokenInit, OAuth2ErrorResponse, isOAuth2ErrorResponse, OAuth2Error, - DPoPSigningAuthority, - DPoPNonceCache -} from '@okta/auth-foundation'; -import OAuth2ClientBase from '@okta/auth-foundation/client'; + type DPoPNonceCache +} from '@okta/auth-foundation/core'; import { SynchronizedResult } from '../utils/SynchronizedResult.ts'; -import { Token } from './Token.ts'; import { PersistentCache } from './dpop/index.ts'; -import { DefaultSigningAuthority } from './dpop/authority.ts'; /** @@ -24,13 +22,8 @@ import { DefaultSigningAuthority } from './dpop/authority.ts'; * @group OAuth2Client */ export class OAuth2Client extends OAuth2ClientBase { - public readonly dpopSigningAuthority: DPoPSigningAuthority = DefaultSigningAuthority; protected readonly dpopNonceCache: DPoPNonceCache = new PersistentCache('okta-dpop-nonce'); - protected createToken (init: TokenInit): Token { - return new Token(init); - } - protected prepareRefreshRequest (token: Token, scopes?: string[]): Promise { if (!token.refreshToken) { throw new OAuth2Error(`Missing token: refreshToken`); diff --git a/packages/spa-platform/src/platform/Token.ts b/packages/spa-platform/src/platform/Token.ts deleted file mode 100644 index 5976ae7..0000000 --- a/packages/spa-platform/src/platform/Token.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @module - * @mergeModuleWith Platform - */ - -import { Token as TokenBase, DPoPSigningAuthority } from '@okta/auth-foundation'; -import { DefaultSigningAuthority } from './dpop/authority.ts'; - - -/** - * Browser-specific implementation of {@link AuthFoundation!Token | Token} - * - * @group Token - * @noInheritDoc - */ -export class Token extends TokenBase { - public readonly dpopSigningAuthority: DPoPSigningAuthority = DefaultSigningAuthority; -} - -/** - * @group Token - */ -export namespace Token { - /** - * Re-exports `@okta/auth-foundation` `Token.Metadata` - */ - export type Metadata = TokenBase.Metadata; - /** - * Re-exports `@okta/auth-foundation` `Token.Context` - */ - export type Context = TokenBase.Context; -} diff --git a/packages/spa-platform/src/platform/dpop/authority.ts b/packages/spa-platform/src/platform/dpop/authority.ts index b7bb859..4e4fb84 100644 --- a/packages/spa-platform/src/platform/dpop/authority.ts +++ b/packages/spa-platform/src/platform/dpop/authority.ts @@ -3,7 +3,7 @@ * @internal */ -import { type DPoPStorage, DPoPSigningAuthorityImpl, DPoPSigningAuthority } from '@okta/auth-foundation'; +import { type DPoPStorage, DPoPSigningAuthorityImpl, DPoPSigningAuthority } from '@okta/auth-foundation/core'; import { IndexedDBStore } from '../../utils/IndexedDBStore.ts'; diff --git a/packages/spa-platform/src/platform/dpop/nonceCache.ts b/packages/spa-platform/src/platform/dpop/nonceCache.ts index 1fceeff..672725c 100644 --- a/packages/spa-platform/src/platform/dpop/nonceCache.ts +++ b/packages/spa-platform/src/platform/dpop/nonceCache.ts @@ -3,12 +3,9 @@ * @internal */ -import { DPoPNonceCache } from '@okta/auth-foundation'; +import type { DPoPNonceCache } from '@okta/auth-foundation/core'; import { LocalStorageCache } from '../../utils/LocalStorageCache.ts'; -/** @internal */ -const _20_HOURS = 60 * 60 * 20; - /** * @internal @@ -18,7 +15,7 @@ export class PersistentCache implements DPoPNonceCache { readonly #cache: LocalStorageCache; constructor (storageKey: string, clearOnParseError: boolean = true) { - this.#cache = new LocalStorageCache(storageKey, _20_HOURS, clearOnParseError); + this.#cache = new LocalStorageCache(storageKey, clearOnParseError); } public async getNonce (key: string): Promise { diff --git a/packages/spa-platform/src/platform/index.ts b/packages/spa-platform/src/platform/index.ts index 940510d..ef5fec4 100644 --- a/packages/spa-platform/src/platform/index.ts +++ b/packages/spa-platform/src/platform/index.ts @@ -2,7 +2,6 @@ * @module Platform */ -export * from './Token.ts'; export * from './OAuth2Client.ts'; import { DefaultSigningAuthority } from './dpop/authority.ts'; diff --git a/packages/spa-platform/src/utils/LocalBroadcastChannel.ts b/packages/spa-platform/src/utils/LocalBroadcastChannel.ts index e66fd87..0a87eea 100644 --- a/packages/spa-platform/src/utils/LocalBroadcastChannel.ts +++ b/packages/spa-platform/src/utils/LocalBroadcastChannel.ts @@ -3,7 +3,7 @@ * @internal */ -import type { JsonRecord, BroadcastChannelLike } from '@okta/auth-foundation'; +import type { JsonRecord, BroadcastChannelLike } from '@okta/auth-foundation/core'; import { validateURL } from '@okta/auth-foundation/internal'; export type LocalBroadcastChannelMessage = { diff --git a/packages/spa-platform/src/utils/LocalStorageCache.ts b/packages/spa-platform/src/utils/LocalStorageCache.ts index 2db2347..ee20ec2 100644 --- a/packages/spa-platform/src/utils/LocalStorageCache.ts +++ b/packages/spa-platform/src/utils/LocalStorageCache.ts @@ -3,10 +3,12 @@ * @internal */ -import { Timestamp, type Json, type JsonPrimitive } from '@okta/auth-foundation'; - -/** @internal */ -const _20_HOURS = 60 * 60 * 20; +import { + Timestamp, + type Json, + type JsonPrimitive, + type Seconds +} from '@okta/auth-foundation/core'; /** @@ -17,10 +19,11 @@ const _20_HOURS = 60 * 60 * 20; export class LocalStorageCache { constructor ( protected storageKey: string, - protected expirationDuration: number = _20_HOURS, public clearOnParseError: boolean = true ) {} + static cacheDuration: Seconds = 60 * 60 * 20; // defaults to 20 hours + protected getStore (): Record { let store: Record = {}; try { @@ -40,7 +43,7 @@ export class LocalStorageCache { for (const [key, value] of Object.entries(store)) { // cast as `any` because .entries assumes type is `unknown` const ts = (value as any).ts; - if (!ts || Math.abs(Timestamp.from(ts).timeSinceNow()) > _20_HOURS) { + if (!ts || Math.abs(Timestamp.from(ts).timeSinceNow()) > LocalStorageCache.cacheDuration) { delete store[key]; } } diff --git a/packages/spa-platform/test/helpers/makeTestResource.ts b/packages/spa-platform/test/helpers/makeTestResource.ts index 2c86a9e..f263c3d 100644 --- a/packages/spa-platform/test/helpers/makeTestResource.ts +++ b/packages/spa-platform/test/helpers/makeTestResource.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Token, OAuth2Client } from 'src/platform'; +import { Token } from '@okta/auth-foundation/core'; +import { OAuth2Client } from 'src/platform'; import { Credential } from 'src/Credential'; import { mockIDToken, mockTokenResponse } from '@repo/jest-helpers/browser/helpers'; diff --git a/packages/spa-platform/test/spec/BrowserTokenStorage.spec.ts b/packages/spa-platform/test/spec/BrowserTokenStorage.spec.ts index 27947c2..d97f667 100644 --- a/packages/spa-platform/test/spec/BrowserTokenStorage.spec.ts +++ b/packages/spa-platform/test/spec/BrowserTokenStorage.spec.ts @@ -1,5 +1,4 @@ -import { CredentialError } from '@okta/auth-foundation'; -import { Token } from 'src/platform'; +import { Token, CredentialError } from '@okta/auth-foundation'; import { BrowserTokenStorage } from 'src/Credential/TokenStorage'; import { makeTestToken, MockIndexedDBStore } from '../helpers/makeTestResource'; diff --git a/packages/spa-platform/test/spec/FetchClient.spec.ts b/packages/spa-platform/test/spec/FetchClient.spec.ts index 0e6c1b0..8bfc963 100644 --- a/packages/spa-platform/test/spec/FetchClient.spec.ts +++ b/packages/spa-platform/test/spec/FetchClient.spec.ts @@ -1,4 +1,4 @@ -import { TokenOrchestrator, APIClientError, DPoPNonceCache } from '@okta/auth-foundation'; +import { TokenOrchestrator, APIClientError, DPoPNonceCache, Platform } from '@okta/auth-foundation'; import { FetchClient } from 'src/FetchClient'; import { randStr } from '@repo/jest-helpers/browser/helpers'; import { makeTestToken } from '../helpers/makeTestResource'; @@ -62,7 +62,7 @@ describe('FetchClient', () => { // test with a (mocked) dpop-bound token const dpopToken = makeTestToken(null, { tokenType: 'DPoP' }); - dpopToken.dpopSigningAuthority.sign = jest.fn().mockImplementation(async (request) => { + Platform.DPoPSigningAuthority.sign = jest.fn().mockImplementation(async (request) => { request.headers.set('dpop', 'fakedpopvalue'); return request; }); diff --git a/packages/spa-platform/test/spec/flows/AuthorizationCodeFlow.spec.ts b/packages/spa-platform/test/spec/flows/AuthorizationCodeFlow.spec.ts index 41e2669..f6745c5 100644 --- a/packages/spa-platform/test/spec/flows/AuthorizationCodeFlow.spec.ts +++ b/packages/spa-platform/test/spec/flows/AuthorizationCodeFlow.spec.ts @@ -13,8 +13,7 @@ jest.mock('@okta/auth-foundation', () => { }; }); -import { OAuth2Error } from '@okta/auth-foundation'; -import { Token } from 'src/platform'; +import { Token, OAuth2Error } from '@okta/auth-foundation'; import { AuthorizationCodeFlow as Base, AuthenticationFlowError } from '@okta/oauth2-flows'; import { AuthorizationCodeFlow } from 'src/flows'; import { oauthClient, makeTestToken } from '../../helpers/makeTestResource'; diff --git a/packages/spa-platform/test/spec/orchestrators/AuthorizationCodeFlowOrchestrator.spec.ts b/packages/spa-platform/test/spec/orchestrators/AuthorizationCodeFlowOrchestrator.spec.ts index 795df29..b307c92 100644 --- a/packages/spa-platform/test/spec/orchestrators/AuthorizationCodeFlowOrchestrator.spec.ts +++ b/packages/spa-platform/test/spec/orchestrators/AuthorizationCodeFlowOrchestrator.spec.ts @@ -1,5 +1,4 @@ -import { TokenOrchestratorError, OAuth2Error } from '@okta/auth-foundation'; -import { Token } from 'src/platform'; +import { Token, TokenOrchestratorError, OAuth2Error } from '@okta/auth-foundation'; import { Credential } from 'src/Credential'; import { AuthorizationCodeFlow } from 'src/flows/AuthorizationCodeFlow'; import { AuthorizationCodeFlowOrchestrator } from 'src/orchestrators'; diff --git a/packages/spa-platform/test/spec/orchestrators/HostOrchestrator.spec.ts b/packages/spa-platform/test/spec/orchestrators/HostOrchestrator.spec.ts index ee8691a..ed3c2ea 100644 --- a/packages/spa-platform/test/spec/orchestrators/HostOrchestrator.spec.ts +++ b/packages/spa-platform/test/spec/orchestrators/HostOrchestrator.spec.ts @@ -1,6 +1,5 @@ -import { TokenOrchestrator, TokenOrchestratorError } from '@okta/auth-foundation'; +import { Token, TokenOrchestrator, TokenOrchestratorError, Platform } from '@okta/auth-foundation'; import { mockTokenResponse } from '@repo/jest-helpers/browser/helpers'; -import { Token } from 'src/platform'; import { HostOrchestrator } from 'src/orchestrators/HostOrchestrator/index'; import { LocalBroadcastChannel } from 'src/utils/LocalBroadcastChannel'; @@ -9,7 +8,7 @@ import { LocalBroadcastChannel } from 'src/utils/LocalBroadcastChannel'; const testToken = new Token(mockTokenResponse(null, { tokenType: 'DPoP' })); // @ts-expect-error - forcing property for testing testToken.context = { dpopPairId: 'dpopkey' }; -testToken.dpopSigningAuthority.sign = jest.fn().mockImplementation(async (request) => { +Platform.DPoPSigningAuthority.sign = jest.fn().mockImplementation(async (request) => { request.headers.set('dpop', 'fakedpopvalue'); return request; }); @@ -305,7 +304,7 @@ describe('HostOrchestrator', () => { }); // error case 2 - dpop header never added (should never occur) - testToken.dpopSigningAuthority.sign = jest.fn().mockImplementation(req => req); + Platform.DPoPSigningAuthority.sign = jest.fn().mockImplementation(req => req); findTokenSpy.mockResolvedValueOnce(testToken); await host.parseRequest(event, reply); diff --git a/packages/spa-platform/test/spec/platform/Token.spec.ts b/packages/spa-platform/test/spec/platform/Token.spec.ts deleted file mode 100644 index 89ae42c..0000000 --- a/packages/spa-platform/test/spec/platform/Token.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Token } from 'src/platform'; -import { makeRawTestToken } from '../../helpers/makeTestResource'; - - -// TODO: write some more tests -describe('Token', () => { - - it('can construct', () => { - const t1 = new Token(makeRawTestToken()); - expect(t1).toBeInstanceOf(Token); - }); -}); diff --git a/tooling/eslint-config/sdk.js b/tooling/eslint-config/sdk.js index 2d426de..0448e9a 100644 --- a/tooling/eslint-config/sdk.js +++ b/tooling/eslint-config/sdk.js @@ -56,6 +56,8 @@ module.exports = { "max-len": 0, "max-statements": 0, "camelcase": 0, + "no-restricted-imports": 0, + "no-restricted-syntax": 0, "@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/no-non-null-assertion": 0 } @@ -113,6 +115,25 @@ module.exports = { "ignoreRestSiblings": true, "caughtErrors": "none" }], - "@typescript-eslint/no-namespace": 0 + "@typescript-eslint/no-namespace": 0, + // NOTE: important linting rule + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@okta/auth-foundation', + message: 'Import from "@okta/auth-foundation/core" instead to avoid bundling default platform implementations.', + }, + ] + }, + ], + 'no-restricted-syntax': [ + 'error', + { + selector: "ImportDeclaration > ImportSpecifier[imported.name=/^__internal/]", + message: "Importing from '__internal*' paths is not allowed. Use Platform.X to access this singleton" + } + ] } }