React Native with Expo: /oauth/token endpoint always results in access_denied in PKCE flow

I’m at my wits end. I’m trying to implement PKCE flow in my react native application. Basically I want a user to authenticate, and then I want a pair of authorization token and refresh token for my custom API, so they wouldn’t ever need to authenticate again (unless physically choosing to log out of course) (my plan is to automatically retrieve new access and refresh tokens on their behalf, and I’m using the rotating refresh token scheme, if I can ever get this PKCE to work…).

So here is my problem.

My call to the /authorize endpoint works like a charm everytime, i.e. an example response is:

{“type”:“success”,“params”:{“code”:“ne34Q0cRDTykpn-J”,“state”:“jim7azbo25f”},“errorCode”:null,“url”:“exp://127.0.0.1:19000/–/expo-auth-session?code=ne34Q0cRDTykpn-J&state=jim7azbo25f”}

Should be a walk in the park to call that /oauth/token endpoint to get the access_token and refresh_token. Nope. Nothing works. Trying for a few hours now, every combination I can think of I’m getting a response like this:

{“error”:“access_denied”,“error_description”:“Unauthorized”}

The options for my fetch call look like this:

const options = {
            method: 'POST',
            headers: { 'content-type': 'application/x-www-form-urlencoded' },
            form: {
                grant_type: 'authorization_code',
                client_id: AUTH0_CLIENT_ID,
                code_verifier: verifier,
                code: code,
                redirect_uri: base64URLEncode(redirectUrl),
            },
        };

Where the code comes off the code param in the response, the verifier is the same const variable as used in the /authorize endpoint, and the client ID is the client ID for my native app.

My first thought is that the ‘url encoding’ for the redirect_url field in the POST call is wrong. I’m using expo so my dev redirect URL is

Sign-in Complete

By the official docs, the ‘URL encoding’ function is:

function base64URLEncode(str) {
        return str.toString('base64')
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
    }

so my URL becomes ‘https:_auth.expo.io@fullstackchris_xn–rst-fya’

Another concern is the client ID for both calls - this should be the same client ID and the one I’ve defined as my ‘Native Application’ in the auth0 dashboard, correct?

By the way, this GET and POST pair of calls can both (and should) be done on the client-side, correct?

Anyone have any common gotchya’s with this PKCE flow? I also have a feeling that I’ve forgotten some setting in the dashboard, but I’ve followed this tutorial 3 times over now:

https://auth0.com/docs/flows/call-your-api-using-the-authorization-code-flow-with-pkce

If you haven’t done so already I would suggest for you to do the flow outside of the application, for example Postman or another tool that can be used to perform the OAuth 2.0 flow. If you replicate the issue outside of the application you have reduced the scope of things to look for which may be useful.

As a first step I would check if the client identifier you’re using is indeed configured to NOT require authentication in the token endpoint. In other words, start by checking that the application in the Auth0 dashboard has Token Endpoint Authentication Method set to none.

Yes! I got it! Your advice with Postman helped me see the error clearly. It turns out my code_verifier was incorrectly formed - it was too long.

Ultimately, the real issue was in what types are returned in node’s crypto package (as a Buffer) and expo’s crypto package (expo-crypto) (as a Uint8Array). I had to import the Buffer package and encode it to Base64 directly (using node’s encode package for Base64 encoding didn’t work for me)

The correct way to produce the code_verifier and code_challenge as specified by auth0 using expo packages is as follows:

import * as Crypto from 'expo-crypto';
import * as Random from 'expo-random';
import { Buffer } from 'buffer';

function URLEncode(str) {
    return str
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
}

async function sha256(buffer) {
    return await Crypto.digestStringAsync(
        Crypto.CryptoDigestAlgorithm.SHA256,
        buffer,
        { encoding: Crypto.CryptoEncoding.BASE64 }
    );
}

const randomBytes = await Random.getRandomBytesAsync(32);
const base64String = Buffer.from(randomBytes).toString('base64');
const code_verifier = URLEncode(base64String);
const code_challenge = URLEncode(await sha256(code_verifier));

(DON’T try and include the node crypto package as it’s deprecated) I hope this will prove useful to anyone else who finds this post!

If anyone is interested, I wrote a more detailed write up on these issues on my blog:

2 Likes

Thanks a lot for sharing that with the rest of community!

This topic was automatically closed 15 days after the last reply. New replies are no longer allowed.