239 lines
6.1 KiB
JavaScript
239 lines
6.1 KiB
JavaScript
const redirectUri = `${window.location.origin}/`;
|
|
|
|
// GENERAL HELPER FUNCTIONS
|
|
|
|
/**
|
|
* Makes a POST request and parse the response as JSON
|
|
*
|
|
* @param {String} url - The resource
|
|
* @param {Object} params - Configuration options
|
|
* @returns {Object}
|
|
*/
|
|
|
|
const sendPostRequest = async (url, params) => {
|
|
const body = Object.keys(params)
|
|
.map(key => `${key}=${params[key]}`)
|
|
.join("&");
|
|
const options = {
|
|
method: "post",
|
|
headers: {
|
|
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
|
|
},
|
|
body
|
|
};
|
|
try {
|
|
const response = await fetch(url, options);
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (err) {
|
|
console.error("Request failed", err);
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Parse a query string into an object
|
|
*
|
|
* @param {String} searchQuery - The search query params
|
|
* @returns {Object}
|
|
*/
|
|
|
|
const parseQueryString = searchQuery => {
|
|
if (searchQuery === "") {
|
|
return {};
|
|
}
|
|
const segments = searchQuery.split("&").map(s => s.split("="));
|
|
const queryString = segments.reduce(
|
|
(obj, el) => ({ ...obj, [el[0]]: el[1] }),
|
|
{}
|
|
);
|
|
return queryString;
|
|
};
|
|
|
|
/**
|
|
* Get OAuth configuration from OpenID Discovery endpoint
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
|
|
const getTokenConfiguration = async endpoint => {
|
|
const options = {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-type": "application/json"
|
|
}
|
|
};
|
|
try {
|
|
const response = await fetch(endpoint, options);
|
|
const config = await response.json();
|
|
return config;
|
|
} catch (err) {
|
|
console.error("Request failed", err);
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
// PKCE HELPER FUNCTIONS
|
|
|
|
/**
|
|
* Generates a secure random string using the browser crypto functions
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
|
|
const generateRandomString = () => {
|
|
const array = new Uint32Array(28);
|
|
window.crypto.getRandomValues(array);
|
|
return Array.from(array, dec => `0${dec.toString(16)}`.substr(-2)).join("");
|
|
};
|
|
|
|
/**
|
|
* Calculate the SHA256 hash of the input text
|
|
*
|
|
* @returns {Promise<ArrayBuffer>}
|
|
*/
|
|
|
|
const sha256 = plain => {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(plain);
|
|
return window.crypto.subtle.digest("SHA-256", data);
|
|
};
|
|
|
|
/**
|
|
* Encodes the input string into Base64 format
|
|
*
|
|
* @param {String} str - The string to be converted
|
|
* @returns {Promise<ArrayBuffer>}
|
|
*/
|
|
|
|
const base64urlencode = (
|
|
str // Converts the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
|
|
) =>
|
|
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
|
|
// Then convert the base64 encoded to base64url encoded
|
|
// (replace + with -, replace / with _, trim trailing =)
|
|
btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
|
|
.replace(/\+/g, "-")
|
|
.replace(/\//g, "_")
|
|
.replace(/=+$/, "");
|
|
|
|
/**
|
|
* Return the base64-urlencoded sha256 hash for the PKCE challenge
|
|
*
|
|
* @param {String} v - The randomly generated string
|
|
* @returns {String}
|
|
*/
|
|
|
|
const pkceChallengeFromVerifier = async v => {
|
|
const hashed = await sha256(v);
|
|
return base64urlencode(hashed);
|
|
};
|
|
|
|
// OAUTH REQUEST
|
|
|
|
/**
|
|
* Initiates PKCE Auth Code flow when requested
|
|
*
|
|
* @param {Object} - The necessary params
|
|
* @returns {Void}
|
|
*/
|
|
|
|
const tokenRequest = async ({
|
|
oidcDiscoveryUrl,
|
|
grantType,
|
|
authUrl,
|
|
accessTokenUrl,
|
|
clientId,
|
|
scope
|
|
}) => {
|
|
// Check oauth configuration
|
|
if (oidcDiscoveryUrl !== "") {
|
|
const {
|
|
authorization_endpoint,
|
|
token_endpoint
|
|
} = await getTokenConfiguration(oidcDiscoveryUrl);
|
|
authUrl = authorization_endpoint;
|
|
accessTokenUrl = token_endpoint;
|
|
}
|
|
|
|
// Store oauth information
|
|
localStorage.setItem("token_endpoint", accessTokenUrl);
|
|
localStorage.setItem("client_id", clientId);
|
|
|
|
// Create and store a random state value
|
|
const state = generateRandomString();
|
|
localStorage.setItem("pkce_state", state);
|
|
|
|
// Create and store a new PKCE code_verifier (the plaintext random secret)
|
|
const code_verifier = generateRandomString();
|
|
localStorage.setItem("pkce_code_verifier", code_verifier);
|
|
|
|
// Hash and base64-urlencode the secret to use as the challenge
|
|
const code_challenge = await pkceChallengeFromVerifier(code_verifier);
|
|
|
|
// Build the authorization URL
|
|
const buildUrl = () =>
|
|
`${authUrl + `?response_type=${grantType}`}&client_id=${encodeURIComponent(
|
|
clientId
|
|
)}&state=${encodeURIComponent(state)}&scope=${encodeURIComponent(
|
|
scope
|
|
)}&redirect_uri=${encodeURIComponent(
|
|
redirectUri
|
|
)}&code_challenge=${encodeURIComponent(
|
|
code_challenge
|
|
)}&code_challenge_method=S256`;
|
|
|
|
// Redirect to the authorization server
|
|
window.location = buildUrl();
|
|
};
|
|
|
|
// OAUTH REDIRECT HANDLING
|
|
|
|
/**
|
|
* Handle the redirect back from the authorization server and
|
|
* get an access token from the token endpoint
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
|
|
const oauthRedirect = async () => {
|
|
let tokenResponse = "";
|
|
let q = parseQueryString(window.location.search.substring(1));
|
|
// Check if the server returned an error string
|
|
if (q.error) {
|
|
alert(`Error returned from authorization server: ${q.error}`);
|
|
}
|
|
// If the server returned an authorization code, attempt to exchange it for an access token
|
|
if (q.code) {
|
|
// Verify state matches what we set at the beginning
|
|
if (localStorage.getItem("pkce_state") != q.state) {
|
|
alert("Invalid state");
|
|
} else {
|
|
try {
|
|
// Exchange the authorization code for an access token
|
|
tokenResponse = await sendPostRequest(
|
|
localStorage.getItem("token_endpoint"),
|
|
{
|
|
grant_type: "authorization_code",
|
|
code: q.code,
|
|
client_id: localStorage.getItem("client_id"),
|
|
redirect_uri: redirectUri,
|
|
code_verifier: localStorage.getItem("pkce_code_verifier")
|
|
}
|
|
);
|
|
} catch (err) {
|
|
console.log(`${error.error}\n\n${error.error_description}`);
|
|
}
|
|
}
|
|
// Clean these up since we don't need them anymore
|
|
localStorage.removeItem("pkce_state");
|
|
localStorage.removeItem("pkce_code_verifier");
|
|
localStorage.removeItem("token_endpoint");
|
|
localStorage.removeItem("client_id");
|
|
return tokenResponse;
|
|
}
|
|
return tokenResponse;
|
|
};
|
|
|
|
export { tokenRequest, oauthRedirect };
|