16

How do I structure a swift POST request to satisfy the Sign In With Apple token revocation requirements?

I am not sure what form-data, client_id, client_secret, token, or token_type_hint are supposed to be. I was able to implement Sign in With Apple to create a user, but very lost on the revocation part of this.

I am looking to perform this client-side with Swift, as that would be the most convenient. Firebase may be developing a solution built into their SDK, but not sure if that is a one-size fits all solution for developers using Firebase.

https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens#url

Edit: source of requirements https://developer.apple.com/support/offering-account-deletion-in-your-app

The following functions live in the same class (ViewModel). The first does my login/registration flow. Some of the code is related to Firebase flows and can be largely ignored, but you can see I grab the token string and nonce for client_secret. The second function resembles the POST request for token revocation (which gets called from a delete account function not shown). Has anyone had success with this approach/boilerplate?

Testing the token revocation method below with a button tap in my app returns status code 400. I cannot revoke tokens with this method, and I am not sure what else to do.

public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    // Sign in using Firebase Auth
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
        guard let nonce = currentNonce else {
            print("Invalid state: A login callback was received, but no login request was sent.")
            return
        }
        
        // JWT
        guard let appleIDToken = appleIDCredential.identityToken else {
            print("Unable to fetch identity token")
            return
        }
        
        guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data")
            return
        }
        
        let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
        Auth.auth().signIn(with: credential) { result, error in
            if error != nil {
                print(error!.localizedDescription)
                return
            }
            else { // successful auth, we can now check if its a login or a registration
                guard let user = Auth.auth().currentUser else {
                    print("No user was found.")
                    return
                }
                let db = Firestore.firestore()
                let docRef = db.collection("Users").document(("\(user.uid)"))
                docRef.getDocument{ (document, error) in
                    if let document = document, document.exists {
                        // User is just logging in, their db store exists
                        print("Successful Apple login.")
                        
                        // Token revocation requirements
                        self.clientSecret = nonce
                        self.appleToken = idTokenString
                        
                        if (self.isDeletingAccount == true) {
                            print("Is deleting account.")
                            self.isReauthenticated = true
                        }
                        self.isLogged = true
                    }
                    else { // document does not exist! we are registering a new user
                        db.collection("Users").document("\(user.uid)").setData([
                            "name": "\(appleIDCredential.fullName?.givenName ?? "")"
                        ])
                        print("Successful Apple registration.")
                        
                        self.clientSecret = nonce
                        self.appleToken = idTokenString
                        
                        self.isLogged = true
                    }
                }
            }
        }
    }
}


// POST request to revoke user's Apple token 
func appleAuthTokenRevoke(completion: (([String: Any]?, Error?) -> Void)? = nil) {
    
    let paramString: [String : Any] = [
        "client_id": Bundle.main.bundleIdentifier!, //"com.MyCompany.Name",
        "client_secret": self.clientSecret,
        "token": self.appleToken,
        "token_type_hint": "access_token"
    ]
    
    let url = URL(string: "https://appleid.apple.com/auth/revoke")!
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    
    do {
        request.httpBody = try JSONSerialization.data(withJSONObject: paramString, options: .prettyPrinted)
    }
    catch let error {
        print(error.localizedDescription)
        completion?(nil, error)
    }
    
    request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
    
    let task =  URLSession.shared.dataTask(with: request as URLRequest)  { (data, response, error) in
        guard let response = response as? HTTPURLResponse, error == nil else {
            print("error", error ?? URLError(.badServerResponse))
            return
        }
        
        guard (200 ... 299) ~= response.statusCode else {
            print("statusCode should be 2xx, but is \(response.statusCode)")
            print("response = \(response)")
            return
        }
        
        
        if let error = error {
            print(error)
        }
        else {
            print("deleted accont")
        }
    }
    task.resume()
}
Andre
  • 562
  • 2
  • 7
  • 18

3 Answers3

6

Note, I am not familiar with swift, please forgive me that I cannot post a good answer with swift. I found some guys are not clear about the parameters of revoke token API. Hope it will help someone who is not clear about those parameters.


