truffls / sign-in-with-apple-using-django

Sign In with Apple and Server-Side verification
86 stars 15 forks source link

Receiving 403 Forbidden from Backend Redirect #5

Closed eytanschulman closed 4 years ago

eytanschulman commented 4 years ago

Hey, thanks for the tutorial, it's super instructive! I was wondering if you had an idea, or maybe came across this issue in development. In response to executing a POST request to /login/apple, I am receiving the following:

<html>\r\n<head><title>403 Forbidden</title></head>\r\n<body>\r\n<center><h1>403 Forbidden</h1></center>\r\n<hr><center>Apple</center>\r\n</body>\r\n</html>\r\n

Here is my backend AppleOAuth2 implementation:

class AppleOAuth2(BaseOAuth2):
    """
    Apple Authentication Backend
    """
    name = 'apple'
    ACCESS_TOKEN_URL = "https://appleid.apple.com/auth/token"
    AUTHORIZATION_URL = 'https://appleid.apple.com/auth/authorize'
    SCOPE_SEPARATOR = ','
    ID_KEY = 'uid'

    @handle_http_errors
    def do_auth(self, access_token, *args, **kwargs):
        """
        Finish the auth process once the access_token was retrieved.
        Get the email from ID token received from Apple.
        """
        response_data = {}
        client_id, client_secret = self.get_key_and_secret()

        headers = {'content-type': 'application/x-www-form-urlencoded'}
        data = {
            'client_id': client_id,
            'client_secret': client_secret,
            'code': access_token,
            'grant_type': 'authorization_code',
        }

        res = requests.post(AppleOAuth2.ACCESS_TOKEN_URL, data=data,
                            header=headers).json()
        id_token = res.get('id_token')
        if id_token:
            decoded = jwt.decode(id_token, '', verify=False)
            (response_data.update({'email': decoded['email']})
             if 'email' in decoded else None)
            (response_data.update({'uid': decoded['sub']})
             if 'sub' in decoded else None)

        response = kwargs.get('response') or {}
        response.update(response_data)
        (response.update({'access_token': access_token})
         if 'access_token' not in response else None)

        kwargs.update({'response': response, 'backend': self})
        return self.strategy.authenticate(*args, **kwargs)

    def get_user_details(self, response):
        """
        Gets the user details
        """
        return dict(email=response.get('email'))

    def get_key_and_secret(self):
        headers = {
            'kid': settings.SOCIAL_AUTH_APPLE_KEY_ID
        }

        payload = {
            'iss': settings.SOCIAL_AUTH_APPLE_TEAM_ID,
            'iat': timezone.now(),
            'exp': timezone.now() + timedelta(days=180),
            'aud': 'https://appleid.apple.com',
            'sub': settings.CLIENT_ID,
        }

        client_secret = jwt.encode(
            payload,
            settings.SOCIAL_AUTH_APPLE_PRIVATE_KEY,
            algorithm='ES256',
            headers=headers
        ).decode("utf-8")

        return settings.CLIENT_ID, client_secret

One difference may be that I am loading SOCIAL_AUTH_APPLE_PRIVATE_KEY like this:

SOCIAL_AUTH_APPLE_PRIVATE_KEY = (Path(os.path.join(BASE_DIR, 'AuthKey.p8')
                                      ).read_text())

On the iOS client side, this is the ViewController:

import UIKit
import AuthenticationServices

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        setupAppleIDButton()
    }

    func setupAppleIDButton() {

        // Instantiate the button with a type and style
        let signInButton = ASAuthorizationAppleIDButton(type: .signIn, style: .black)

        // Add an action to be called when tapping the button
        signInButton.addTarget(self, action: #selector(signInButtonPressed), for: .touchUpInside)

        // Add button to view and setup constraints
        signInButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(signInButton)
        NSLayoutConstraint.activate([
            signInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            signInButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            signInButton.widthAnchor.constraint(equalToConstant: 250),
            signInButton.heightAnchor.constraint(equalToConstant: 44)
        ])
    }

    @objc func signInButtonPressed() {
        // First you create an apple id provider request with the scope of full name and email
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.fullName, .email]

        // Instanstiate and configure the authorization controller
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.presentationContextProvider = self
        authorizationController.delegate = self

        // Perform the request
        authorizationController.performRequests()
    }

    func presentCouldNotAuthenticateAlert() {
        // Present alert
        print("Could not authenticate.")
    }

    func exchangeCode(_ code: String, handler: @escaping(String?, Error?) -> Void) {
        // Call your backend to exchange an API token with the code.
        let session = URLSession.shared
        let url = URL(string: "http://192.168.1.35:8000/login/apple/")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        let jsonData = try! JSONSerialization.data(withJSONObject: ["authorizationCode": code], options: [])
        let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
            if let data = data, let dataString = String(data: data, encoding: .utf8) {
                handler(dataString, error)
            }
        }
        task.resume()
    }

}

