packages/core: added AuthSessionStore
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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 { AuthSessionStore } from './AuthSessionStore';
|
||||
import { SessionManager } from './types';
|
||||
|
||||
const defaultOptions = {
|
||||
storageKey: 'my-key',
|
||||
sessionScopes: (session: string) => new Set(session.split(' ')),
|
||||
};
|
||||
|
||||
class LocalStorage {
|
||||
private store: Record<string, string> = {};
|
||||
|
||||
getItem(key: string) {
|
||||
return this.store[key] || null;
|
||||
}
|
||||
setItem(key: string, value: string) {
|
||||
this.store[key] = value.toString();
|
||||
}
|
||||
removeItem(key: string) {
|
||||
delete this.store[key];
|
||||
}
|
||||
}
|
||||
|
||||
class MockManager implements SessionManager<string> {
|
||||
getSession = jest.fn();
|
||||
removeSession = jest.fn();
|
||||
}
|
||||
|
||||
describe('GheAuth AuthSessionStore', () => {
|
||||
beforeEach(() => {
|
||||
delete (window as any).localStorage;
|
||||
(window as any).localStorage = new LocalStorage();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should load session', async () => {
|
||||
localStorage.setItem('my-key', '"a b c"');
|
||||
|
||||
const manager = new MockManager();
|
||||
const store = new AuthSessionStore({ manager, ...defaultOptions });
|
||||
|
||||
await expect(store.getSession({})).resolves.toBe('a b c');
|
||||
expect(manager.getSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not use session without enough scope', async () => {
|
||||
localStorage.setItem('my-key', '"a b c"');
|
||||
|
||||
const manager = new MockManager();
|
||||
manager.getSession.mockResolvedValue('a b c d');
|
||||
const store = new AuthSessionStore({ manager, ...defaultOptions });
|
||||
|
||||
await expect(store.getSession({ scopes: new Set(['d']) })).resolves.toBe(
|
||||
'a b c d',
|
||||
);
|
||||
expect(manager.getSession).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not use expired session', async () => {
|
||||
localStorage.setItem('my-key', '"a b c"');
|
||||
|
||||
const manager = new MockManager();
|
||||
manager.getSession.mockResolvedValue('123');
|
||||
const store = new AuthSessionStore({
|
||||
manager,
|
||||
...defaultOptions,
|
||||
sessionShouldRefresh: () => true,
|
||||
});
|
||||
|
||||
await expect(store.getSession({})).resolves.toBe('123');
|
||||
expect(manager.getSession).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not load missing session', async () => {
|
||||
const manager = new MockManager();
|
||||
manager.getSession.mockResolvedValue('123');
|
||||
const store = new AuthSessionStore({ manager, ...defaultOptions });
|
||||
|
||||
await expect(store.getSession({})).resolves.toBe('123');
|
||||
expect(manager.getSession).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(localStorage.getItem('my-key')).toBe('"123"');
|
||||
});
|
||||
|
||||
it('should ignore bad session values', async () => {
|
||||
localStorage.setItem('my-key', 'derp');
|
||||
|
||||
const manager = new MockManager();
|
||||
manager.getSession.mockResolvedValue('123');
|
||||
const store = new AuthSessionStore({ manager, ...defaultOptions });
|
||||
|
||||
await expect(store.getSession({})).resolves.toBe('123');
|
||||
expect(manager.getSession).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should clear session', () => {
|
||||
localStorage.setItem('my-key', '"a b c"');
|
||||
|
||||
const manager = new MockManager();
|
||||
const store = new AuthSessionStore({ manager, ...defaultOptions });
|
||||
store.removeSession();
|
||||
|
||||
expect(localStorage.getItem('my-key')).toBe(null);
|
||||
});
|
||||
});
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 {
|
||||
SessionManager,
|
||||
SessionScopesFunc,
|
||||
SessionShouldRefreshFunc,
|
||||
} from './types';
|
||||
import { SessionScopeHelper } from './common';
|
||||
|
||||
type Options<T> = {
|
||||
/** The connector used for acting on the auth session */
|
||||
manager: SessionManager<T>;
|
||||
/** Storage key to use to store sessions */
|
||||
storageKey: string;
|
||||
/** Used to get the scope of the session */
|
||||
sessionScopes: SessionScopesFunc<T>;
|
||||
/** Used to check if the session needs to be refreshed, defaults to never refresh */
|
||||
sessionShouldRefresh?: SessionShouldRefreshFunc<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
* AuthSessionStore decorates another SessionManager with a functionality
|
||||
* to store the session in local storage.
|
||||
*/
|
||||
export class AuthSessionStore<T> implements SessionManager<T> {
|
||||
private readonly manager: SessionManager<T>;
|
||||
private readonly storageKey: string;
|
||||
private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc<T>;
|
||||
private readonly helper: SessionScopeHelper<T>;
|
||||
|
||||
constructor(options: Options<T>) {
|
||||
const {
|
||||
manager,
|
||||
storageKey,
|
||||
sessionScopes,
|
||||
sessionShouldRefresh = () => false,
|
||||
} = options;
|
||||
|
||||
this.manager = manager;
|
||||
this.storageKey = storageKey;
|
||||
this.sessionShouldRefreshFunc = sessionShouldRefresh;
|
||||
this.helper = new SessionScopeHelper({
|
||||
sessionScopes,
|
||||
defaultScopes: new Set(),
|
||||
});
|
||||
}
|
||||
|
||||
async getSession(options: {
|
||||
optional: false;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T>;
|
||||
async getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T | undefined>;
|
||||
async getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T | undefined> {
|
||||
const { scopes } = options;
|
||||
const session = this.loadSession();
|
||||
|
||||
if (this.helper.sessionExistsAndHasScope(session, scopes)) {
|
||||
const shouldRefresh = this.sessionShouldRefreshFunc(session!);
|
||||
|
||||
if (!shouldRefresh) {
|
||||
return session!;
|
||||
}
|
||||
}
|
||||
|
||||
const newSession = await this.manager.getSession(options);
|
||||
this.saveSession(newSession);
|
||||
return newSession;
|
||||
}
|
||||
|
||||
async removeSession() {
|
||||
localStorage.removeItem(this.storageKey);
|
||||
await this.manager.removeSession();
|
||||
}
|
||||
|
||||
private loadSession(): T | undefined {
|
||||
try {
|
||||
const sessionJson = localStorage.getItem(this.storageKey);
|
||||
if (sessionJson) {
|
||||
const session = JSON.parse(sessionJson);
|
||||
return session;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
localStorage.removeItem(this.storageKey);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private saveSession(session: T | undefined) {
|
||||
if (session === undefined) {
|
||||
localStorage.removeItem(this.storageKey);
|
||||
} else {
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(session));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user