Authorization Code Grant Flow with PKCE
Hey there, I'd like to request input or discussion on using Auth0's PKCE flow that allows for a refresh token in addition to the standard id token and access token.
The standard Implicit flow works fine, as demonstrated in the example code. This flow is important in mobile applications, and example here would be extremely valuable and helpful for those using the Expo flavor of React Native and integrating with Auth0. Their documentation makes use of the Node crypto library, which appears to be unavailable in a React Native app.
any luck with this - I am not sure how to use the access token provided as it doesn't look like a full JSON token - and the documentation seems to be quite lacking on this front - any ideas?
Hi @BenjaminWatts. Your issue may be unrelated to the lack of support of crypto as noted above.
At least with Auth0, my understanding is: to get a JWT access_token back, you need to supply an audience matching your API audience value when you make your initial /authorize request. Without the audience, Auth0 seems to return a generic access token, rather than a JWT access_token.
I'm also very interested in this. Has anyone gotten this working in a reasonable way?
@zth If you have access to a high quality random number source (which afaik is not available in non-ejected expo), you can do something like this:
const redirectUrl = AuthSession.getRedirectUrl();
const verifier = base64URLEncode(randomBytes(32));
const challenge = createChallenge(verifier);
const result = await AuthSession.startAsync({
authUrl:
`${auth0Domain}/authorize` +
toQueryString({
audience: "https://some.hostname.com/some/api/url",
client_id: auth0ClientId,
response_type: "code",
scope: "openid profile email",
code_challenge: challenge,
code_challenge_method: "S256",
redirect_uri: redirectUrl,
connection: "optional-id-for-connection-that-will-be-preselected-auth0-lock-screen"
})
});
if (result.type !== "success") {
throw Error(
`result.type was ${
result.type
} instead of "success", full result was: ${JSON.stringify(
result,
null,
2
)}`
);
}
const code = result.params.code;
const body = {
grant_type: "authorization_code",
client_id: auth0ClientId,
code_verifier: verifier,
code,
redirect_uri: redirectUrl
};
const resp = await fetch("https://some.host.domain.com/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const respJson = await resp.json();
console.log("respJson=", JSON.stringify(respJson, null, 2));
const token: IJwtAccessToken = jwtDecoder(respJson.access_token);
console.log("token=", JSON.stringify(token, null, 2));
const email = token.sub.split("|")[1];
To follow up on this, for what it's worth, we decided to make use of a public endpoint that would return the verifier and challenge.
We have an express endpoint serve the following. It's not great, but works for now. Most of our users only ever log in once.
const verifier = base64.encode(crypto.randomBytes(32))
const challenge = base64.encode(sha256(verifier))
return {
verifier,
challenge
}
Thank you for the responses! I ended up doing the same thing, exposing an endpoint for the challenge/verifier. I think expo is working on supporting a native crypto module that can get safe random numbers, so I'll migrate to that whenever that's available. But the endpoint is fine for now for me at least. Thanks!
Doesn't exposing an endpoint for challenge/verifier defeat the purpose of signing with PKCE?
hi there, is there any update here? this is still an issue and it's impossible to get a refresh token with social login (Google) without using PKCE flow, and we don't have crypto module available until SDK33... :(
Expo SDK 33 has been released now and comes with https://www.npmjs.com/package/expo-random
came up with this Expo solution based on the JavaScript example in the Auth0 Doc:
import { AuthSession } from 'expo';
import * as Crypto from 'expo-crypto';
import * as Random from 'expo-random';
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);
let verifier = URLEncode(btoa(randomBytes.toString()));
let challenge = URLEncode(await sha256(verifier));
const redirectUrl = AuthSession.getRedirectUrl();
let authUrl = `${auth0Domain}/authorize?` + this.toQueryString({
audience: `${auth0Audience}`,
client_id: `${auth0ClientID}`,
connection: 'facebook',
scope: 'openid profile email offline_access',
redirect_uri: redirectUrl,
response_type: 'code',
code_challenge: challenge,
code_challenge_method: "S256"
nonce: 'test',
});
const result = await AuthSession.startAsync({
authUrl: authUrl
});
if (result.type === 'success' && result.params && result.params.code)
let code = result.params.code;
// Proxy call to /oauth/token through backend API using code, verifier, and redirectUrl
// /oauth/token will return a 401 if called from JavaScript front end