The three required values ​​are required for appleid.apple.com/auth/revoke.

  • client_id: This is the App ID you can find in Apple Developer's Identifiers. Team ID is an excluded identifier.

  • client_secret: A secret JSON Web Token (JWT) that uses the Sign in with Apple private key associated with your developer account. You need to create it using JWT and download a key file from developer.apple.com/account/resources/authkeys/list

  • token: A token that requires revoke. The token is access_token or refresh_token returned from auth/token.

As for the auth/token, there are two additional parameters as below

  • code: The authorization code received in an authorization response sent to your app. The code is single-use only and valid for five minutes. Authorization code validation requests require this parameter. It is the same to the authorizationCode key of the response of apple signing, and its type is base64 (like XXXXXXXXXXXXXXXXXXX). It should be decoded to utf-8 (like xxxxxxxxxxxx.0.zzzz.YYYYYYYYYYYYYYYYYYY) before assigning to auth/token API.
  • grant_type: (Required) The grant type determines how the client app interacts with the validation server. Authorization code and refresh token validation requests require this parameter. For authorization code validation, use authorization_code. For refresh token validation requests, use refresh_token.

Finally, we could call revoke token api (appleid.apple.com/auth/revoke) successfully, and the apple id binding information is deleted under Apps Using Apple ID of Settings. And the Node.js sample could be found here


Summary the whole process as below.

  • Get authorizationCode from Apple login.
  • Get a refresh token \ access token with no expiry time using authorizationCode through auth\token
  • Revoke the refresh token or access token through token\revoke
zangw
  • 43,869
  • 19
  • 177
  • 214
  • `code: It is the same as the authorization_code key of the response of apple signing, and its type is base64. It should be decoded to utf-8 before assigning to auth/token API.` *It worked for me without converting to utf-8* – Youhana Sheriff Dec 07 '22 at 15:39
  • @YouhanaSheriff, the base64 type of `code` is like `XXXXXXXXXXXX`, whereas, the `utf-8` type of `code` is like `xxxxxxxxxxxx.0.zzzz.YYYYYYYYYYYYYYYYYYY` . Which type of `code` is in your case? – zangw Dec 08 '22 at 05:43
  • AuthorizationCode leaves only 5 minutes, and can be used only once. Otherwise, I'm getting 400 error. I haven't found this information anywhere except apple official documentation https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens – Yura Babiy Mar 05 '23 at 21:09
  • @YuraBabiy, Yes, if the Auth code was used over 5 minutes, the 400 error would come up. – zangw Mar 06 '23 at 03:07
  • 1
    @zangw Thanks. Do you know why apple sends 200 OK on revoke api call, but app is not getting removed under settings? – Yura Babiy Mar 06 '23 at 21:38
  • @YuraBabiy, you may find some clues from this question https://stackoverflow.com/questions/72556424/how-to-validate-the-apple-sign-in-revoke-token-api-successfully-when-handle-dele?noredirect=1&lq=1. – zangw Mar 07 '23 at 02:43
  • @YuraBabiy, you may double check the `client_secret` and `token` is correct. – zangw Mar 07 '23 at 02:45
  • I found the cause of my issue. I was sending raw response string from refresh token API, instead of just getting the token. Stupid thing, but was hard for me to find ) – Yura Babiy Mar 07 '23 at 13:14
6

For any one who wants to implement account deletion with flutter , swift or react .

Before you continue reading , take a moment to read the api requirement of two urls you will need to complete the revoking process.

The link below shows how to do this with iOS(swift) and firebase using firebase functions .And by reading it you can implement a solution with the other SDKs i.e flutter , react-native etc.

Implementing account deletion with apple and firebase

But in an attempt to summarise :-

  1. When you go through the process of setting up Sign In with Apple you will need to create a key in your apple developer account that comes in a file form which can be downloaded and should be download on to your device and looks like "*********.p8" where the * stands for a bunch of numbers and words usually representing the key.This apple refers to as the private associated key.

2.This you will use to create a Jwt using popular libraries of your own choosing .which then becomes the client_secret parameter for apple's api end point .

  1. When you then sign in a user you get an authorisation code(NB:Is the code parameter for token generation endpoint) which you will then make an api call endpoint to generate a token which then become the token parameter for the revoke endpoint .

4.Finally you call the revoke endpoint with the parameters from the above processes .

