react-oauth2-code-pkce
Version:
Provider agnostic react package for OAuth2 Authorization Code flow with PKCE
121 lines (120 loc) • 7.35 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchWithRefreshToken = exports.fetchTokens = void 0;
exports.redirectToLogin = redirectToLogin;
exports.redirectToLogout = redirectToLogout;
exports.validateState = validateState;
const httpUtils_1 = require("./httpUtils");
const pkceUtils_1 = require("./pkceUtils");
const popupUtils_1 = require("./popupUtils");
const codeVerifierStorageKey = 'PKCE_code_verifier';
const stateStorageKey = 'ROCP_auth_state';
function redirectToLogin(config_1, customState_1, additionalParameters_1) {
return __awaiter(this, arguments, void 0, function* (config, customState, additionalParameters, method = 'redirect') {
const storage = config.storage === 'session' ? sessionStorage : localStorage;
const navigationMethod = method === 'replace' ? 'replace' : 'assign';
// Create and store a random string in storage, used as the 'code_verifier'
const codeVerifier = (0, pkceUtils_1.generateRandomString)(96);
// Prefix the code verifier key name to prevent multi-application collisions
const codeVerifierStorageKeyName = config.storageKeyPrefix + codeVerifierStorageKey;
storage.setItem(codeVerifierStorageKeyName, codeVerifier);
// Hash and Base64URL encode the code_verifier, used as the 'code_challenge'
return (0, pkceUtils_1.generateCodeChallenge)(codeVerifier).then((codeChallenge) => {
// Set query parameters and redirect user to OAuth2 authentication endpoint
const params = new URLSearchParams(Object.assign(Object.assign({ response_type: 'code', client_id: config.clientId, redirect_uri: config.redirectUri, code_challenge: codeChallenge, code_challenge_method: 'S256' }, config.extraAuthParameters), additionalParameters));
if (config.scope !== undefined && !params.has('scope')) {
params.append('scope', config.scope);
}
storage.removeItem(stateStorageKey);
const state = customState !== null && customState !== void 0 ? customState : config.state;
if (state) {
storage.setItem(stateStorageKey, state);
params.append('state', state);
}
const loginUrl = `${config.authorizationEndpoint}?${params.toString()}`;
// Call any preLogin function in authConfig
if (config === null || config === void 0 ? void 0 : config.preLogin)
config.preLogin();
if (method === 'popup') {
const { width, height, left, top } = (0, popupUtils_1.calculatePopupPosition)(600, 600);
const handle = window.open(loginUrl, 'loginPopup', `width=${width},height=${height},top=${top},left=${left}`);
if (handle)
return;
console.warn('Popup blocked. Redirecting to login page. Disable popup blocker to use popup login.');
}
window.location[navigationMethod](loginUrl);
});
});
}
// This is called a "type predicate". Which allow us to know which kind of response we got, in a type safe way.
function isTokenResponse(body) {
return body.access_token !== undefined;
}
function postTokenRequest(tokenEndpoint, tokenRequest, credentials) {
return (0, httpUtils_1.postWithXForm)({ url: tokenEndpoint, request: tokenRequest, credentials: credentials }).then((response) => {
return response.json().then((body) => {
if (isTokenResponse(body)) {
return body;
}
throw Error(JSON.stringify(body));
});
});
}
const fetchTokens = (config) => {
const storage = config.storage === 'session' ? sessionStorage : localStorage;
/*
The browser has been redirected from the authentication endpoint with
a 'code' url parameter.
This code will now be exchanged for Access- and Refresh Tokens.
*/
const urlParams = new URLSearchParams(window.location.search);
const authCode = urlParams.get('code');
// Prefix the code verifier key name to prevent multi-application collisions
const codeVerifierStorageKeyName = config.storageKeyPrefix + codeVerifierStorageKey;
const codeVerifier = storage.getItem(codeVerifierStorageKeyName);
if (!authCode) {
throw Error("Parameter 'code' not found in URL. \nHas authentication taken place?");
}
if (!codeVerifier) {
throw Error("Can't get tokens without the CodeVerifier. \nHas authentication taken place?");
}
const tokenRequest = Object.assign(Object.assign({ grant_type: 'authorization_code', code: authCode, client_id: config.clientId, redirect_uri: config.redirectUri, code_verifier: codeVerifier }, config.extraTokenParameters), config.extraAuthParams);
return postTokenRequest(config.tokenEndpoint, tokenRequest, config.tokenRequestCredentials);
};
exports.fetchTokens = fetchTokens;
const fetchWithRefreshToken = (props) => {
const { config, refreshToken } = props;
const refreshRequest = Object.assign({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: config.clientId, redirect_uri: config.redirectUri }, config.extraTokenParameters);
if (config.refreshWithScope)
refreshRequest.scope = config.scope;
return postTokenRequest(config.tokenEndpoint, refreshRequest, config.tokenRequestCredentials);
};
exports.fetchWithRefreshToken = fetchWithRefreshToken;
function redirectToLogout(config, token, refresh_token, idToken, state, logoutHint, additionalParameters) {
var _a;
const params = new URLSearchParams(Object.assign(Object.assign({ token: refresh_token || token, token_type_hint: refresh_token ? 'refresh_token' : 'access_token', client_id: config.clientId, post_logout_redirect_uri: (_a = config.logoutRedirect) !== null && _a !== void 0 ? _a : config.redirectUri, ui_locales: window.navigator.languages.join(' ') }, config.extraLogoutParameters), additionalParameters));
if (idToken)
params.append('id_token_hint', idToken);
if (state)
params.append('state', state);
if (logoutHint)
params.append('logout_hint', logoutHint);
window.location.assign(`${config.logoutEndpoint}?${params.toString()}`);
}
function validateState(urlParams, storageType) {
const storage = storageType === 'session' ? sessionStorage : localStorage;
const receivedState = urlParams.get('state');
const loadedState = storage.getItem(stateStorageKey);
if (receivedState !== loadedState) {
throw new Error('"state" value received from authentication server does no match client request. Possible cross-site request forgery');
}
}
;