willow
willow

Reputation: 1

Authorization code with PKCE returning invalid code verifier

I'm trying to generate an access token to use with Spotify's API using an authorization code with PKCE. After completing a post request, the response I get is that the code verifier is invalid. I'm fairly new to this, so I appreciate some of my code is not the most efficient way of doing things.

from dotenv import load_dotenv
import os
import base64
import requests
import random
import hashlib 
from urllib.parse import urlparse, urlunparse, urlencode

load_dotenv()
#retrieves client id and secret from .env file

client_ID = os.getenv("CLIENT_ID")
client_secret = os.getenv("CLIENT_SECRET")


def Generate_Random_String(length):
    possible_Values = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
    values = [] 
    while len(values) < length: 
        index = random.randint(0,65)
        random_Value = (possible_Values[index])
        values.append(random_Value)
    values = str(values)
    return (values)
    #create an array of random values of length given by parameter
    

code_Verifier = Generate_Random_String(64)
perm_code_verifier = open("Code Verifier.txt","w")
perm_code_verifier.write(code_Verifier)
#stores code verifier in a text file for more permanent access

def code_hash(plain):
    #will use sha256 hash to encode a value
    plaintext = str(plain)
    object = hashlib.sha256(plaintext.encode("utf-8"))
    hex_dig = object.hexdigest()
    return(hex_dig)


def base64encode(hashed):
    #encodes this in base64
    hashed = str(hashed)
    auth_bytes = hashed.encode("utf-8")
    auth_base64 = str(base64.b64encode(auth_bytes),"utf-8")
    array = auth_base64
    array = array.replace("+","-")
    array = array.replace("/","_")
    array = array.rstrip("=")
    #replaces these characters so it can be used in a url
    return array
