Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spotify PKCE authorization flow returns "code_verifier was incorrect"

I've been following the Spotify API's Authentication Guide to authenticate my app using PKCE.

As of now, I am using a dummy code verifier with a pre-calculated challenge for debugging. These values were calculated using multiple online tools (SHA256, SHA256, base64url, base64url) and match the values returned from the hashing/encoding functions I've written in Swift. Feel free to use those links above to verify these.

let verifier = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
let challenge = "66d34fba71f8f450f7e45598853e53bfc23bbd129027cbb131a2f4ffd7878cd0"
let challengeBase64URL = "NjZkMzRmYmE3MWY4ZjQ1MGY3ZTQ1NTk4ODUzZTUzYmZjMjNiYmQxMjkwMjdjYmIxMzFhMmY0ZmZkNzg3OGNkMA"

I use ASWebAuthenticationSession to make my initial request in step 2, like so:

var components = URLComponents()
components.scheme = "https"
components.host = "accounts.spotify.com"
components.path = "/authorize"
components.queryItems = [
    URLQueryItem(name: "client_id", value: SpotifyClientID),
    URLQueryItem(name: "response_type", value: "code"),
    URLQueryItem(name: "redirect_uri", value: SpotifyRedirectURL.absoluteString),
    URLQueryItem(name: "code_challenge_method", value: "S256"),
    URLQueryItem(name: "code_challenge", value: challenge),
    URLQueryItem(name: "state", value: "testing-state"),
    URLQueryItem(name: "scope", value: "user-follow-read")
]
let urlString = components.url!.absoluteString

guard let authURL = URL(string: urlString) else { return }
print(authURL)
let authSession = ASWebAuthenticationSession(url: authURL, callbackURLScheme: callbackScheme, completionHandler: handleLoginResponse)
authSession.presentationContextProvider = self
authSession.prefersEphemeralWebBrowserSession = true
authSession.start()

In handleLoginResponse, I parse the response in step 3 and make network request for step 4 using Alamofire:

guard let items = URLComponents(string: callbackURL?.absoluteString ?? "").queryItems else { return }
let authCode = items[0].value!
let endpoint = "https://accounts.spotify.com/api/token"

let headers = HTTPHeaders(["Content-Type": "application/x-www-form-urlencoded"])
let parameters: [String: String] = [
    "client_id": SpotifyClientID,
    "grant_type": "authorization_code",
    "code": authCode,
    "redirect_uri": SpotifyRedirectURL.absoluteString,
    "code_verifier": verifier!
]
AF.request(endpoint,
           method: .post,
           parameters: parameters,
           encoder: URLEncodedFormParameterEncoder.default,
           headers: headers
).cURLDescription() { description in
    print(description)
}
.responseJSON() { (json) in
    print(json)
}

Alamofire creates an interface to make cURL requests from within Swift, and calling cURLDescription() allows me to see exactly what the actual cURL command ends up being:

$ curl -v \
    -X POST \
    -b "__Host-device_id=AQBHyRKdulrPJU6vY5xlua1xKOZBtBZVcrW9IK-X0LQ_MPj5x3N4mZkF4OzgLMdQwviWUxJ2dY6d49d0QpjG0ayFtCfrhwzG5-g" \
    -H "User-Agent: SpotifyUserGraph/1.0 (hl999.SpotifyUserGraph; build:1; iOS 14.0.0) Alamofire/5.1.0" \
    -H "Accept-Encoding: br;q=1.0, gzip;q=0.9, deflate;q=0.8" \
    -H "Accept-Language: en-US;q=1.0, zh-Hans-US;q=0.9, ko-US;q=0.8" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=e11fb810282946569aab8f89e52f78d5&code=AQC3Lm3KDPFCg3mBjSAiXMyvjdn5GvUJCjjCTQzPhAFe5mLntAHcAeiEufXcCv3Jne2qn345MZxBNiCggO-35mn6AAFsjRlm5lPynyC6clWABSzBK1OdWIynTlf0CiyR8vWYeO54GHHEXBSzj6URKWnAiXuxTUV6n1Axra6Oet8FY6-0jwU0CNGMaB91q1JFXlyl5J9JvrRtrP3s2Ef8Xb5A7gcCzqW6RHRzO0--BKiPHFnprK0SitiLxi-md2aaMnS2aHsRTqvc_NfFcuRpFR05WmSm6Gvkk_9trSBqRvVZYuGs-Ap3-ydVGk7BCqNc3lpbh4Jku6W_930fOg9kI__zRA&code_verifier=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&grant_type=authorization_code&redirect_uri=hl999-spotifyusergraph%3A//spotify-login-callback" \
    "https://accounts.spotify.com/api/token"

It's a little bit difficult to read, but I'm pretty sure the request is made correctly.

However, on step 4, I always receive this error message from the server:

error = "invalid_grant";
"error_description" = "code_verifier was incorrect";

I've tried many things over the course of several hours and still can't figure it out. Any pointers would be very much appreciated. Thanks!

like image 803
dishanest Avatar asked Oct 23 '25 22:10

dishanest


2 Answers

Your issue is that the raw bytes of the SHA hash are what needs to be base64ed. I'm also working on an app that uses Alamofire and Spotify PKCE and had trouble with the code challenge. What I did was use some code from the Auth0 documentation that was written for Swift 3 and modified it to work with Swift 5:

import Foundation
import CommonCrypto
 
func challenge(verifier: String) -> String {
    
    guard let verifierData = verifier.data(using: String.Encoding.utf8) else { return "error" }
        var buffer = [UInt8](repeating: 0, count:Int(CC_SHA256_DIGEST_LENGTH))
 
        verifierData.withUnsafeBytes {
            CC_SHA256($0.baseAddress, CC_LONG(verifierData.count), &buffer)
        }
    let hash = Data(_: buffer)
    print(hash)
    let challenge = hash.base64EncodedData()
    return String(decoding: challenge, as: UTF8.self)
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")
        .trimmingCharacters(in: .whitespaces)
}
print(challenge(verifier: "ExampleVerifier"))

Hope this helps and good luck to you!

like image 100
Sn0w0d Dev Avatar answered Oct 25 '25 13:10

Sn0w0d Dev


A slightly different version than this other one, using CryptoKit or SwiftCrypto depending on the platform:

func base64URLEncode<S>(octets: S) -> String where S : Sequence, UInt8 == S.Element {
    let data = Data(octets)
    return data
        .base64EncodedString()
        .replacingOccurrences(of: "=", with: "")
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .trimmingCharacters(in: .whitespaces)
}

func challenge(for verifier: String) -> String {
    let challenge = verifier
        .data(using: .ascii)
        .map { SHA256.hash(data: $0) }
        .map { base64URLEncode(octets: $0) }

    if let challenge = challenge {
        return challenge
    } else {
        fatalError()
    }
}

The whole thing is detailed on this blog post.

like image 26
Dirty Henry Avatar answered Oct 25 '25 13:10

Dirty Henry



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!