NB: I believe the solution above is so far the best way assuming that you secure your key on the server side. and unless you have a way of scrambling the key you should not attempt to store this in your project(or app).

Agyakwalf
  • 1,674
  • 1
  • 5
  • 8
  • AuthorizationCode leaves only 5 minutes, and can be used only once. Otherwise, I'm getting 400 error. I haven't found this information anywhere except apple official documentation https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens – Yura Babiy Mar 05 '23 at 21:09
-1

The .P8 File is here: https://developer.apple.com/account/resources/authkeys/list

For SwiftJWT, it's here: https://github.com/Kitura/Swift-JWT

    import SwiftJWT
    func makeJWT() -> String{
        let myHeader = Header(kid: "YOUR KEY ID")
        struct MyClaims: Claims {
            let iss: String
            let iat: Int
            let exp: Int
            let aud: String
            let sub: String
        }
        
        let nowDate = Date()
        var dateComponent = DateComponents()
        dateComponent.month = 6
        let sixDate = Calendar.current.date(byAdding: dateComponent, to: nowDate) ?? Date()
        let iat = Int(Date().timeIntervalSince1970)
        let exp = Int(sixDate.timeIntervalSince1970)
        let myClaims = MyClaims(iss: "YOUR TEAM ID",
                                iat: iat,
                                exp: exp,
                                aud: "https://appleid.apple.com",
                                sub: "YOUR CLIENT ID")
        
        var myJWT = JWT(header: myHeader, claims: myClaims)
        guard let url = Bundle.main.url(forResource: "CustFiles", withExtension: "bundle") else{
            return ""
        }
        let bundle = Bundle(url: url)
        let path = bundle?.path(forResource: "YOUR AUTHKEY_FILE", ofType: "p8")
        let privateKeyPath = URL(fileURLWithPath: path ?? "")
        let privateKey: Data = try! Data(contentsOf: privateKeyPath, options: .alwaysMapped)
        let jwtSigner = JWTSigner.es256(privateKey: privateKey)
        let signedJWT = try! myJWT.sign(using: jwtSigner)
        return signedJWT
    }

Add removeAppleAccount Function

      func removeAppleAccount() async throws{
        guard let refreshToken = UserDefaults.standard.string(forKey: UserDefaultKeys.LoginInfo.refreshToken)else{
            return
        }
        let param = "token=\(refreshToken)&client_id=YOUR CLIENT ID&client_secret=\(makeJWT())&token_type_hint=refresh_token".data(using: .utf8)!
        var request : URLRequest! = URLRequest(url: URL(string: "https://appleid.apple.com/auth/revoke")!)
        request.cachePolicy = .reloadIgnoringLocalCacheData
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField:"Content-Type")
        request.httpMethod = "POST"
        request.httpBody = param
        let (_ , response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse,httpResponse.statusCode == 200 else {
            throw FetchAppleTokenError.invalidServerResponse
        }
        UserDefaults.standard.removeObject(forKey: UserDefaultKeys.LoginInfo.refreshToken)
        
    }

Add getRefreshToken Function in ASAuthorizationControllerDelegate -> authorizationController

    import SwiftyJSON
    func getRefreshToken(authorizationCode: Data) async throws{
        guard let codeString = String(data: authorizationCode, encoding: .utf8) else{
            return
        }
        let param = "code=\(codeString)&client_id=YOUR CLIENT ID&client_secret=\(makeJWT())&grant_type=authorization_code".data(using: .utf8)!
        var request : URLRequest! = URLRequest(url: URL(string: "https://appleid.apple.com/auth/token")!)
        request.cachePolicy = .reloadIgnoringLocalCacheData
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField:"Content-Type")
        request.httpMethod = "POST"
        request.httpBody = param
        let (data, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse,httpResponse.statusCode == 200 else {
            throw FetchAppleTokenError.invalidServerResponse
        }
        let json: JSON = try! JSON(data: data)
        let refreshToken = json["refresh_token"].string ?? ""
        UserDefaults.standard.set(refreshToken, forKey: UserDefaultKeys.LoginInfo.refreshToken)
    }
    enum FetchAppleTokenError: Error, LocalizedError {
            case invalidServerResponse
    }