Initial commit

This commit is contained in:
2019-01-15 13:21:24 +01:00
commit dc50642ed9
49 changed files with 10493 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
import auth0 from 'auth0-js';
import {
storeStateAndNonce,
clearStateAndNonce,
getStateAndNonce,
storeAuth,
getIdToken,
getUserInfo,
clear,
getExpiresAt,
} from './storage';
export default class AuthenticationClient {
/**
* Instantiates an authentication client.
* You should only need one per app and authentication method.
* @param {Object} auth0Config An auth0 configuration object, as per their API.
*/
constructor(auth0Config) {
this.config = auth0Config;
this.webAuth = new auth0.WebAuth(auth0Config);
}
logout() {
clear();
}
getUserInfo() {
return getUserInfo();
}
isAuthenticated() {
return !!getIdToken();
}
isExpired() {
const expiresAt = getExpiresAt();
return new Date().getTime() > expiresAt && this.isAuthenticated();
}
/**
* Triggers a login by redirecting to auth0.
*/
triggerLogin(state) {
// clear the state and nonce when triggering a login - otherwise hash parsing wont work.
clearStateAndNonce();
this.webAuth.authorize({ state: JSON.stringify(state) });
}
triggerSilentLogin(state) {
if (!state) {
throw new Error('You must specify a state.');
}
const nonce = new Date().getTime().toString();
// before we trigger the silent login - store the state and nonce in localstorage.
storeStateAndNonce(state, nonce);
let url = this.webAuth.client.buildAuthorizeUrl({
...this.config,
nonce,
state: JSON.stringify(state),
});
url += '&prompt=none';
window.location.href = url;
}
storeSession(authResult, resolve, reject) {
this.webAuth.client.userInfo(authResult.accessToken, (err, user) => {
if (err) {
// if any error happens when fetching user info - nuke all session info
this.logout();
return reject('Authentication failed');
}
const expiresAt = JSON.stringify(authResult.expiresIn * 1000 + new Date().getTime());
storeAuth({ ...authResult, user, expiresAt });
return resolve(JSON.parse(authResult.state));
});
}
handleAuthentication() {
return new Promise((resolve, reject) => {
// retrieve stored state and nonce from localstorage
const { state, nonce } = getStateAndNonce();
// however, if there is no state and nonce stored - do not provide any param to parseHash.
// Otherwise, the non-silent logins will fail due to invalid state.
const parseHashParam = state && nonce ? { state, nonce } : undefined;
this.webAuth.parseHash(parseHashParam, (err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
// If we fail to either set the session or store user info - reject and clear the session.
try {
return this.storeSession(authResult, resolve, reject);
} catch (error) {
return reject(error || 'Authentication failed');
}
} else {
return reject(err || 'Authentication failed');
}
});
});
}
idToken() {
return getIdToken();
}
}
+152
View File
@@ -0,0 +1,152 @@
import AuthClient, { __RewireAPI__ as rewire } from '.'; // eslint-disable-line
import { __RewireAPI__ as storageRewire } from './storage'; // eslint-disable-line
describe('Auth module', () => {
const STORAGE = {
ACCESS: storageRewire.__get__('STORAGE_ACCESS'),
ID: storageRewire.__get__('STORAGE_ID'),
EXPIRES: storageRewire.__get__('STORAGE_EXPIRES'),
USER: storageRewire.__get__('STORAGE_USER'),
STATE: storageRewire.__get__('STORAGE_STATE'),
NONCE: storageRewire.__get__('STORAGE_NONCE'),
};
let authZero;
let TEST_DATE;
let webAuth;
// populates the storage with some mock data to mimic a login
const populateStorage = () => {
localStorage.setItem(STORAGE.ACCESS, 'foo');
localStorage.setItem(STORAGE.ID, 'foo');
localStorage.setItem(STORAGE.EXPIRES, 'foo');
localStorage.setItem(STORAGE.USER, '{"foo": "bar"}');
localStorage.setItem(STORAGE.STATE, 'foo');
localStorage.setItem(STORAGE.NONCE, 'foo');
};
beforeEach(() => {
// freeze time for this test suite - we're going back to the 90s!
TEST_DATE = new Date('1999');
global.Date = jest.fn(() => TEST_DATE);
// mop up current localStorage before each run - it will not clean itself
localStorage.clear();
authZero = {
authorize: jest.fn(),
parseHash: jest.fn(),
client: {
buildAuthorizeUrl: jest.fn(() => 'https://example.com'),
userInfo: jest.fn(),
},
};
rewire.__set__('auth0', { WebAuth: () => authZero });
webAuth = new AuthClient();
});
it('stores user info on successful login', (done) => {
const state = { returnUrl: '/foo' };
const authResult = {
accessToken: 'foo',
idToken: 'bar',
expiresIn: 86400,
state: JSON.stringify(state),
};
const userInfo = { foo: 'bar' };
authZero.parseHash = jest.fn((options, cb) => {
cb(null, authResult);
});
authZero.client.userInfo = jest.fn((token, cb) => {
cb(null, userInfo);
});
return webAuth.handleAuthentication()
.then((result) => {
const expiresAt = JSON.stringify(authResult.expiresIn * 1000 + TEST_DATE.getTime());
expect(authZero.parseHash).toBeCalled();
expect(localStorage.getItem(STORAGE.ACCESS)).toEqual(authResult.accessToken);
expect(localStorage.getItem(STORAGE.ID)).toEqual(authResult.idToken);
expect(localStorage.getItem(STORAGE.EXPIRES)).toEqual(expiresAt);
expect(localStorage.getItem(STORAGE.USER)).toEqual(JSON.stringify(userInfo));
// verify that the state we sent in is what we get back
expect(result).toEqual(state);
done();
});
});
it('clears state and nonce from storage on normal login attempt', () => {
populateStorage();
webAuth.triggerLogin({ returnUrl: '/' });
expect(localStorage.getItem(STORAGE.NONCE)).toBeFalsy();
expect(localStorage.getItem(STORAGE.STATE)).toBeFalsy();
});
it('sets correct localstorage content on silent login attempt', () => {
const state = { returnUrl: '/some-path/', foo: 'bar', ham: 34 };
const serializedState = JSON.stringify(state);
webAuth.triggerSilentLogin(state);
expect(localStorage.getItem(STORAGE.NONCE)).toEqual(TEST_DATE.getTime().toString());
expect(localStorage.getItem(STORAGE.STATE)).toEqual(serializedState);
});
it('rejects failed logins', (done) => {
const authResult = {
error: 'failed to authenticate',
};
authZero.parseHash = jest.fn((options, cb) => {
cb(null, authResult);
});
return webAuth.handleAuthentication()
.catch(() => {
expect(authZero.parseHash).toBeCalled();
expect(localStorage.length).toBe(0);
done();
});
});
it('rejects logins followed by failed user fetches', (done) => {
const authResult = {
accessToken: 'foo',
idToken: 'bar',
expiresIn: 86400,
};
authZero.parseHash = jest.fn((options, cb) => {
cb(null, authResult);
});
authZero.client.userInfo = jest.fn((token, cb) => {
cb('Credentials invalid');
});
return webAuth.handleAuthentication()
.catch(() => {
expect(authZero.parseHash).toBeCalled();
expect(authZero.client.userInfo).toBeCalled();
expect(localStorage.length).toBe(0);
done();
});
});
it('clears session on logout', () => {
populateStorage();
webAuth.logout();
expect(localStorage.length).toBe(0);
});
it('indicates authenticated and expired users based on expiry time', () => {
populateStorage();
localStorage.setItem(STORAGE.EXPIRES, global.Date().getTime() - 1000);
expect(webAuth.isAuthenticated()).toBe(true);
expect(webAuth.isExpired()).toBe(true);
localStorage.setItem(STORAGE.EXPIRES, global.Date().getTime());
expect(webAuth.isAuthenticated()).toBe(true);
expect(webAuth.isExpired()).toBe(false);
localStorage.setItem(STORAGE.EXPIRES, global.Date().getTime() + 1000);
expect(webAuth.isAuthenticated()).toBe(true);
expect(webAuth.isExpired()).toBe(false);
});
});
+46
View File
@@ -0,0 +1,46 @@
const STORAGE_ACCESS = 'access_token';
const STORAGE_ID = 'id_token';
const STORAGE_EXPIRES = 'expires_at';
const STORAGE_USER = 'user_info';
const STORAGE_STATE = 'auth-state';
const STORAGE_NONCE = 'auth-nonce';
/**
* Removes all auth specific items from storage.
*/
export const clear = () => {
localStorage.removeItem(STORAGE_ACCESS);
localStorage.removeItem(STORAGE_ID);
localStorage.removeItem(STORAGE_EXPIRES);
localStorage.removeItem(STORAGE_USER);
localStorage.removeItem(STORAGE_STATE);
localStorage.removeItem(STORAGE_NONCE);
};
export const storeAuth = ({
accessToken, idToken, user, expiresAt,
}) => {
localStorage.setItem(STORAGE_ACCESS, accessToken);
localStorage.setItem(STORAGE_ID, idToken);
localStorage.setItem(STORAGE_EXPIRES, expiresAt);
localStorage.setItem(STORAGE_USER, JSON.stringify(user));
};
export const storeStateAndNonce = (state, nonce) => {
localStorage.setItem(STORAGE_STATE, JSON.stringify(state));
localStorage.setItem(STORAGE_NONCE, nonce);
};
export const getStateAndNonce = () => ({
state: localStorage.getItem(STORAGE_STATE),
nonce: localStorage.getItem(STORAGE_NONCE),
});
export const clearStateAndNonce = () => {
localStorage.removeItem(STORAGE_STATE);
localStorage.removeItem(STORAGE_NONCE);
};
export const getUserInfo = () => JSON.parse(localStorage.getItem(STORAGE_USER));
export const getIdToken = () => localStorage.getItem(STORAGE_ID);
export const getExpiresAt = () => JSON.parse(localStorage.getItem(STORAGE_EXPIRES));
+18
View File
@@ -0,0 +1,18 @@
import AuthClient from './auth-client';
const getRedirectUri = () => {
return location ? `${location.origin}/authorize/` : "http://localhost:3000/authorize/";
};
const auth0Config = {
domain: "unbound.eu.auth0.com",
clientID: "orQfnvCPUR5C3mJkKoiWLQHOVQsBn60e",
redirectUri: getRedirectUri(),
audience: "https://unbound.eu.auth0.com/userinfo",
responseType: "token id_token",
scope: "openid profile"
};
const webAuth = new AuthClient(auth0Config);
export default webAuth;
@@ -0,0 +1,15 @@
import {
findEvents,
} from '../index';
const verifyResponse = (response) => {
expect(response.errors).toBe(undefined);
};
const verifyError = (response) => {
expect(response.errors.length).toBeGreaterThan(0);
};
describe('GQL Queries', () => {
test('findEvents', () => findEvents().then(verifyResponse));
});
+11
View File
@@ -0,0 +1,11 @@
export {
findEvents
} from './queries';
export {
ignoreBand,
ignoreDanceHall,
ignoreCity,
ignoreMunicipality,
ignoreState
} from './mutations';
+15
View File
@@ -0,0 +1,15 @@
module.exports = {
includeCredentials: (tokenFn) => {
return ({options}, next) => {
if (!options.headers) {
options.headers = {}; // Create the headers object if needed.
}
const token = tokenFn();
if (token) {
options.headers['Authorization'] = 'Bearer ' + tokenFn();
}
options.credentials = 'same-origin'; // eslint-disable-line
next();
}
},
};
+27
View File
@@ -0,0 +1,27 @@
module.exports = {
ignoreBandMutation: `
mutation IgnoreBand($name: String!) {
ignore: IgnoreBand(name: $name)
}
`,
ignoreDanceHallMutation: `
mutation IgnoreDanceHall($name: String!) {
ignore: IgnoreDanceHall(name: $name)
}
`,
ignoreCityMutation: `
mutation IgnoreCity($name: String!) {
ignore: IgnoreCity(name: $name)
}
`,
ignoreMunicipalityMutation: `
mutation IgnoreMunicipality($name: String!) {
ignore: IgnoreMunicipality(name: $name)
}
`,
ignoreStateMutation: `
mutation IgnoreState($name: String!) {
ignore: IgnoreState(name: $name)
}
`
};
+27
View File
@@ -0,0 +1,27 @@
import { createQuery } from './utils';
import {
ignoreBandMutation,
ignoreDanceHallMutation,
ignoreCityMutation,
ignoreMunicipalityMutation,
ignoreStateMutation,
} from './mutationStrings';
import webAuth from '../auth';
/* eslint-disable max-len */
export const ignoreBand = variables => {
return createQuery(webAuth.idToken, ignoreBandMutation, variables)
};
export const ignoreDanceHall = variables => {
return createQuery(webAuth.idToken, ignoreDanceHallMutation, variables)
};
export const ignoreCity = variables => {
return createQuery(webAuth.idToken, ignoreCityMutation, variables)
};
export const ignoreMunicipality = variables => {
return createQuery(webAuth.idToken, ignoreMunicipalityMutation, variables)
};
export const ignoreState = variables => {
return createQuery(webAuth.idToken, ignoreStateMutation, variables)
};
/* eslint-enable max-len */
+10
View File
@@ -0,0 +1,10 @@
import { createQuery } from './utils';
import {
eventQuery,
} from './queryStrings';
import webAuth from '../auth';
/* eslint-disable max-len */
export const findEvents = () => createQuery(webAuth.idToken, eventQuery);
/* eslint-enable max-len */
+18
View File
@@ -0,0 +1,18 @@
export const eventQuery = `
{
events: Events {
date
time
band {
name
}
danceHall {
name
city
municipality
state
}
extraInfo
}
}
`;
+13
View File
@@ -0,0 +1,13 @@
const { createApolloFetch } = require('apollo-fetch');
const { includeCredentials } = require('./middleware');
const defaultGraphUri = '/graph';
export const createQuery = (tokenFn, query, variables) => { // eslint-disable-line
const apollo = createApolloFetch({ uri: defaultGraphUri });
apollo.use(includeCredentials(tokenFn));
// apollo.useAfter(trackErrors);
return apollo({ query, variables });
};