Spotify PKCE authorization flow returns "code_verifier was incorrect"

Viewed 1164

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!

3 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!

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.

When I used this online PKCE verifier I got a different challenge for the verifier so I'd maybe retry with the values that tool gives you.

OVERALL SECURITY

I would use a respected security library such as AppAuth, so that you don't need to code the security yourself. Libraries help us avoid potential mistakes and we get new security features for free in future.

APPAUTH INTEGRATION

If interested in this approach, these steps and resources of mine might prove useful:

CODE

Here is some Swift code of mine that integrates the libraries, and there is no low level PKCE handling needed.

Related