Created
February 4, 2020 01:52
-
-
Save kalda341/36ae3f49c844d5059651ba888679662d to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import React, { useEffect } from 'react'; | |
import PropTypes from 'prop-types'; | |
import { | |
compose, | |
unless, | |
prop, | |
isNil, | |
merge, | |
propOr, | |
tryCatch, | |
map, | |
always | |
} from 'ramda'; | |
import { useLoadingReducer, useTask } from 'hooks'; | |
import { timeout, UserReadableError } from 'func-utils'; | |
export const AuthContext = React.createContext(); | |
// From https://stackoverflow.com/questions/38552003/how-to-decode-jwt-token-in-javascript-without-using-a-library | |
const parseJwt = token => { | |
const base64Url = token.split('.')[1]; | |
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | |
const jsonPayload = decodeURIComponent( | |
atob(base64) | |
.split('') | |
.map(function(c) { | |
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); | |
}) | |
.join('') | |
); | |
return JSON.parse(jsonPayload); | |
}; | |
// Returns time until token expiry in seconds | |
const getTokenEpiry = compose( | |
exp => exp - new Date() / 1000, | |
propOr(0, 'exp'), | |
tryCatch(parseJwt, always({})) | |
); | |
// Allow the user's clock to be 20 minutes off | |
const EXPIRY_OFFSET = 1200; | |
export function AuthProvider({ | |
loadToken, | |
saveToken, | |
requestToken, | |
refreshToken, | |
onSetToken, | |
children | |
}) { | |
const [loadingState, dispatchLoading] = useLoadingReducer({ | |
isLoading: true | |
}); | |
const loadAuthenticationTask = useTask(function*() { | |
dispatchLoading({ | |
type: 'LOADING', | |
message: 'Loading auth token...' | |
}); | |
let loadedToken = yield loadToken(); | |
if (isNil(loadedToken)) { | |
setTokenTask.perform(null); | |
} else if (getTokenEpiry(loadedToken.access) - EXPIRY_OFFSET > 0) { | |
// Use the token we have if it hasn't expired | |
setTokenTask.perform(loadedToken); | |
} else { | |
// We need to update the token. We might as well try a refresh even | |
// if the refresh token is expired. | |
refreshTokenTask.perform(loadedToken); | |
} | |
}, 'RESTARTABLE'); | |
useEffect(() => { | |
loadAuthenticationTask.perform(); | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, []); | |
const setTokenTask = useTask(function*(token) { | |
yield saveToken(token); | |
if (!isNil(token)) { | |
dispatchLoading({ type: 'SUCCESS', data: token }); | |
// Schedule token refresh any time we update the token | |
// Don't yield on this! | |
refreshTokenTask.perform(token); | |
// Notify token has changed | |
onSetToken && onSetToken(map(parseJwt, token)); | |
} else { | |
dispatchLoading({ | |
type: 'ERROR', | |
message: 'Not authenticated' | |
}); | |
// We don't want a refresh to replace the token! | |
refreshTokenTask.cancelAll(); | |
} | |
}, 'RESTARTABLE'); | |
const refreshTokenTask = useTask(function*(token) { | |
const timeUntilRefresh = | |
(getTokenEpiry(token.access) - EXPIRY_OFFSET) * 1000; | |
// Wait until we need to refresh, if we don't need to immediately | |
if (timeUntilRefresh > 0) { | |
yield timeout(timeUntilRefresh); | |
} | |
try { | |
const newToken = merge(token, yield refreshToken(token)); | |
// Note - no yield as it will call refreshTokenTask | |
setTokenTask.perform(newToken); | |
} catch (error) { | |
console.log('Failed to refresh access token'); | |
// eslint-disable-next-line no-console | |
console.log(error); | |
setTokenTask.perform(null); | |
} | |
}, 'RESTARTABLE'); | |
const authenticationTask = useTask(function*(email, password) { | |
try { | |
const token = yield requestToken({ username: email, password }); | |
yield setTokenTask.perform(token); | |
} catch (error) { | |
setTokenTask.perform(null); | |
if (error.status === 401) { | |
throw new UserReadableError('Invalid email or password'); | |
} else { | |
throw new UserReadableError('An unknown error occured'); | |
} | |
} | |
}, 'RESTARTABLE'); | |
const authHeaders = compose( | |
unless(isNil, t => ({ Authorization: `Bearer ${t}` })), | |
prop('access') | |
)(loadingState.data); | |
const tokenData = compose( | |
unless(isNil, parseJwt), | |
prop('access') | |
)(loadingState.data); | |
return ( | |
<AuthContext.Provider | |
value={{ | |
authenticate: (email, password) => | |
authenticationTask.perform(email, password), | |
unauthenticate: () => setTokenTask.perform(null), | |
isAuthenticated: loadingState.isSuccess, | |
isLoading: loadingState.isLoading, | |
authHeaders, | |
tokenData | |
}} | |
> | |
{children} | |
</AuthContext.Provider> | |
); | |
} | |
AuthProvider.propTypes = { | |
loadToken: PropTypes.func.isRequired, | |
saveToken: PropTypes.func.isRequired, | |
requestToken: PropTypes.func.isRequired, | |
refreshToken: PropTypes.func.isRequired, | |
onSetToken: PropTypes.func, | |
children: PropTypes.any.isRequired | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment