Unable to Exchange Auth Code for Access Token
-
I am trying to use a PKCE Auth Code flow to get an access token and I’m trying to do so entirely in vanilla javascript.
I tried to follow the concepts Justin presented here.
Everything seems to work well in retrieving an auth code. After clicking the
#leo-login-submit-button
button I am redirected to the appropriate link where I enter my credentials and am redirected back to my local install where the auth code is included in the uri.My javascript parses the auth code from the uri and submits it along with
grant_type
,client_id
,redirect_uri
, andcode_verifier
. The response is400
with content ofinvalid_client: This client is invalid or must authenticate using a client secret
.If I add the client secret then I get and error of
invalid_grant: Authorization code doesn't exist or is invalid for the client
.Here is my javascript:
var config = { client_id: '{client_id}', redirect_uri: 'https://api.test/api-test/', authorization_endpoint: 'https://leoadventures.com/oauth/authorize', token_endpoint: 'https://leoadventures.com/oauth/token', }; // Initiate the PKCE Auth Code flow when the link is clicked document.getElementById("leo-login-submit-button").addEventListener("click", async function(e){ e.preventDefault(); // Create and store a random "state" value var state = generateRandomString(); localStorage.setItem("pkce_state", state); // Create and store a new PKCE code_verifier (the plaintext random secret) var code_verifier = generateRandomString(); localStorage.setItem("pkce_code_verifier", code_verifier); // Hash and base64-urlencode the secret to use as the challenge var code_challenge = await pkceChallengeFromVerifier(code_verifier); // Build the authorization URL var url = config.authorization_endpoint + "?response_type=code" + "&client_id=" + encodeURIComponent(config.client_id) + "&state=" + encodeURIComponent(state) + "&redirect_uri=" + encodeURIComponent(config.redirect_uri) + "&code_challenge=" + encodeURIComponent(code_challenge) + "&code_challenge_method=S256" ; // Redirect to the authorization server window.location.replace( url ); }); // Handle the redirect back from the authorization server and // get an access token from the token endpoint var 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); document.getElementById("error_details").innerText = q.error + "\n\n" + q.error_description; document.getElementById("error").classList = ""; } // 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 { // Exchange the authorization code for an access token sendPostRequest(config.token_endpoint, { grant_type: "authorization_code", code: q.code, client_id: config.client_id, redirect_uri: config.redirect_uri, code_verifier: localStorage.getItem("pkce_code_verifier") }, function(request, body) { // Initialize your application now that you have an access token. // Here we just display it in the browser. document.getElementById("access_token").innerText = body.access_token; document.getElementById("start").classList = "hidden"; document.getElementById("token").classList = ""; // Replace the history entry to remove the auth code from the browser address bar window.history.replaceState({}, null, "/"); }, function(request, error) { // This could be an error response from the OAuth server, or an error because the // request failed such as if the OAuth server doesn't allow CORS requests document.getElementById("error_details").innerText = error.error + "\n\n" + error.error_description + "\n\ncode: " + q.code + "\n\ncode: " + q.code; document.getElementById("error").classList = ""; }); } // Clean these up since we don't need them anymore localStorage.removeItem("pkce_state"); localStorage.removeItem("pkce_code_verifier"); } // Generate a secure random string using the browser crypto functions function 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 function sha256(plain) { const encoder = new TextEncoder(); const data = encoder.encode(plain); return window.crypto.subtle.digest('SHA-256', data); } // Base64-urlencodes the input string function base64urlencode(str) { // Convert 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 async function pkceChallengeFromVerifier(v) { hashed = await sha256(v); return base64urlencode(hashed); } // Parse a query string into an object function parseQueryString(string) { if(string == "") { return {}; } var segments = string.split("&").map(s => s.split("=") ); var queryString = {}; segments.forEach(s => queryString[s[0]] = s[1]); return queryString; } // Make a POST request and parse the response as JSON function sendPostRequest(url, params, success, error) { var request = new XMLHttpRequest(); request.open('POST', url, true); request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); request.onload = function() { var body = {}; try { body = JSON.parse(request.response); } catch(e) {} if(request.status == 200) { success(request, body); } else { error(request, body); } } request.onerror = function() { error(request, {}); } var body = Object.keys(params).map(key => key + '=' + params[key]).join('&'); request.send(body); }
- The topic ‘Unable to Exchange Auth Code for Access Token’ is closed to new replies.