`
  
hashed = code_hash(code_Verifier)
codeChallenge = base64encode(hashed)
#retrieves the code challenge

scope = "user-read-private user-read-email"
authURL = urlparse("https://accounts.spotify.com/authorize")
redirectURI = "http://localhost:3000"
#scope user agrees to and url for general spotify log in page



parameters = {
    "response_type": "code",
    "client_id": client_ID,
    "scope": scope,
    "code_challenge_method": "S256",
    "code_challenge": codeChallenge,
    "redirect_uri" : redirectURI,
}

authUrl_incl_params = authURL._replace(query=urlencode(parameters))
final_url = urlunparse(authUrl_incl_params)
#creates final url for my program to allow them to log in using appropriate scopes

print("Go to ", final_url, "to authenticate")
code = input("Enter code recieved:")


def get_token(code):
    file = open("Code Verifier.txt")
    codeVerifier = file.read()

    url = "https://accounts.spotify.com/api/token"

    payload = {
        "client_id": client_ID,
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": redirectURI,
        "code_verifier":codeVerifier,
    }

    headers = {
        "Content-Type": "application/x-www-form-urlencoded",
    }
    
    try:
        response = requests.post(url,data=payload,headers = headers)
        response_ans = response.json()
        print (response_ans)
        token = response_ans.get("access_token")
        return(token)

    except:
        print("Error with code")
    

token = get_token(code)
print (token)

My value for the token always comes back as none, and I get response_ans coming back as saying that code_verifier is invalid.

Upvotes: 0

Views: 173

Answers (1)

Bench Vue
Bench Vue

Reputation: 9390

This is official documentation from Spotify

https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow

Overview

enter image description here

  1. Request Authorization: The client sends a request to the Authorization Server, including the client ID, scope, code challenge, and redirect URI.
  2. Save Code Challenge: The Authorization Server saves the code challenge and prepares for user authentication.
  3. Authenticate User UI: The user views and agrees to the authentication request via the Spotify UI.
  4. Redirect URI with Code: After authentication, the Authorization Server redirects back to the specified redirect URI, sending an authorization code.
  5. Send Authorization Code: The client sends the authorization code, code verifier, and other details to the Token Endpoint.
  6. Validate Code Verifier: The Token Endpoint validates the code verifier against the previously saved code challenge.
  7. Generate Access Token: If the code verifier is valid, the Token Endpoint issues an access token.
  8. Receive Access Token: The client receives the access token from the Token Endpoint.
  9. Request Playlist: The client uses the access token to request a playlist from Spotify's API.
  10. Verify Access Token: Spotify's API validates the access token to ensure it is authorized.
  11. Return Playlist: Spotify's API sends the playlist data back to the client.
  12. Display Playlist: The client displays the playlist details (e.g., songs and artists) to the user

Demo Code

1. Install dependencies by pip

requirements.txt

Flask
requests
python-dotenv
pip install -r requirements.txt 

2. Copy Client ID & Redirect URI

from Developer Dashboard

https://developer.spotify.com/dashboard

enter image description here

3. Save Client ID

.env

CLIENT_ID=your client ID without bracket

4. demo.py

demo.py

from flask import Flask, request, redirect
import os
import requests
import json
import base64
import hashlib
import secrets
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

app = Flask(__name__)

AUTH_URL = 'https://accounts.spotify.com/authorize'
TOKEN_URL = 'https://accounts.spotify.com/api/token'
PORT = 3000  # Replace with your redirect port
REDIRECT_URI = f'http://localhost:{PORT}/callback'
CLIENT_ID = os.getenv("CLIENT_ID")  # Read CLIENT_ID from .env file

# Ensure CLIENT_ID is set
if not CLIENT_ID:
    raise EnvironmentError("CLIENT_ID is not set in the environment or .env file")

SCOPE = 'user-read-private user-read-email'

def validate_code_verifier(received_verifier, original_challenge):
    # Recalculate the code_challenge from the received code_verifier
    recalculated_challenge = base64.urlsafe_b64encode(
        hashlib.sha256(received_verifier.encode('utf-8')).digest()
    ).decode('utf-8').rstrip('=')
    
    # Compare the recalculated challenge with the original challenge
    if recalculated_challenge == original_challenge:
        print("Validation successful: code_verifier is valid.")
        return True
    else:
        print("Validation failed: code_verifier is invalid.")
        return False

# PKCE: Generate code_verifier and code_challenge
def generate_pkce_pair():
    code_verifier = secrets.token_urlsafe(64)  # Create a secure random string
    code_challenge = base64.urlsafe_b64encode(
        hashlib.sha256(code_verifier.encode('utf-8')).digest()
    ).decode('utf-8').rstrip('=')
    # Print the values to the server terminal
    print("Code Verifier:", code_verifier)
    print("Code Challenge:", code_challenge)    
    # For verification
    validate_code_verifier(code_verifier, code_challenge)
    return code_verifier, code_challenge

# Global PKCE variables
CODE_VERIFIER, CODE_CHALLENGE = generate_pkce_pair()

@app.route("/login")
def login():
    # Build authorization URL with PKCE parameters
    params = {
        'response_type': 'code',
        'client_id': CLIENT_ID,
        'scope': SCOPE,
        'code_challenge_method': 'S256',  # Required for PKCE
        'code_challenge': CODE_CHALLENGE,
        'redirect_uri': REDIRECT_URI,
    }
    authorization_url = AUTH_URL + '?' + '&'.join([f'{k}={v}' for k, v in params.items()])
    return redirect(authorization_url)

@app.route("/callback", methods=['GET'])
def callback():
    code = request.args.get('code')
    if not code:
        return "Error: No code provided", 400

    # Exchange authorization code for tokens
    response = requests.post(
        TOKEN_URL,
        data={
            'client_id': CLIENT_ID,
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': REDIRECT_URI,
            'code_verifier': CODE_VERIFIER  # Send the original code_verifier
        },
        headers={'Content-Type': 'application/x-www-form-urlencoded'}
    )

    if response.status_code != 200:
        return f"Error: {response.json()}", response.status_code

    # Save Access Token to environment variable
    os.environ['ACCESS_TOKEN'] = response.json().get('access_token')

    headers = {
        # Get Access Token from environment variable
        'Authorization': f"Bearer {os.environ['ACCESS_TOKEN']}"
    }

    # Example: Get playlist API call with your playlist ID
    url = 'https://api.spotify.com/v1/playlists/{}'.format('2CKnioFobYQRK3EEMf9Jr6')
    response = requests.get(url, headers=headers)
    results = response.json()

    # Extract and display songs
    songs = []
    for item in results['tracks']['items']:
        if item is not None and item['track']['artists'][0] is not None:
            # Display format <artist name : song title>
            songs.append(f"{item['track']['artists'][0]['name']} : {item['track']['name']}")

    return json.dumps(songs)

if __name__ == '__main__':
    app.run(port=PORT, debug=True)

5. Running demo

python demo.py

enter image description here

6. Login by Browser

http://localhost:3000/login

Login and Agree enter image description here

6. Got Token and Got Playlist

enter image description here

It matched Original playlist

https://open.spotify.com/playlist/2CKnioFobYQRK3EEMf9Jr6

enter image description here

Upvotes: 0

Related Questions