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('&'); 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 const parseQueryString = string => { if(string == "") { return {}; } let segments = string.split("&").map(s => s.split("=") ); let queryString = {}; segments.forEach(s => queryString[s[0]] = s[1]); return queryString; } // Get OAuth configuration from OpenID Discovery endpoint 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 // Generate a secure random string using the browser crypto functions const generateRandomString = () => { var 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 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. // 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); } ////////////////////////////////////////////////////////////////////// // OAUTH REQUEST // Initiate PKCE Auth Code flow when requested 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 = () => { 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' ; } // 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 = ''; 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 }