packages/core: added AuthSessionStore

This commit is contained in:
Patrik Oldsberg
2020-05-25 16:30:09 +02:00
parent 98ebcf9c54
commit 1fb3ac0695
2 changed files with 239 additions and 0 deletions
@@ -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);
});
});
@@ -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));
}
}
}