react-oauth2-code-pkce
Version:
Provider agnostic react package for OAuth2 Authorization Code flow with PKCE
266 lines (265 loc) • 13.4 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthProvider = exports.AuthContext = void 0;
const react_1 = __importStar(require("react"));
const Hooks_1 = __importDefault(require("./Hooks"));
const authConfig_1 = require("./authConfig");
const authentication_1 = require("./authentication");
const decodeJWT_1 = require("./decodeJWT");
const errors_1 = require("./errors");
const timeUtils_1 = require("./timeUtils");
exports.AuthContext = (0, react_1.createContext)({
token: '',
login: () => null,
logIn: () => null,
logOut: () => null,
error: null,
loginInProgress: false,
});
const AuthProvider = ({ authConfig, children }) => {
const config = (0, react_1.useMemo)(() => (0, authConfig_1.createInternalConfig)(authConfig), [authConfig]);
const [refreshToken, setRefreshToken] = (0, Hooks_1.default)(`${config.storageKeyPrefix}refreshToken`, undefined, config.storage);
const [refreshTokenExpire, setRefreshTokenExpire] = (0, Hooks_1.default)(`${config.storageKeyPrefix}refreshTokenExpire`, undefined, config.storage);
const [token, setToken] = (0, Hooks_1.default)(`${config.storageKeyPrefix}token`, '', config.storage);
const [tokenExpire, setTokenExpire] = (0, Hooks_1.default)(`${config.storageKeyPrefix}tokenExpire`, (0, timeUtils_1.epochAtSecondsFromNow)(timeUtils_1.FALLBACK_EXPIRE_TIME), config.storage);
const [idToken, setIdToken] = (0, Hooks_1.default)(`${config.storageKeyPrefix}idToken`, undefined, config.storage);
const [loginInProgress, setLoginInProgress] = (0, Hooks_1.default)(`${config.storageKeyPrefix}loginInProgress`, false, config.storage);
const [refreshInProgress, setRefreshInProgress] = (0, Hooks_1.default)(`${config.storageKeyPrefix}refreshInProgress`, false, config.storage);
const [loginMethod, setLoginMethod] = (0, Hooks_1.default)(`${config.storageKeyPrefix}loginMethod`, 'redirect', config.storage);
const tokenData = (0, react_1.useMemo)(() => {
if (config.decodeToken)
return (0, decodeJWT_1.decodeAccessToken)(token);
}, [token]);
const idTokenData = (0, react_1.useMemo)(() => (0, decodeJWT_1.decodeIdToken)(idToken), [idToken]);
const [error, setError] = (0, react_1.useState)(null);
function clearStorage() {
setRefreshToken(undefined);
setToken('');
setTokenExpire((0, timeUtils_1.epochAtSecondsFromNow)(timeUtils_1.FALLBACK_EXPIRE_TIME));
setRefreshTokenExpire(undefined);
setIdToken(undefined);
setLoginInProgress(false);
}
function logOut(state, logoutHint, additionalParameters) {
clearStorage();
setError(null);
if ((config === null || config === void 0 ? void 0 : config.logoutEndpoint) && token)
(0, authentication_1.redirectToLogout)(config, token, refreshToken, idToken, state, logoutHint, additionalParameters);
}
function logIn(state, additionalParameters, method = 'redirect') {
clearStorage();
setLoginInProgress(true);
setLoginMethod(method);
// TODO: Raise error on wrong state type in v2
let typeSafePassedState = state;
if (state && typeof state !== 'string') {
const jsonState = JSON.stringify(state);
console.warn(`Passed login state must be of type 'string'. Received '${jsonState}'. Ignoring value. In a future version, an error will be thrown here.`);
typeSafePassedState = undefined;
}
(0, authentication_1.redirectToLogin)(config, typeSafePassedState, additionalParameters, method).catch((error) => {
console.error(error);
setError(error.message);
setLoginInProgress(false);
});
}
function handleTokenResponse(response) {
var _a, _b, _c;
setToken(response.access_token);
if (response.id_token) {
setIdToken(response.id_token);
}
let tokenExp = timeUtils_1.FALLBACK_EXPIRE_TIME;
// Decode IdToken, so we can use "exp" from that as fallback if expire not returned in the response
try {
if (response.id_token) {
const decodedToken = (0, decodeJWT_1.decodeJWT)(response.id_token);
tokenExp = Math.round(Number(decodedToken.exp) - Date.now() / 1000); // number of seconds from now
}
}
catch (e) {
console.warn(`Failed to decode idToken: ${e.message}`);
}
const tokenExpiresIn = (_b = (_a = config.tokenExpiresIn) !== null && _a !== void 0 ? _a : response.expires_in) !== null && _b !== void 0 ? _b : tokenExp;
setTokenExpire((0, timeUtils_1.epochAtSecondsFromNow)(tokenExpiresIn));
const refreshTokenExpiresIn = (_c = config.refreshTokenExpiresIn) !== null && _c !== void 0 ? _c : (0, timeUtils_1.getRefreshExpiresIn)(tokenExpiresIn, response);
if (response.refresh_token) {
setRefreshToken(response.refresh_token);
if (!refreshTokenExpire || config.refreshTokenExpiryStrategy !== 'absolute') {
setRefreshTokenExpire((0, timeUtils_1.epochAtSecondsFromNow)(refreshTokenExpiresIn));
}
}
setError(null);
}
function handleExpiredRefreshToken(initial = false) {
if (config.autoLogin && initial)
return logIn(undefined, undefined, config.loginMethod);
// TODO: Breaking change - remove automatic login during ongoing session
if (!config.onRefreshTokenExpire)
return logIn(undefined, undefined, config.loginMethod);
config.onRefreshTokenExpire({
login: logIn,
logIn,
});
}
function refreshAccessToken(initial = false) {
if (!token)
return;
// The token has not expired. Do nothing
if (!(0, timeUtils_1.epochTimeIsPast)(tokenExpire))
return;
// Other instance (tab) is currently refreshing. This instance skip the refresh if not initial
if (refreshInProgress && !initial)
return;
// If no refreshToken, act as if the refreshToken expired (session expired)
if (!refreshToken)
return handleExpiredRefreshToken(initial);
// The refreshToken has expired
if (refreshTokenExpire && (0, timeUtils_1.epochTimeIsPast)(refreshTokenExpire))
return handleExpiredRefreshToken(initial);
// The access_token has expired, and we have a non-expired refresh_token. Use it to refresh access_token.
if (refreshToken) {
setRefreshInProgress(true);
(0, authentication_1.fetchWithRefreshToken)({ config, refreshToken })
.then((result) => handleTokenResponse(result))
.catch((error) => {
if (error instanceof errors_1.FetchError) {
// If the fetch failed with status 400, assume expired refresh token
if (error.status === 400) {
handleExpiredRefreshToken(initial);
return;
}
// Unknown error. Set error, and log in if first page load
console.error(error);
setError(error.message);
if (initial)
logIn(undefined, undefined, config.loginMethod);
}
// Unknown error. Set error, and log in if first page load
else if (error instanceof Error) {
console.error(error);
setError(error.message);
if (initial)
logIn(undefined, undefined, config.loginMethod);
}
})
.finally(() => {
setRefreshInProgress(false);
});
return;
}
console.warn('Failed to refresh access_token. Most likely there is no refresh_token, or the authentication server did not reply with an explicit expire time, and the default expire times are longer than the actual tokens expire time');
}
// Register the 'check for soon expiring access token' interval (every ~10 seconds).
(0, react_1.useEffect)(() => {
// The randomStagger is used to avoid multiple tabs logging in at the exact same time.
const randomStagger = 10000 * Math.random();
const interval = setInterval(() => refreshAccessToken(), 5000 + randomStagger);
return () => clearInterval(interval);
}, [token, refreshToken, refreshTokenExpire, tokenExpire, refreshInProgress]); // Replace the interval with a new when values used inside refreshAccessToken changes
// This ref is used to make sure the 'fetchTokens' call is only made once.
// Multiple calls with the same code will, and should, return an error from the API
// See: https://beta.reactjs.org/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
const didFetchTokens = (0, react_1.useRef)(false);
// Runs once on page load
(0, react_1.useEffect)(() => {
// The client has been redirected back from the auth endpoint with an auth code
if (loginInProgress) {
const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.get('code')) {
// This should not happen. There should be a 'code' parameter in the url by now...
const error_description = urlParams.get('error_description') ||
'Bad authorization state. Refreshing the page and log in again might solve the issue.';
console.error(`${error_description}\nExpected to find a '?code=' parameter in the URL by now. Did the authentication get aborted or interrupted?`);
setError(error_description);
clearStorage();
return;
}
// Make sure we only try to use the auth code once
if (!didFetchTokens.current) {
didFetchTokens.current = true;
try {
(0, authentication_1.validateState)(urlParams, config.storage);
}
catch (e) {
console.error(e);
setError(e.message);
}
// Request tokens from auth server with the auth code
(0, authentication_1.fetchTokens)(config)
.then((tokens) => {
handleTokenResponse(tokens);
// Call any postLogin function in authConfig
if (config === null || config === void 0 ? void 0 : config.postLogin)
config.postLogin();
if (loginMethod === 'popup')
window.close();
})
.catch((error) => {
console.error(error);
setError(error.message);
})
.finally(() => {
if (config.clearURL) {
// Clear ugly url params
window.history.replaceState(null, '', `${window.location.pathname}${window.location.hash}`);
}
setLoginInProgress(false);
});
}
return;
}
// First page visit
if (!token && config.autoLogin)
return logIn(undefined, undefined, config.loginMethod);
refreshAccessToken(true); // Check if token should be updated
}, []);
return (react_1.default.createElement(exports.AuthContext.Provider, { value: {
token,
tokenData,
idToken,
idTokenData,
login: logIn,
logIn,
logOut,
error,
loginInProgress,
} }, children));
};
exports.AuthProvider = AuthProvider;
;