My Question or Issue
Hi all, I am not usually one to post in forums as I enjoy getting to the bottom of my issues on my own, but I am having issues with the PKCE flow for the Spotify API.
For context, I am developing an app in React Native using Expo. I'm not an expert in mobile development, and this is the first large project I am attempting in mobile development.
The problem I am facing is that when my authentication flow executes, I am constantly getting an error 400 when attempting to exchange the code for a token. Let me walk you through what I have so far:
I have a very simple login screen like so:
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Button title="Login with Spotify" onPress={authenticateWithSpotify} />
</View>
);
authenticateWithSpotify calls this hook: const { authenticateWithSpotify, isAuthenticated } = useSpotifyAuth();
which calls my AuthenticationProvider context which wraps the entire App. This executes the authentication flow. The first step of my authentication flow is to generate a codeVerifier and codeChallenge. This is where I think the main issue may be. I call this function to create the verifier and challenge:
async function generatePKCEPair() {
try {
const codeVerifierLength = Math.floor(Math.random() * (128 - 43 + 1)) + 43;
const codeVerifier = generateCodeVerifier(codeVerifierLength);
const codeChallenge = await generateCodeChallenge(codeVerifier);
await AsyncStorage.setItem("spotify_code_verifier", codeVerifier);
await AsyncStorage.setItem("spotify_code_challenge", codeChallenge);
return codeChallenge;
}
catch (error) {
console.error("Error creating code verifier or code challenge: ", error);
return "";
}
}
The generateCodeVerifier() function will simply generate a random string of uppercase or lowercase letters and numbers, of random length between 43 and 128 characters.
The generateCodeChallenge() function is my primary suspect for the error I am receiving. It looks like this:
export async function generateCodeChallenge(codeVerifier: string): Promise<string> {
try
{
const hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
codeVerifier,
{ encoding: Crypto.CryptoEncoding.BASE64 }
);
// Convert the hash to base64-url encoding
return hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
catch (e) {
console.error("Error in PKCE process: ", e);
return "Error in PKCE process!";
}
}
from my research, I know I need to hash the codeVerifier using SHA-256 and then encode it using base64-url encoding. On the Spotify documentation page for PKCE flow, they use this method:
const sha256 = async (plain) => {const encoder = new TextEncoder()
const data = encoder.encode(plain)
return window.crypto.subtle.digest('SHA-256', data)
}
but whenever I tried using window.crypto.subtle.digest, I got an error that window was not available because the mobile app is not browser based (I am testing in an iOS emulator).
After generating the PKCE values, I attempt to retrieve an auth code like this:
const authRequest = new AuthSession.AuthRequest({
responseType: AuthSession.ResponseType.Code,
clientId: SPOTIFY_CLIENT_ID,
scopes: ["user-read-private", "user-read-email"],
codeChallengeMethod: AuthSession.CodeChallengeMethod.S256,
codeChallenge: codeChallenge,
redirectUri: SPOTIFY_REDIRECT_URI1,
usePKCE: true,
});
const authResult = await authRequest.promptAsync(discovery);
if (authResult.type === "success") {
console.log("Auth code: " + authResult.params.code);
console.log("Auth state: " + authResult.params.state);
exchangeCodeForToken(authResult.params.code);
}
if a code is successfully retrieved I call exchangeCodeForToken():
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
const data = {
grant_type: AuthSession.GrantType.AuthorizationCode,
code: code,
redirect_uri: SPOTIFY_REDIRECT_URI1,
client_id: SPOTIFY_CLIENT_ID,
code_verifier: code_verifier, // stored and then retrieved from local storage
};
await axios.post(
new URLSearchParams(data),
{ headers },
).then((response) => {
const tokenData = response.data;
setAccessToken(tokenData.access_token);
fetchUserProfile(tokenData.access_token);
}).catch((error) => {
throw new Error(`Token exchange failed: ${error}`);
});
So far, I successfully get redirected to spotify's authorization page, but when I click agree I get an error and do not receive a token. I tried executing a curl request with the authorization code I receive in the first authentication step within terminal as well, but I get the error 'code_verifier was incorrect'. I am failing to understand what I am doing wrong, but it seems to be something with the code verifier or my code challenge. If anyone has any ideas, please I would love some help and guidance. Thanks!