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:
- You have read the authorization guide.
- You have created an app following the apps guide.
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:
_10function 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:
_10const digest = await window.crypto.subtle.digest('SHA-256', data);
The generateCodeChallenge
function returns the base64
representation of the
digest by calling to base64encode()
:
_14async 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 Parameter | Value |
---|---|
client_id | Required The Client ID generated after registering your application. |
response_type | Required Set to code . |
redirect_uri | Required 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. |
state | Optional, but strongly recommended This provides protection against attacks such as cross-site request forgery. See RFC-6749. |
scope | Optional 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_method | Required. Set to S256 . |
code_challenge | Required. Set to the code challenge that your app calculated in the previous step |
The code to request the user authorization looks like this:
_23const clientId = 'YOUR_CLIENT_ID';_23const redirectUri = 'http://localhost:8080';_23_23let codeVerifier = generateRandomString(128);_23_23generateCodeChallenge(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 Parameter | Value |
---|---|
code | An authorization code that can be exchanged for an access token. |
state | The 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:
_10const urlParams = new URLSearchParams(window.location.search);_10let 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 PARAMETER | VALUE |
---|---|
grant_type | Required This field must contain the value "authorization_code" . |
code | Required The authorization code returned from the previous request. |
redirect_uri | Required 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_id | Required. The client ID for your app, available from the developer dashboard. |
code_verifier | Required. 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:
_10let codeVerifier = localStorage.getItem('code_verifier');_10_10let 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:
_19const 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:
_11async 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 PARAMETER | VALUE |
---|---|
grant_type | Required Set it to refresh_token . |
refresh_token | Required The refresh token returned from the authorization code exchange. |
client_id | Required 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.