Skip to content

Authorization Code with PKCE Flow

The authorization code flow with PKCE is the recommended authorization flow if you’re implementing authorization in a mobile app, single page web app, or any other type of application where the client secret can’t be safely stored.

This guide will cover the implementation of the PKCE flow in a single page application in order to display Spotify user related information.

The steps to implement the PKCE extension are quite similar to the steps involved with the authorization code flow:

  • A Spotify user visits our application and taps on the Log in button.
  • The application makes a request to the authorization server.
  • The authorization server displays a dialog asking the user to grant permissions to the application.
  • Once the user accepts the permissions, the authorization server redirects the user back to the application using a URL which contains an authorization code.
  • The application requests an access token using the code provided in the previous step.
  • Once received, the application uses the access token to make API calls.

In this tutorial you will:

  • Understand why PKCE flow is recommended over the implicit grant flow.
  • Implement the access token request.
  • Make an API call to request Spotify user information.

Pre-requisites

This guide assumes that:

Code Verifier

The PKCE authorization flow starts with the creation of a code verifier. According to the PKCE standard, a code verifier is a high-entropy cryptographic random string with a length between 43 and 128 characters. It can contain letters, digits, underscores, periods, hyphens, or tildes.

The code verifier will be generated using the following JavaScript function:


_10
function generateRandomString(length) {
_10
let text = '';
_10
let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
_10
_10
for (let i = 0; i < length; i++) {
_10
text += possible.charAt(Math.floor(Math.random() * possible.length));
_10
}
_10
return text;
_10
}

Code Challenge

Once the code verifier has been generated, we must transform (hash) it using the SHA256 algorithm. This is the value that will be sent within the user authorization request.

We are creating a single page app so we can't use the Node.js crypto libraries to generate the hash. Let's use window.crypto.subtle.digest to generate the value using the SHA256 algorithm from the given data:


_10
const digest = await window.crypto.subtle.digest('SHA-256', data);

The generateCodeChallenge function returns the base64 representation of the digest by calling to base64encode():


