diff --git a/assets/js/oauth.js b/assets/js/oauth.js index 8bd7b0423..447fe5334 100644 --- a/assets/js/oauth.js +++ b/assets/js/oauth.js @@ -1,89 +1,144 @@ -const redirectUri = `${ window.location.origin }/`; +const redirectUri = `${window.location.origin}/`; -////////////////////////////////////////////////////////////////////// // GENERAL HELPER FUNCTIONS -// Make a POST request and parse the response as JSON -const sendPostRequest = async(url, params) => { - let body = Object.keys(params).map(key => key + '=' + params[key]).join('&'); +/** + * 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', + method: "post", headers: { - 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' + "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); + console.error("Request failed", err); throw err; } -} -// Parse a query string into an object -const parseQueryString = string => { - if(string == "") { return {}; } - let segments = string.split("&").map(s => s.split("=") ); - let queryString = {}; - segments.forEach(s => queryString[s[0]] = s[1]); +}; + +/** + * 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 +}; + +/** + * Get OAuth configuration from OpenID Discovery endpoint + * + * @returns {Object} + */ + const getTokenConfiguration = async endpoint => { const options = { - method: 'GET', + method: "GET", headers: { - 'Content-type': 'application/json' + "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); + console.error("Request failed", err); throw err; } -} +}; -////////////////////////////////////////////////////////////////////// // PKCE HELPER FUNCTIONS -// Generate a secure random string using the browser crypto functions +/** + * Generates a secure random string using the browser crypto functions + * + * @returns {Object} + */ + const generateRandomString = () => { - var array = new Uint32Array(28); + 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 a promise that resolves to an ArrayBuffer + return Array.from(array, dec => `0${dec.toString(16)}`.substr(-2)).join(""); +}; + +/** + * Calculate the SHA256 hash of the input text + * + * @returns {Promise} + */ + const sha256 = plain => { const encoder = new TextEncoder(); const data = encoder.encode(plain); - return window.crypto.subtle.digest('SHA-256', data); -} -// Base64-urlencodes the input string -const base64urlencode = str => { - // Convert the ArrayBuffer to string using Uint8 array to conver to what btoa accepts. + 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} + */ + +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 =) - return btoa(String.fromCharCode.apply(null, new Uint8Array(str))) - .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); -} -// Return the base64-urlencoded sha256 hash for the PKCE challenge -const pkceChallengeFromVerifier = async(v) => { - let hashed = await sha256(v); - return base64urlencode(hashed); -} + 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 -// Initiate PKCE Auth Code flow when requested -const tokenRequest = async({ +/** + * Initiates PKCE Auth Code flow when requested + * + * @param {Object} - The necessary params + * @returns {Void} + */ + +const tokenRequest = async ({ oidcDiscoveryUrl, grantType, authUrl, @@ -91,85 +146,93 @@ const tokenRequest = async({ clientId, scope }) => { - // Check oauth configuration - if (oidcDiscoveryUrl !== '') { - const { authorization_endpoint, token_endpoint } = await getTokenConfiguration(oidcDiscoveryUrl); + 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); + 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); + 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); + 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 = () => { - return 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' - ; - } + 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 -const oauthRedirect = async() => { - let tokenResponse = ''; +/** + * 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 (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) { + if (q.code) { // Verify state matches what we set at the beginning - if(localStorage.getItem('pkce_state') != q.state) { - alert('Invalid state'); + 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') - }); + 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); + 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'); + localStorage.removeItem("pkce_state"); + localStorage.removeItem("pkce_code_verifier"); + localStorage.removeItem("token_endpoint"); + localStorage.removeItem("client_id"); return tokenResponse; } return tokenResponse; -} +}; -export { tokenRequest, oauthRedirect } +export { tokenRequest, oauthRedirect }; diff --git a/components/autocomplete.vue b/components/autocomplete.vue index 3a0a936d8..6a32c306b 100644 --- a/components/autocomplete.vue +++ b/components/autocomplete.vue @@ -58,6 +58,7 @@ padding: 8px 16px; font-size: 16px; font-family: "Roboto Mono", monospace; + font-weight: 400; &:last-child { border-radius: 0 0 8px 8px; diff --git a/components/graphql/argument.vue b/components/graphql/argument.vue new file mode 100644 index 000000000..d70f0d01c --- /dev/null +++ b/components/graphql/argument.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/components/graphql/field.vue b/components/graphql/field.vue index b5acb1b54..b0fc403e5 100644 --- a/components/graphql/field.vue +++ b/components/graphql/field.vue @@ -1,6 +1,23 @@