extension ViewController: ASAuthorizationControllerPresentationContextProviding {

    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return view.window!
    }
}

extension ViewController: ASAuthorizationControllerDelegate {

    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        let authError = ASAuthorizationError(_nsError: error as NSError)
        switch authError.code {
        case .canceled:
            break
        default:
            presentCouldNotAuthenticateAlert()
        }
    }

    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
            if let data = credential.authorizationCode, let code = String(data: data, encoding: .utf8) {
                // Now send the 'code' to your backend to get an API token.
                exchangeCode(code) { apiToken, error in
                    // Handle response
                    print("APIToken: \(String(describing: apiToken))")
                }
            } else {
                // Handle missing authorization code ...
                print("Missing code..")
            }
        }
    }
}

Would be happy to give additional details, thanks again!

aamishbaloch commented 4 years ago

@eytanschulman As far as I remember, we didn't come across this kind of issue in our development. Believing that your routes are working fine, I would say just to double check client_id, client_secret, code or grant_type is correct or not, because may be these params are the cause of your issue.

eytanschulman commented 4 years ago

@eytanschulman As far as I remember, we didn't come across this kind of issue in our development. Believing that your routes are working fine, I would say just to double check client_id, client_secret, code or grant_type is correct or not, because may be these params are the cause of your issue.

Thanks @aamishbaloch , I'll check them again. Did you see what I wrote above about how I am retrieving the client_secret?

aamishbaloch commented 4 years ago

@eytanschulman Yes, I have seen that, looking fine to me, but we cannot rule out anything. Sometime apple don't give us the exact error responses, but I'm sure issue lies in the params I mentioned above. Something is invalid, that you have to check.

eytanschulman commented 4 years ago

@eytanschulman Yes, I have seen that, looking fine to me, but we cannot rule out anything. Sometime apple don't give us the exact error responses, but I'm sure issue lies in the params I mentioned above. Something is invalid, that you have to check.

What do you mean exactly by code ? I just checked everything else, they all seem to be in check to me.. By the way, when debugging this in Python I do not even enter the functions within AppleOAuth2 before I get the response.

The Django logs a 302 Redirect, and then within the app I get back the 403 from Apple.

aamishbaloch commented 4 years ago

Code is the access_token from app.

aamishbaloch commented 4 years ago

Have you added AppleOAuth2 in AUTHENTICATION_BACKENDS?

eytanschulman commented 4 years ago

I just checked the access_token from the app, which I am sending as a parameter called authorizationCode. Just tried changing from authorizationCode to access_token, still didn't work 😄

Question: When you send the access_token from the app to your backend, are you sending it in the body as a JSON dict, like this?

var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        let jsonData = try! JSONSerialization.data(withJSONObject: ["authorizationCode": code], options: [])
        let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
            if let data = data, let dataString = String(data: data, encoding: .utf8) {
                handler(dataString, error)
            }
        }

Yes, I have the proper path to the AppleOAuth2 class in my AUTHENTICATION_BACKENDS. When debugging, I enter the do_auth func in social_django/views, and the backend variable is an instance of AppleOAuth2.

aamishbaloch commented 4 years ago

I have no idea about that but I guess you can find that here.

eytanschulman commented 4 years ago

Thanks, the function which does it is empty in the example 😄

func exchangeCode(_ code: String, handler: (String?, Error?) -> Void) {
        // Call your backend to exchange an API token with the code.
    }

ping @lukaswuerzburger

lukaswuerzburger commented 4 years ago

@eytanschulman sorry, I totally missed the ping. In our app we send the access_token as a url parameter, not in the payload. Keep us posted if it helped! Sorry again for the delay.

lukaswuerzburger commented 4 years ago

I'll close this ticket due to inactivity