This repository has been archived by the owner on Jun 19, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
replace existing redux-implicit-oauth2 implementation
Replace the existing redux-implicit-oauth2 authorisation with a work-alike which uses the same format for the redux state. This is a little bit of a halfway-horse; ideally we'd store the full client-oauth2 token object in the state but a lot of the rest of the app depends on the state having the same form and this commit is intended to be a small surgical change. Since we already hid the redux-implicit-oauth2 actions inside the src/redux/actions/auth.js module, replacing them is relatively straightforward. We add the ability to do prompt-less login via the login action. This is currently unused but is in place for the token timeout handling behaviour. We keep the existing behaviour of keeping the auth token in localStorage so that re-visits to the app can have speedy login. We load it as a by-product of initialising the local state which means we can do away with the existing auth middleware. Since we no-longer use the redux-implicit-oauth2 implementation, we can remove the special-case route in AppRoutes for it.
- Loading branch information
Showing
10 changed files
with
277 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,45 @@ | ||
/** | ||
* Redux actions for authenticating and authorising current user. | ||
* | ||
* These are relatively thin wrappers around redux-implicit-oauth2's actions. They're put here to | ||
* decouple the login/logout logic from having to know the mechanism by which login and logout are | ||
* achieved. | ||
*/ | ||
import { login as implicitLogin, logout as implicitLogout } from 'redux-implicit-oauth2'; | ||
import history from '../../history' | ||
import config from '../../config'; | ||
import { login as authLogin } from '../../auth'; | ||
|
||
/** | ||
* OAuth2 credentials configuration for the IAR frontend application. | ||
*/ | ||
const oauth2Config = { | ||
url: config.oauth2AuthEndpoint, | ||
client: config.oauth2ClientId, | ||
redirect: config.oauth2RedirectUrl, | ||
scope: config.oauth2Scopes, | ||
width: config.oauth2PopupWidth, // Width (in pixels) of login popup window. | ||
height: config.oauth2PopupHeight, // Height (in pixels) of login popup window. | ||
}; | ||
// Action types | ||
export const LOGIN_REQUEST = 'LOGIN_REQUEST'; | ||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; | ||
export const LOGIN_FAILURE = 'LOGIN_FAILURE'; | ||
export const LOGOUT = 'LOGOUT'; | ||
|
||
/** | ||
* Initialise login to application. | ||
* | ||
* Takes an optional options object as passed to auth.login(). Returns a thunk function which | ||
* returns a promise resolved with the result of dispatching a LOGIN_SUCCESS or LOGIN_FAILURE | ||
* action. | ||
* | ||
* Returns a function and requires the redux-thunk middleware. | ||
*/ | ||
export const login = () => implicitLogin(oauth2Config); | ||
export const login = options => dispatch => { | ||
dispatch({ type: LOGIN_REQUEST }); | ||
|
||
return authLogin(options) | ||
.then(token => dispatch({ | ||
type: LOGIN_SUCCESS, | ||
payload: { token }, | ||
})) | ||
.catch(error => dispatch({ | ||
type: LOGIN_FAILURE, | ||
payload: { error }, | ||
})); | ||
} | ||
|
||
/** | ||
* Log out from the application and redirect to "/". | ||
* | ||
* Returns a function and requires the redux-thunk middleware. | ||
*/ | ||
export const logout = () => dispatch => { | ||
dispatch(implicitLogout()); | ||
dispatch({ type: LOGOUT }); | ||
history.push('/'); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
jest.mock('../../auth'); | ||
|
||
import { login, LOGIN_SUCCESS, LOGIN_FAILURE } from './auth'; | ||
import { login as authLogin } from '../../auth'; | ||
|
||
describe('login', () => { | ||
let dispatch; | ||
|
||
beforeEach(() => { | ||
authLogin.mockReset(); | ||
authLogin.mockImplementation(() => Promise.resolve('some-token')); | ||
dispatch = jest.fn(action => action); | ||
}); | ||
|
||
test('passes options to auth.login', () => { | ||
const options = { prompt: true }; | ||
login(options)(dispatch); | ||
expect(authLogin).toHaveBeenCalledTimes(1); | ||
expect(authLogin).toHaveBeenCalledWith(options); | ||
}); | ||
|
||
test('returns promise resolved with LOGIN_SUCCESS action if login succeeds', () => { | ||
const options = { prompt: true }, token = 'new-token'; | ||
authLogin.mockImplementation(() => Promise.resolve(token)); | ||
const authPromise = login(options)(dispatch); | ||
expect(authPromise).toBeInstanceOf(Promise); | ||
expect(login(options)(dispatch)).resolves.toMatchObject({ | ||
type: LOGIN_SUCCESS, payload: { token } | ||
}); | ||
}); | ||
|
||
test('returns promise resolved with LOGIN_FAILURE action if login succeeds', () => { | ||
const options = { prompt: true }, error = 'do-not-like-you'; | ||
authLogin.mockImplementation(() => Promise.reject(error)); | ||
const authPromise = login(options)(dispatch); | ||
expect(authPromise).toBeInstanceOf(Promise); | ||
expect(login(options)(dispatch)).resolves.toMatchObject({ | ||
type: LOGIN_FAILURE, payload: { error } | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE, LOGOUT } from '../actions/auth'; | ||
|
||
export const AUTH_TOKEN_STORAGE_KEY = 'cachedOAuth2TokenData'; | ||
|
||
// Update initial state based upon any token saved in the localStorage | ||
export const retrieveStoredToken = state => { | ||
// no localStorage support in browser? | ||
if(!window.localStorage) { return state; } | ||
|
||
// is there any stored data? | ||
const storedTokenJSON = window.localStorage.getItem(AUTH_TOKEN_STORAGE_KEY); | ||
if(!storedTokenJSON) { return state; } | ||
|
||
// attempt to parse it as JSON | ||
let storedToken; | ||
try { | ||
storedToken = JSON.parse(storedTokenJSON); | ||
} catch(err) { | ||
// we silently swallow the error here to avoid polluting the console but we could report it | ||
// via, for example: | ||
// console.error(err); | ||
return state; | ||
} | ||
|
||
// extract token data | ||
const { token, expiresAt } = storedToken; | ||
|
||
// is the token still alive? | ||
if(!token || !expiresAt || expiresAt < (new Date()).getTime()) { return state; } | ||
|
||
// everything checks out, retrieve it | ||
return { ...state, isLoggedIn: true, token, expiresAt }; | ||
}; | ||
|
||
// Save access token to localStorage | ||
const saveStoredToken = (token, expiresAt) => { | ||
// no localStorage support in browser? | ||
if(!window.localStorage) { return; } | ||
window.localStorage.setItem(AUTH_TOKEN_STORAGE_KEY, JSON.stringify({ token, expiresAt })); | ||
}; | ||
|
||
// Remove any access token cached in local storage. | ||
const clearStoredToken = () => { | ||
// no localStorage support in browser? | ||
if(!window.localStorage) { return; } | ||
window.localStorage.removeItem(AUTH_TOKEN_STORAGE_KEY); | ||
} | ||
|
||
// The initial state is passed through retrieveStoredToken which sets the token, expiresAt and | ||
// isLoggedIn fields if there is a valid token saved in the browser's local storage. | ||
export const initialState = retrieveStoredToken({ | ||
// Current access token for user or null if there is not one. | ||
token: null, | ||
|
||
// If non-null, the last login error. | ||
error: null, | ||
|
||
// If true, the user is currently logging in. | ||
isLoggingIn: false, | ||
|
||
// If true, the user is currently logged in. | ||
isLoggedIn: false, | ||
|
||
// A timestamp (as returned by Date.getTime()) indicating when the token expires. | ||
expiresAt: null, | ||
}); | ||
|
||
export default (state = initialState, action) => { | ||
switch(action.type) { | ||
case LOGIN_REQUEST: | ||
return {...state, isLoggingIn: true }; | ||
case LOGIN_SUCCESS: | ||
{ | ||
// extract the authorisation token from the action's payload | ||
const { token: tokenObject } = action.payload; | ||
const token = tokenObject.accessToken, expiresAt = tokenObject.expires.getTime(); | ||
|
||
// cache the token in the browser's localStorage | ||
saveStoredToken(token, expiresAt); | ||
|
||
return { ...state, isLoggingIn: false, isLoggedIn: true, token, expiresAt }; | ||
} | ||
case LOGIN_FAILURE: | ||
return {...state, isLoggingIn: false, isLoggedIn: false, token: null, expiresAt: null}; | ||
case LOGOUT: | ||
// clear any cached token from localStorage | ||
clearStoredToken(); | ||
return {...state, isLoggedIn: false, token: null, expiresAt: null}; | ||
default: | ||
return state; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// mock window.localStorage | ||
import '../../test/mock-localstorage'; | ||
|
||
import reducer, { AUTH_TOKEN_STORAGE_KEY, retrieveStoredToken } from './auth'; | ||
import { LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE, LOGOUT } from '../actions/auth'; | ||
|
||
describe('LOGIN_REQUEST', () => { | ||
test('sets isLoggingIn', () => { | ||
const state = reducer(undefined, { type: LOGIN_REQUEST }); | ||
expect(state.isLoggingIn).toBe(true); | ||
}); | ||
}); | ||
|
||
describe('LOGIN_SUCCESS', () => { | ||
let action, state; | ||
|
||
beforeEach(() => { | ||
// make sure local storage is clear | ||
window.localStorage.removeItem(AUTH_TOKEN_STORAGE_KEY); | ||
|
||
action = { | ||
type: LOGIN_SUCCESS, payload: { token: { accessToken: 'xxx', expires: new Date() } } | ||
}; | ||
|
||
state = reducer(undefined, action); | ||
}); | ||
|
||
test('sets isLoggedIn', () => { | ||
expect(state.isLoggedIn).toBe(true); | ||
}); | ||
|
||
test('sets token', () => { | ||
expect(state.token).toBe(action.payload.token.accessToken); | ||
}); | ||
|
||
test('sets expiresAt', () => { | ||
expect(state.expiresAt).toBe(action.payload.token.expires.getTime()); | ||
}); | ||
|
||
test('clears isLoggingIn', () => { | ||
expect(state.isLoggingIn).toBe(false); | ||
}); | ||
|
||
test('stores state', () => { | ||
// now initial state should find the token cached | ||
const newInitialState = retrieveStoredToken(reducer(undefined, { })); | ||
expect(newInitialState).toMatchObject({ | ||
token: action.payload.token.accessToken, | ||
expiresAt: action.payload.token.expires.getTime(), | ||
}); | ||
}); | ||
}); | ||
|
||
describe('LOGIN_FAILURE', () => { | ||
let action, state; | ||
|
||
beforeEach(() => { | ||
action = { | ||
type: LOGIN_FAILURE, payload: { error: 'xxx' } | ||
}; | ||
state = reducer(undefined, action); | ||
}); | ||
|
||
test('clears isLoggingIn', () => { | ||
expect(state.isLoggingIn).toBe(false); | ||
}); | ||
|
||
test('clears isLoggedIn', () => { | ||
expect(state.isLoggedIn).toBe(false); | ||
}); | ||
|
||
test('clears token', () => { | ||
expect(state.token).toBe(null); | ||
}); | ||
|
||
test('clears expiresAt', () => { | ||
expect(state.expiresAt).toBe(null); | ||
}); | ||
}); | ||
|
||
describe('LOGOUT', () => { | ||
let action, state; | ||
|
||
beforeEach(() => { | ||
window.localStorage.setItem(AUTH_TOKEN_STORAGE_KEY, 'xxx'); | ||
action = { type: LOGOUT }; | ||
state = reducer(undefined, action); | ||
}); | ||
|
||
test('clears isLoggedIn', () => { | ||
expect(state.isLoggedIn).toBe(false); | ||
}); | ||
|
||
test('clears token', () => { | ||
expect(state.token).toBe(null); | ||
}); | ||
|
||
test('clears expiresAt', () => { | ||
expect(state.expiresAt).toBe(null); | ||
}); | ||
|
||
test('clears stored token', () => { | ||
}); | ||
}); | ||
|
||
describe('retrieveStoredToken', () => { | ||
test('returns its input if stored state invalid', () => { | ||
window.localStorage.setItem(AUTH_TOKEN_STORAGE_KEY, 'not-some-json'); | ||
const initialState = reducer(undefined, {}); | ||
expect(retrieveStoredToken(initialState)).toBe(initialState); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters