Reputation: 1
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
Reputation: 9390
This is official documentation from Spotify
https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow
requirements.txt
Flask
requests
python-dotenv
pip install -r requirements.txt
from Developer Dashboard
https://developer.spotify.com/dashboard
.env
CLIENT_ID=your client ID without bracket
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)
python demo.py
http://localhost:3000/login
It matched Original playlist
https://open.spotify.com/playlist/2CKnioFobYQRK3EEMf9Jr6
Upvotes: 0