I’m trying to implement the Authorization Code flow with PKCE for our Android app (Kotlin). The goal is to avoid storing client secrets on the device.
The initial auth step works fine. I generate the code_verifier and code_challenge using SHA-256 and Base64Url encoding. I redirect the user to https://login.genesyscloud.com/oauth2/v1/authorize with the correct params.
val codeVerifier = generateRandomString(128)
val codeChallenge = generateCodeChallenge(codeVerifier)
val authUrl = "https://login.genesyscloud.com/oauth2/v1/authorize?" +
"client_id=${clientId}" +
"&response_type=code" +
"&redirect_uri=${URLEncoder.encode(redirectUri, "UTF-8")}" +
"&code_challenge=${codeChallenge}" +
"&code_challenge_method=S256" +
"&scope=${URLEncoder.encode("openid profile email", "UTF-8")}"
The user logs in, and the browser redirects back to our app with the code. I then attempt to exchange this code for an access token via a POST request to https://login.genesyscloud.com/oauth2/v1/token.
Here is the body I’m sending:
{
"grant_type": "authorization_code",
"code": "AUTH_CODE_FROM_CALLBACK",
"redirect_uri": "com.myapp://callback",
"client_id": "MY_CLIENT_ID",
"code_verifier": "ORIGINAL_CODE_VERIFIER"
}
I’m getting a 401 Unauthorized response. The error payload says: {"error": "invalid_client", "error_description": "Client authentication failed"}.
I’ve double-checked that the client_id matches the one in the Genesys Cloud admin console under Apps > API Integrations. The redirect URI is also whitelisted. Since this is a public client (no secret), I shouldn’t need a client_secret.
Is there something specific about how Genesys handles PKCE validation in the token endpoint? I’ve compared my code_challenge generation against the RFC 7636 spec and it looks identical. The code_verifier I send back is the exact same string used to generate the challenge.
Any ideas on what might be causing the invalid_client error here?