Skip to content

Instantly share code, notes, and snippets.

@kalda341
Created February 4, 2020 01:52
Show Gist options
  • Save kalda341/36ae3f49c844d5059651ba888679662d to your computer and use it in GitHub Desktop.
Save kalda341/36ae3f49c844d5059651ba888679662d to your computer and use it in GitHub Desktop.
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