Brian Peterson Posted January 28, 2023 Posted January 28, 2023 I am working on a custom php test app that validates the user for an API call using OATH2 with PKCE. I am able to successfully get the authorization_code from the invision server, but when I post that back to get the access_token to make the API calls I get the following error: "invalid_grant","error_description":400 For background I'm using the following code to get the authorization_code // Remove all previous session variables as they are not valid at this point session_unset(); // generate a random 16 digit string for the session $_SESSION['state']= bin2hex(random_bytes(16)); // Request the authorization code from the auth0 server $href = $authorization_endpoint; $href .= '?response_type=code&client_id='. $client_id; $href .= '&redirect_uri=' . $redirect_uri; $href .= '&scope=profile%20email'; // choices are profile or email $href .= '&state=' . $_SESSION['state']; if ($PKCE == 'Yes'){ // generate the PKCE $random = bin2hex(openssl_random_pseudo_bytes(32)); $_SESSION['verifier'] = base64url_encode(pack('H*', $random)); $_SESSION['challenge'] = base64url_encode(pack('H*', hash('sha256', $_SESSION['verifier']))); // add the PKCE to the HREF $href .= '&code_challenge=' . $_SESSION['challenge']; $href .= '&code_challenge_method=S256'; } $_SESSION['authcode'] = ""; echo '<br><br><a class="button" href=' . $href . ';">Login</a><br>'; Which when tested using the login button it does result in the "code" and "state" sent back properly as expected. HTTP/1.1 302 Found Location: {yourCallbackUrl}?code={authorizationCode}&state=xyzABC123 Then I am using the following code to get the access_token. Note $token_endpoint is exactly what was provided when setting up the OATH token, e.g., 'https://{sitename}/forum/oauth/token/' if (!empty ($_GET['code']) && ($_GET['state'] == $_SESSION['state'])) { $_SESSION['authcode'] = $_GET['code']; //prepare POST to verify code with auth0 server // Intialize the cURL $curl = curl_init(); // Setup the next page of cURL Options array curl_setopt_array($curl, [ CURLOPT_URL => $token_endpoint, CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_POSTFIELDS => "grant_type=authorization_code&client_id=" . $client_id ."&code_verifier=" . $_SESSION['verifier']. "&code=" . $_SESSION['authcode'] ."&redirect_uri=" . $redirect_uri, //CURLOPT_POSTFIELDS => "grant_type=authorization_code&client_id=" . $client_id ."&code_verifier=%7B" . $_SESSION['verifier']. "%7D&code=%7B" . $_SESSION['authcode'] ."%7D&redirect_uri=" . $redirect_uri, CURLOPT_HTTPHEADER => [ "content-type: application/x-www-form-urlencoded" ], ]); // Execute the cURL and save the results $oath1Response = curl_exec( $curl ); $err = curl_error($curl); curl_close($curl); if ($err) { echo "<br><br>!! cURL Error #:" . $err; } else { echo $oath1Response; } } I had found an example of the commented out CURLOPT_POSTFIELDS here on auth0.com. But both versions generate the same "invalid_grant" error message. Any guidance as to what I'm missing or sending incorrectly?
Brian Peterson Posted January 29, 2023 Author Posted January 29, 2023 For context the configuration for the Authorization Grant Code is If it is set to "Required: only S256 accepted" it throws an error of "transform algorithm not supported" : "code challenge required". Which is weird because its set to 'S256' which forum\oauth\authorize\index.php is looking for. $href .= '&code_challenge_method=S256'; 2 hours ago, Brian Peterson said: For background I'm using the following code to get the authorization_code // Remove all previous session variables as they are not valid at this point session_unset(); // generate a random 16 digit string for the session $_SESSION['state']= bin2hex(random_bytes(16)); // Request the authorization code from the auth0 server $href = $authorization_endpoint; $href .= '?response_type=code&client_id='. $client_id; $href .= '&redirect_uri=' . $redirect_uri; $href .= '&scope=profile%20email'; // choices are profile or email $href .= '&state=' . $_SESSION['state']; if ($PKCE == 'Yes'){ // generate the PKCE $random = bin2hex(openssl_random_pseudo_bytes(32)); $_SESSION['verifier'] = base64url_encode(pack('H*', $random)); $_SESSION['challenge'] = base64url_encode(pack('H*', hash('sha256', $_SESSION['verifier']))); // add the PKCE to the HREF $href .= '&code_challenge=' . $_SESSION['challenge']; $href .= '&code_challenge_method=S256'; } $_SESSION['authcode'] = ""; echo '<br><br><a class="button" href=' . $href . ';">Login</a><br>'; I did a bunch of digging and found a reference here about the error I'm getting, quoted below. Quote Once you have your method chosen using following the documentation to generate a string between 43 and 128 characters long. Then base64 encode it, using a URL-safe base64 encoding or replacing the + and / characters as appropriate for your language. Quote Use your hash function as documented to create a SHA256 hash. Important note for PHP users: Make sure to set the “raw_output” boolean to true or you’ll get an invalid_grant error. Then base64 encode as before. So I have edited the above code to be the following, bumping up the random string from 32 to 64 char and changing implementation methods of the hashes and base64 encoding. The old code is commented out directly below for easy reference. Basically removing the pack function and ensuring the Boolean true is enabled on the hash function. // Remove all previous session variables as they are not valid at this point session_unset(); // generate a random 16 digit string for the session $_SESSION['state']= bin2hex(random_bytes(16)); // Request the authorization code from the auth0 server $href = $authorization_endpoint; $href .= '?response_type=code&client_id='. $client_id; $href .= '&redirect_uri=' . $redirect_uri; $href .= '&scope=profile%20email'; // choices are profile or email $href .= '&state=' . $_SESSION['state']; if ($PKCE == 'Yes'){ // generate the PKCE // generate a string between 43 and 128 characters long $random = bin2hex(openssl_random_pseudo_bytes(64)); // base64 URL safe encode random string $_SESSION['verifier'] = base64url_encode($random); //$_SESSION['verifier'] = base64url_encode(pack('H*', $random)); // SHA 256 hash the "verifier", then base64 URL safe encode // **NOTE: In PHP hash function set the “raw_output” boolean to true or you’ll get an invalid_grant error $_SESSION['challenge'] = base64url_encode(hash('sha256', $_SESSION['verifier'], true)); //$_SESSION['challenge'] = base64url_encode(pack('H*', hash('sha256', $_SESSION['verifier'], true))); // add the PKCE to the HREF $href .= '&code_challenge=' . $_SESSION['challenge']; $href .= '&code_challenge_method=S256'; } $_SESSION['authcode'] = ""; echo '<br><br><a class="button" href=' . $href . ';">Login</a><br>'; This still generates {"error":"invalid_grant","error_description":400} which is thrown from forum\oauth\token\index.php which indicates it is failing in the validateAuthorizationCode() function. /* Validate grant */ $accessToken = NULL; switch ( $request->grantType( \IPS\Request::i()->grant_type ) ) { case 'authorization_code': $accessToken = $request->validateAuthorizationCode( \IPS\Request::i()->code, \IPS\Request::i()->redirect_uri, isset( \IPS\Request::i()->code_verifier ) ? \IPS\Request::i()->code_verifier : NULL ); break; ... } /* Return */ if ( $accessToken ) { ... } else { throw new \IPS\Login\Handler\OAuth2\Exception( 'invalid_grant', 400 ); } further debugging into validateAuthorizationCode() function we see several things: $authorizationCode['code_challenge_method'] is blank/empty $this->client->pkce is set to "plain" which would seem to indicate that it ignores the original POST "&code_challenge_method=S256" Also it is interesting to note that method for rtrim( strtr( base64_encode( pack( 'H*', hash( 'sha256', $codeVerifier ) ) ), '+/', '-_' ), '=' ); is equivalent to $_SESSION['challenge'] = base64url_encode(pack('H*', hash('sha256', $_SESSION['verifier']))); which does NOT provide the "raw_output", not sure which method is "correct". /* If it has a code verifier, validate that */ if ( $authorizationCode['code_challenge'] or $this->client->pkce !== 'none' ) { if ( !$codeVerifier or !$authorizationCode['code_challenge'] ) { return; } if ( $authorizationCode['code_challenge_method'] === 'S256' or $this->client->pkce === 'S256' ) { $codeVerifier = rtrim( strtr( base64_encode( pack( 'H*', hash( 'sha256', $codeVerifier ) ) ), '+/', '-_' ), '=' ); } if ( !\IPS\Login::compareHashes( $authorizationCode['code_challenge'], $codeVerifier ) ) { return; } }
Solution Brian Peterson Posted January 29, 2023 Author Solution Posted January 29, 2023 Continuing troubleshooting from the last post... digging I noticed that a '"' was being attached to the original POST to start the OATH in the strings being passed to forum\oauth\authorize\index.php echo '<br><br><a class="button" href=' . $href . ';">Login</a><br>'; changed to echo '<br><br><a class="button" href=' . $href . '>Login</a><br>'; That fixes the proper setting of to be recognized properly code_challenge_method=S256 because it had been seen as: code_challenge_method=S256" Half solved!! Yay! And it results in -= Var_dump $json =-array(4) { ["access_token"]=> string(97) "914...b373" ["token_type"]=> string(6) "bearer" ["expires_in"]=> int(86400) ["scope"]=> string(13) "profile email" } WOOT! Problem solved. Successfully got an access token!
Recommended Posts