packages/core: separate some common things out from RefreshingAuthSessionManager
This commit is contained in:
+33
-68
@@ -14,29 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { hasScopes } from '../../OAuthRequestManager/OAuthPendingRequests';
|
||||
import { SessionManager } from './types';
|
||||
import {
|
||||
SessionManager,
|
||||
SessionScopesFunc,
|
||||
SessionShouldRefreshFunc,
|
||||
} from './types';
|
||||
import { AuthConnector } from '../AuthConnector';
|
||||
import { SessionScopeHelper } from './common';
|
||||
import { hasScopes } from '../../OAuthRequestManager/OAuthPendingRequests';
|
||||
|
||||
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.
|
||||
*/
|
||||
type Options<T> = {
|
||||
/** The connector used for acting on the auth session */
|
||||
connector: AuthConnector<T>;
|
||||
/** Used to get the scope of the session */
|
||||
sessionScopes: SessionScopesFunc<T>;
|
||||
/** Used to check if the session needs to be refreshed */
|
||||
sessionShouldRefresh: SessionShouldRefreshFunc<T>;
|
||||
/** The default scopes that should always be present in a session, defaults to none. */
|
||||
defaultScopes?: Set<string>;
|
||||
};
|
||||
|
||||
@@ -44,17 +38,16 @@ type Options<AuthSession> = {
|
||||
* RefreshingAuthSessionManager manages an underlying session that has
|
||||
* and expiration time and needs to be refreshed periodically.
|
||||
*/
|
||||
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;
|
||||
export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
|
||||
private readonly connector: AuthConnector<T>;
|
||||
private readonly helper: SessionScopeHelper<T>;
|
||||
private readonly sessionScopesFunc: SessionScopesFunc<T>;
|
||||
private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc<T>;
|
||||
|
||||
private refreshPromise?: Promise<AuthSession>;
|
||||
private currentSession: AuthSession | undefined;
|
||||
private refreshPromise?: Promise<T>;
|
||||
private currentSession: T | undefined;
|
||||
|
||||
constructor(options: Options<AuthSession>) {
|
||||
constructor(options: Options<T>) {
|
||||
const {
|
||||
connector,
|
||||
defaultScopes = new Set(),
|
||||
@@ -63,24 +56,26 @@ export class RefreshingAuthSessionManager<AuthSession>
|
||||
} = options;
|
||||
|
||||
this.connector = connector;
|
||||
this.defaultScopes = defaultScopes;
|
||||
this.sessionScopesFunc = sessionScopes;
|
||||
this.sessionShouldRefreshFunc = sessionShouldRefresh;
|
||||
this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes });
|
||||
}
|
||||
|
||||
async getSession(options: {
|
||||
optional: false;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<AuthSession>;
|
||||
}): Promise<T>;
|
||||
async getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<AuthSession | undefined>;
|
||||
}): Promise<T | undefined>;
|
||||
async getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<AuthSession | undefined> {
|
||||
if (this.sessionExistsAndHasScope(this.currentSession, options.scopes)) {
|
||||
}): Promise<T | undefined> {
|
||||
if (
|
||||
this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes)
|
||||
) {
|
||||
const shouldRefresh = this.sessionShouldRefreshFunc(this.currentSession!);
|
||||
if (!shouldRefresh) {
|
||||
return this.currentSession!;
|
||||
@@ -111,7 +106,7 @@ export class RefreshingAuthSessionManager<AuthSession>
|
||||
// The session might not have the scopes requested so go back and check again
|
||||
return this.getSession(options);
|
||||
} catch {
|
||||
// If the refresh attemp fails we assume we don't have a session, so continue to create one.
|
||||
// If the refresh attempt fails we assume we don't have a session, so continue to create one.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +117,7 @@ export class RefreshingAuthSessionManager<AuthSession>
|
||||
|
||||
// We can call authRequester multiple times, the returned session will contain all requested scopes.
|
||||
this.currentSession = await this.connector.createSession(
|
||||
this.getExtendedScope(options.scopes),
|
||||
this.helper.getExtendedScope(this.currentSession, options.scopes),
|
||||
);
|
||||
return this.currentSession;
|
||||
}
|
||||
@@ -132,37 +127,7 @@ export class RefreshingAuthSessionManager<AuthSession>
|
||||
window.location.reload(); // TODO(Rugvip): make this work without reload?
|
||||
}
|
||||
|
||||
private sessionExistsAndHasScope(
|
||||
session: AuthSession | undefined,
|
||||
scopes?: Set<string>,
|
||||
): boolean {
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
if (!scopes) {
|
||||
return true;
|
||||
}
|
||||
const sessionScopes = this.sessionScopesFunc(session);
|
||||
return hasScopes(sessionScopes, scopes);
|
||||
}
|
||||
|
||||
private getExtendedScope(scopes?: Set<string>) {
|
||||
const newScope = new Set(this.defaultScopes);
|
||||
if (this.currentSession) {
|
||||
const sessionScopes = this.sessionScopesFunc(this.currentSession);
|
||||
for (const scope of sessionScopes) {
|
||||
newScope.add(scope);
|
||||
}
|
||||
}
|
||||
if (scopes) {
|
||||
for (const scope of scopes) {
|
||||
newScope.add(scope);
|
||||
}
|
||||
}
|
||||
return newScope;
|
||||
}
|
||||
|
||||
private async collapsedSessionRefresh(): Promise<AuthSession> {
|
||||
private async collapsedSessionRefresh(): Promise<T> {
|
||||
if (this.refreshPromise) {
|
||||
return this.refreshPromise;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SessionScopesFunc } from './types';
|
||||
|
||||
export function hasScopes(
|
||||
searched: Set<string>,
|
||||
searchFor: Set<string>,
|
||||
): boolean {
|
||||
for (const scope of searchFor) {
|
||||
if (!searched.has(scope)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
type ScopeHelperOptions<T> = {
|
||||
sessionScopes: SessionScopesFunc<T>;
|
||||
defaultScopes?: Set<string>;
|
||||
};
|
||||
|
||||
export class SessionScopeHelper<T> {
|
||||
constructor(private readonly options: ScopeHelperOptions<T>) {}
|
||||
|
||||
sessionExistsAndHasScope(
|
||||
session: T | undefined,
|
||||
scopes?: Set<string>,
|
||||
): boolean {
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
if (!scopes) {
|
||||
return true;
|
||||
}
|
||||
const sessionScopes = this.options.sessionScopes(session);
|
||||
return hasScopes(sessionScopes, scopes);
|
||||
}
|
||||
|
||||
getExtendedScope(session: T | undefined, scopes?: Set<string>) {
|
||||
const newScope = new Set(this.options.defaultScopes);
|
||||
if (session) {
|
||||
const sessionScopes = this.options.sessionScopes(session);
|
||||
for (const scope of sessionScopes) {
|
||||
newScope.add(scope);
|
||||
}
|
||||
}
|
||||
if (scopes) {
|
||||
for (const scope of scopes) {
|
||||
newScope.add(scope);
|
||||
}
|
||||
}
|
||||
return newScope;
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,25 @@
|
||||
* multiple simultaneous requests for sessions with different scope are handled
|
||||
* in a correct way.
|
||||
*/
|
||||
export type SessionManager<AuthSession> = {
|
||||
getSession(options: {
|
||||
optional: false;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<AuthSession>;
|
||||
export type SessionManager<T> = {
|
||||
getSession(options: { optional: false; scopes?: Set<string> }): Promise<T>;
|
||||
getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<AuthSession | undefined>;
|
||||
}): Promise<T | undefined>;
|
||||
|
||||
removeSession(): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function called to determine the scopes of a session.
|
||||
*/
|
||||
export type SessionScopesFunc<T> = (session: T) => 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.
|
||||
*/
|
||||
export type SessionShouldRefreshFunc<T> = (session: T) => boolean;
|
||||
|
||||
Reference in New Issue
Block a user