src/utils/oauth.js
import {
OAUTH_CLIENT_ID,
OAUTH_CLIENT_SECRET,
OAUTH_PROXY_HOSTNAME,
OAUTH_REDIRECT_URI,
OAUTH_SCOPE,
ORIGIN,
} from '../globals';
import {
oauthUpdateToken,
refreshingTokenPendingAction,
refreshingTokenErrorAction,
} from '../actions/authentication';
import { saveToken } from './secureStorage';
// https://stackoverflow.com/a/3855394/4028896
const _getParams = query => {
if (!query) {
return {};
}
return (/^[?#]/.test(query) ? query.slice(1) : query)
.split('&')
.reduce((params, param) => {
const [key, value] = param.split('=');
params[key] = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : '';
return params;
}, {});
};
const getParams = query => new Map(Object.entries(_getParams(query)));
const requestAuthToken = `${ORIGIN}/account/authorize/?client_id=${OAUTH_CLIENT_ID}&scope=${OAUTH_SCOPE}&response_type=code&redirect_uri=${OAUTH_REDIRECT_URI}`;
const exchangeCodeForAuthToken = code => {
const url = `${OAUTH_PROXY_HOSTNAME}/account/token/?client_id=${OAUTH_CLIENT_ID}&client_secret=${OAUTH_CLIENT_SECRET}&grant_type=authorization_code&code=${code}&redirect_uri=${OAUTH_REDIRECT_URI}`;
return fetch(url, { method: 'post' });
};
const refreshAccessToken = refreshToken => {
const url = `${OAUTH_PROXY_HOSTNAME}/account/token/?client_id=${OAUTH_CLIENT_ID}&client_secret=${OAUTH_CLIENT_SECRET}&grant_type=refresh_token&refresh_token=${refreshToken}&scope=${OAUTH_SCOPE}`;
return fetch(url, {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
});
};
const getJsonOrThrow = async fetchingPromise => {
const response = await fetchingPromise;
if (!response.ok) {
throw new Error(`${response.status}: ${response.url} ${response.body}`);
}
return response.json();
};
// should be done in the reducer
const getTokens = body => {
return {
accessToken: body.access_token,
refreshToken: body.refresh_token,
expiresIn: body.expires_in,
timeStamp: Date.now(),
};
};
const fetchInitialToken = url => {
return new Promise(async (resolve, reject) => {
const paramString = url.substr(OAUTH_REDIRECT_URI.length);
const params = getParams(paramString);
if (params.has('error')) {
const errorMessage =
params.get('error') +
(params.has('error_description')
? `: ${params.get('error_description')}`
: '');
reject(new Error(errorMessage));
}
if (!params.has('code')) {
reject(new Error('no auth code was provided by the backend'));
}
try {
const code = params.get('code');
const exchangeTokenResponseJson = await getJsonOrThrow(
exchangeCodeForAuthToken(code)
);
const token = getTokens(exchangeTokenResponseJson);
resolve(token);
} catch (error) {
reject(error);
}
});
};
const getCurrentAccessTokenOrRefresh = (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const {
timeStamp,
expiresIn,
accessToken,
refreshToken,
} = getState().authentication;
if (refreshToken == null) reject('no valid refresh token');
// JavaScript UTC is in milliseconds, normally UTC is in seconds
const milliSecondsLeftBeforeRefreshing = 60 * 1000;
const expiresInMS = expiresIn * 1000;
// is the token at least for X seconds valid?
if (
timeStamp + expiresInMS >
Date.now() + milliSecondsLeftBeforeRefreshing
) {
// if yes, return
resolve(accessToken);
} else {
// if no,
dispatch(refreshingTokenPendingAction());
try {
// 1. refresh the access token
const refreshedToken = await getJsonOrThrow(
refreshAccessToken(refreshToken, accessToken, expiresIn)
);
const token = getTokens(refreshedToken);
// 2. update the access token in the redux store (async)
dispatch(oauthUpdateToken(token));
// 3. persist new token (async)
saveToken(token);
// 4. return the new access token
resolve(token.accessToken);
} catch (error) {
dispatch(refreshingTokenErrorAction(error));
reject(error);
}
}
});
};
export {
getParams,
requestAuthToken,
fetchInitialToken,
getCurrentAccessTokenOrRefresh,
};