packages/core: refactor to remove need to BaseAuthSession

This commit is contained in:
Patrik Oldsberg
2020-05-20 16:32:06 +02:00
parent 930c2dba2b
commit 31202a1e95
5 changed files with 58 additions and 26 deletions
@@ -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.
@@ -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();
@@ -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>;