packages/core: refactor to remove need to BaseAuthSession
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -14,11 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type BaseAuthSession = {
|
||||
scopes: Set<string>;
|
||||
expiresAt: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
* An AuthConnector is responsible for realizing auth session actions
|
||||
* by for example communicating with a backend or interacting with the user.
|
||||
|
||||
+13
-5
@@ -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<string> }) => 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();
|
||||
|
||||
+39
-13
@@ -16,10 +16,27 @@
|
||||
|
||||
import { hasScopes } from '../../OAuthRequestManager/OAuthPendingRequests';
|
||||
import { SessionManager } from './types';
|
||||
import { BaseAuthSession, AuthConnector } from '../AuthConnector';
|
||||
import { AuthConnector } from '../AuthConnector';
|
||||
|
||||
type Options<AuthSession extends BaseAuthSession> = {
|
||||
type Options<AuthSession> = {
|
||||
/**
|
||||
* The connector used for acting on the auth session.
|
||||
*/
|
||||
connector: AuthConnector<AuthSession>;
|
||||
/**
|
||||
* A function called to determine the scopes of the session.
|
||||
*/
|
||||
sessionScopes: (session: AuthSession) => Set<string>;
|
||||
/**
|
||||
* 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<string>;
|
||||
};
|
||||
|
||||
@@ -27,19 +44,28 @@ type Options<AuthSession extends BaseAuthSession> = {
|
||||
* RefreshingAuthSessionManager manages an underlying session that has
|
||||
* and expiration time and needs to be refreshed periodically.
|
||||
*/
|
||||
export class RefreshingAuthSessionManager<AuthSession extends BaseAuthSession>
|
||||
export class RefreshingAuthSessionManager<AuthSession>
|
||||
implements SessionManager<AuthSession> {
|
||||
private readonly connector: AuthConnector<AuthSession>;
|
||||
private readonly defaultScopes?: Set<string>;
|
||||
private readonly sessionScopesFunc: (session: AuthSession) => Set<string>;
|
||||
private readonly sessionShouldRefreshFunc: (session: AuthSession) => boolean;
|
||||
|
||||
private refreshPromise?: Promise<AuthSession>;
|
||||
private currentSession: AuthSession | undefined;
|
||||
|
||||
constructor(options: Options<AuthSession>) {
|
||||
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<AuthSession extends BaseAuthSession>
|
||||
scope?: Set<string>;
|
||||
}): Promise<AuthSession | undefined> {
|
||||
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<AuthSession extends BaseAuthSession>
|
||||
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<string>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AuthSession extends BaseAuthSession> = {
|
||||
export type SessionManager<AuthSession> = {
|
||||
getSession(options: {
|
||||
optional: false;
|
||||
scope?: Set<string>;
|
||||
|
||||
Reference in New Issue
Block a user