From 31202a1e95a5859748a04a91371f83a5d643ce4d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 20 May 2020 16:32:06 +0200 Subject: [PATCH] packages/core: refactor to remove need to BaseAuthSession --- .../implementations/auth/google/GoogleAuth.ts | 5 ++ .../lib/AuthConnector/types.ts | 5 -- .../RefreshingAuthSessionManager.test.ts | 18 +++++-- .../RefreshingAuthSessionManager.ts | 52 ++++++++++++++----- .../lib/AuthSessionManager/types.ts | 4 +- 5 files changed, 58 insertions(+), 26 deletions(-) diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts b/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts index e3c10d71f7..04b5404c0a 100644 --- a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts @@ -62,6 +62,11 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi { `${SCOPE_PREFIX}userinfo.email`, `${SCOPE_PREFIX}userinfo.profile`, ]), + sessionScopes: session => session.scopes, + sessionShouldRefresh: session => { + const expiresInSec = (session.expiresAt.getTime() - Date.now()) / 1000; + return expiresInSec < 60 * 5; + }, }); return new GoogleAuth(sessionManager); diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/types.ts b/packages/core/src/api/apis/implementations/lib/AuthConnector/types.ts index ad3188dee4..146c31cbe1 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthConnector/types.ts +++ b/packages/core/src/api/apis/implementations/lib/AuthConnector/types.ts @@ -14,11 +14,6 @@ * limitations under the License. */ -export type BaseAuthSession = { - scopes: Set; - expiresAt: Date; -}; - /** * An AuthConnector is responsible for realizing auth session actions * by for example communicating with a backend or interacting with the user. diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts index fb56b9e62d..6841934357 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts +++ b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts @@ -16,15 +16,18 @@ import { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager'; -const theFuture = new Date(Date.now() + 3600000); -const thePast = new Date(Date.now() - 10); +const defaultOptions = { + sessionScopes: (session: { scopes: Set }) => session.scopes, + sessionShouldRefresh: (session: { expired: boolean }) => session.expired, +}; describe('RefreshingAuthSessionManager', () => { it('should save result form createSession', async () => { - const createSession = jest.fn().mockResolvedValue({ expiresAt: theFuture }); + const createSession = jest.fn().mockResolvedValue({ expired: false }); const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); const manager = new RefreshingAuthSessionManager({ connector: { createSession, refreshSession }, + ...defaultOptions, } as any); await manager.getSession({}); @@ -41,11 +44,12 @@ describe('RefreshingAuthSessionManager', () => { const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); const manager = new RefreshingAuthSessionManager({ connector: { createSession, refreshSession }, + ...defaultOptions, } as any); createSession.mockResolvedValue({ scopes: new Set(['a']), - expiresAt: theFuture, + expired: false, }); await manager.getSession({ scope: new Set(['a']) }); expect(createSession).toBeCalledTimes(1); @@ -65,11 +69,12 @@ describe('RefreshingAuthSessionManager', () => { .mockResolvedValue({ scopes: new Set(['a']) }); const manager = new RefreshingAuthSessionManager({ connector: { createSession, refreshSession }, + ...defaultOptions, } as any); createSession.mockResolvedValue({ scopes: new Set(['a']), - expiresAt: thePast, + expired: true, }); await manager.getSession({ scope: new Set(['a']) }); @@ -86,6 +91,7 @@ describe('RefreshingAuthSessionManager', () => { const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); const manager = new RefreshingAuthSessionManager({ connector: { createSession, refreshSession }, + ...defaultOptions, } as any); createSession.mockRejectedValueOnce(new Error('some error')); @@ -99,6 +105,7 @@ describe('RefreshingAuthSessionManager', () => { const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); const manager = new RefreshingAuthSessionManager({ connector: { createSession, refreshSession }, + ...defaultOptions, } as any); expect(await manager.getSession({ optional: true })).toBe(undefined); @@ -119,6 +126,7 @@ describe('RefreshingAuthSessionManager', () => { const removeSession = jest.fn(); const manager = new RefreshingAuthSessionManager({ connector: { removeSession }, + ...defaultOptions, } as any); await manager.removeSession(); diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts index fbbb95a1eb..147d9f4b49 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts @@ -16,10 +16,27 @@ import { hasScopes } from '../../OAuthRequestManager/OAuthPendingRequests'; import { SessionManager } from './types'; -import { BaseAuthSession, AuthConnector } from '../AuthConnector'; +import { AuthConnector } from '../AuthConnector'; -type Options = { +type Options = { + /** + * The connector used for acting on the auth session. + */ connector: AuthConnector; + /** + * A function called to determine the scopes of the session. + */ + sessionScopes: (session: AuthSession) => Set; + /** + * A function called to determine whether it's time for a session to refresh. + * + * This should return true before the session expires, for example, if a session + * expires after 60 minutes, you could return true if the session is older than 45 minutes. + */ + sessionShouldRefresh: (session: AuthSession) => boolean; + /** + * The default scopes that should always be present in a session, defaults to none. + */ defaultScopes?: Set; }; @@ -27,19 +44,28 @@ type Options = { * RefreshingAuthSessionManager manages an underlying session that has * and expiration time and needs to be refreshed periodically. */ -export class RefreshingAuthSessionManager +export class RefreshingAuthSessionManager implements SessionManager { private readonly connector: AuthConnector; private readonly defaultScopes?: Set; + private readonly sessionScopesFunc: (session: AuthSession) => Set; + private readonly sessionShouldRefreshFunc: (session: AuthSession) => boolean; private refreshPromise?: Promise; private currentSession: AuthSession | undefined; constructor(options: Options) { - const { connector, defaultScopes = new Set() } = options; + const { + connector, + defaultScopes = new Set(), + sessionScopes, + sessionShouldRefresh, + } = options; this.connector = connector; this.defaultScopes = defaultScopes; + this.sessionScopesFunc = sessionScopes; + this.sessionShouldRefreshFunc = sessionShouldRefresh; } async getSession(options: { @@ -55,13 +81,16 @@ export class RefreshingAuthSessionManager scope?: Set; }): Promise { if (this.sessionExistsAndHasScope(this.currentSession, options.scope)) { - if (!this.sessionWillExpire(this.currentSession!)) { + const shouldRefresh = this.sessionShouldRefreshFunc(this.currentSession!); + if (!shouldRefresh) { return this.currentSession!; } try { const refreshedSession = await this.collapsedSessionRefresh(); - if (hasScopes(refreshedSession.scopes, this.currentSession!.scopes)) { + const currentScopes = this.sessionScopesFunc(this.currentSession!); + const refreshedScopes = this.sessionScopesFunc(refreshedSession); + if (hasScopes(refreshedScopes, currentScopes)) { this.currentSession = refreshedSession; } return refreshedSession; @@ -113,18 +142,15 @@ export class RefreshingAuthSessionManager if (!scope) { return true; } - return hasScopes(session.scopes, scope); - } - - private sessionWillExpire(session: AuthSession) { - const expiresInSec = (session.expiresAt.getTime() - Date.now()) / 1000; - return expiresInSec < 60 * 5; + const sessionScopes = this.sessionScopesFunc(session); + return hasScopes(sessionScopes, scope); } private getExtendedScope(scopes?: Set) { const newScope = new Set(this.defaultScopes); if (this.currentSession) { - for (const scope of this.currentSession.scopes) { + const sessionScopes = this.sessionScopesFunc(this.currentSession); + for (const scope of sessionScopes) { newScope.add(scope); } } diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/types.ts b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/types.ts index 6475776505..440944cce0 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/types.ts +++ b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/types.ts @@ -14,14 +14,12 @@ * limitations under the License. */ -import { BaseAuthSession } from '../AuthHelper/types'; - /** * A sessions manager keeps track of the current session and makes sure that * multiple simultaneous requests for sessions with different scope are handled * in a correct way. */ -export type SessionManager = { +export type SessionManager = { getSession(options: { optional: false; scope?: Set;