_14
async function generateCodeChallenge(codeVerifier) {
_14
function base64encode(string) {
_14
return btoa(String.fromCharCode.apply(null, new Uint8Array(string)))
_14
.replace(/\+/g, '-')
_14
.replace(/\//g, '_')
_14
.replace(/=+$/, '');
_14
}
_14
_14
const encoder = new TextEncoder();
_14
const data = encoder.encode(codeVerifier);
_14
const digest = await window.crypto.subtle.digest('SHA-256', data);
_14
_14
return base64encode(digest);
_14
}

Request User Authorization

In order to request authorization from the user, we must make a GET request to the /authorize endpoint passing the same parameters as authorization code flow does along with two additional ones: code_challenge and code_challenge_method.

Query ParameterValue
client_idRequired The Client ID generated after registering your application.
response_typeRequired Set to code.
redirect_uriRequired The URI to redirect to after the user grants or denies permission. This URI needs to have been entered in the Redirect URI allowlist that you specified when you registered your application (See the app guide). The value of redirect_uri here must exactly match one of the values you entered when you registered your application, including upper or lowercase, terminating slashes, and such.
stateOptional, but strongly recommended This provides protection against attacks such as cross-site request forgery. See RFC-6749.
scopeOptional A space-separated list of scopes.If no scopes are specified, authorization will be granted only to access publicly available information: that is, only information normally visible in the Spotify desktop, web, and mobile players.
code_challenge_methodRequired. Set to S256.
code_challengeRequired. Set to the code challenge that your app calculated in the previous step

The code to request the user authorization looks like this:


_23
const clientId = 'YOUR_CLIENT_ID';
_23
const redirectUri = 'http://localhost:8080';
_23
_23
let codeVerifier = generateRandomString(128);
_23
_23
generateCodeChallenge(codeVerifier).then(codeChallenge => {
_23
let state = generateRandomString(16);
_23
let scope = 'user-read-private user-read-email';
_23
_23
localStorage.setItem('code_verifier', codeVerifier);
_23
_23
let args = new URLSearchParams({
_23
response_type: 'code',
_23
client_id: clientId,
_23
scope: scope,
_23
redirect_uri: redirectUri,
_23
state: state,
_23
code_challenge_method: 'S256',
_23
code_challenge: codeChallenge
_23
});
_23
_23
window.location = 'https://accounts.spotify.com/authorize?' + args;
_23
});

The app generates a PKCE code challenge and redirects to the Spotify authorization server login page by updating the window.location object value, so the user can grant permissions to our application. Note how the code verifier value is stored locally using the localStorage JavaScript property to be used in the next step of the authorization flow.

Once the user accepts the requested permissions, the OAuth service redirects the user back to the URL specified in the redirect_uri field. This callback contains two parameters within the URL:

Query ParameterValue
codeAn authorization code that can be exchanged for an access token.
stateThe value of the state parameter supplied in the request.

We must parse the URL and save the code parameter to request the access token afterwards:


_10
const urlParams = new URLSearchParams(window.location.search);
_10
let code = urlParams.get('code');

Request an access token

Once the user has accepted the request of the previous step, we can request an access token by making a POST request to the /api/token endpoint, with the code and the code verifier values.

The parameters are similar to the authorization code flow, with two additional ones: client_id and code_verifier.

REQUEST BODY PARAMETERVALUE
grant_typeRequired This field must contain the value "authorization_code".
codeRequired The authorization code returned from the previous request.
redirect_uriRequired This parameter is used for validation only (there is no actual redirection). The value of this parameter must exactly match the value of redirect_uri supplied when requesting the authorization code.
client_idRequired. The client ID for your app, available from the developer dashboard.
code_verifierRequired. The value of this parameter must match the value of the code_verifier that your app generated in the previous step.

The body of the request can be implemented as follows:


_10
let codeVerifier = localStorage.getItem('code_verifier');
_10
_10
let body = new URLSearchParams({
_10
grant_type: 'authorization_code',
_10
code: code,
_10
redirect_uri: redirectUri,
_10
client_id: clientId,
_10
code_verifier: codeVerifier
_10
});

Finally, we can make the POST request and store the access token by parsing the JSON response from the server:


_19
const response = fetch('https://accounts.spotify.com/api/token', {
_19
method: 'POST',
_19
headers: {
_19
'Content-Type': 'application/x-www-form-urlencoded'
_19
},
_19
body: body
_19
})
_19
.then(response => {
_19
if (!response.ok) {
_19
throw new Error('HTTP status ' + response.status);
_19
}
_19
return response.json();
_19
})
_19
.then(data => {
_19
localStorage.setItem('access_token', data.access_token);
_19
})
_19
.catch(error => {
_19
console.error('Error:', error);
_19
});

Once the server verifies the code verifier parameter sent in the request, it will return the access token. We can locally store the access token in order to make an API call by sending the Authorization header along with the value.

The following code implements the getProfile() function which performs the API call to the /me endpoint in order to retrieve the user profile related information:


_11
async function getProfile(accessToken) {
_11
let accessToken = localStorage.getItem('access_token');
_11
_11
const response = await fetch('https://api.spotify.com/v1/me', {
_11
headers: {
_11
Authorization: 'Bearer ' + accessToken
_11
}
_11
});
_11
_11
const data = await response.json();
_11
}

Refreshing the access token

In order to refresh the token, a POST request must be sent with the following body parameters encoded in application/x-www-form-urlencoded:

REQUEST BODY PARAMETERVALUE
grant_typeRequired Set it to refresh_token.
refresh_tokenRequired The refresh token returned from the authorization code exchange.
client_idRequired The client ID for your app, available from the developer dashboard.

The headers of this POST request must contain the Content-Type header set to application/x-www-form-urlencoded value.