django-allauth icon indicating copy to clipboard operation
django-allauth copied to clipboard

Apple SSO: switch between service ID and bundle ID as the client ID?

Open lee-hodg opened this issue 4 years ago • 22 comments

For the Apple SSO, there are 2 Client IDs, a Bundle ID and a Services ID. When the flow is started from a mobile iOS device the app would use the Bundle ID as the Client ID, but when it is started from a web app such as react the authorization flow would be started with the Service ID as the Client ID.

How does the django-allauth Apple provider handle these 2 different Client ID?

There are some comments in the PR thread https://github.com/pennersr/django-allauth/pull/2424 that suggest setting up SOCIALPROVIDERS using a comma-delimited string of the 2 client IDs, like Client id = <APPLE_SERVICE_ID>, <APPLE_APP_ID> (https://github.com/pennersr/django-allauth/pull/2424#issuecomment-670597679) however looking at allauth/socialaccount/providers/apple/client.py shows that always the first client ID in the string would be used anyway

    def get_client_id(self):
        """ We support multiple client_ids, but use the first one for api calls """
        return self.consumer_key.split(",")[0]

This means if the Service ID is the first in the settings, then it would ALWAYS be the one used.

If iOS initiated the auth flow therefore with the Bundle ID as the client ID, then the backend (allauth) tries to use the Service ID, it will fail with invalid_id because of the mismatch. If however the Bundle ID was first in the settings string, then things would work for iOS, but would fail for flows started by web (react) because web would use the Service ID, but backend would use the Bundle ID.

By what mechanism is allauth intending to cope with these 2 disparate IDs?

Maybe the identity token from the auth response should be used to derive the sub (subject claim) so it always matches the requesting client id, rather than just taking the first client id in the settings list?

lee-hodg avatar Nov 18 '20 15:11 lee-hodg

Any chance you figured anything out here?

julianpacker avatar Jan 21 '22 19:01 julianpacker

For anyone still wondering: you need to have client ID for web listed first before the one for native.

Web always picks the first one, but for native all of them are checked against the aud field in the id_token.

blablacio avatar May 16 '22 09:05 blablacio

@blablacio What do you mean "native all of them are checked against the aud field in the id_token"? We have that issue in our project but the endpoint for web and in our case react-native apps is exactly the same. After signup/login by Apple we are sending request to Django server. And always there is an error of 2 different ID's because react-native is using a different one and web different as well.

I've changed that as you suggested, Web client ID is on first place: com.xxx.login, com.xxx. But now works only web, and mobile is crashing :/ If I change place of these ID's mobile is working, web is crashing :D

benrychter avatar May 16 '22 10:05 benrychter

Well, hard to explain why it is like that as it's hard to debug this locally, but it seems to me that the native call to the endpoint goes through get_verified_identity_data and get_client_id methods on the AppleOAuth2Adapter, while web call goes through get_client_id on the AppleOAuth2Client.

Don't quote me on that, that's just my gut feeling because get_verified_identity_data and get_client_id on the AppleOAuth2Adapter checks against all the client IDs, while get_client_id on the AppleOAuth2Client just takes the first client ID.

My current configuration:

SOCIALACCOUNT_PROVIDERS = {
    'apple': {
        'APP': {
            'client_id': 'com.app.web,com.app.native',
            'key': 'key',
            'secret': 'secret',
            'certificate_key': os.getenv('APPLE_LOGIN_CERTIFICATE')
        }
    }
}

blablacio avatar May 16 '22 11:05 blablacio

Hmm that's very interesting. But you have created only one endpoint for both platforms, right? Because I've got same configuration basically, and this is my endpoint view for apple login, and it's not working.

class AppleLoginView(SocialLoginView):
    adapter_class = AppleOAuth2Adapter
    client_class = AppleOAuth2Client
    serializer_class = SocialLoginSerializer

benrychter avatar May 16 '22 11:05 benrychter

I also have only one endpoint handling both web and native.

Here's the view:

class AppleLoginView(BaseLoginView):
    adapter_class = AppleOAuth2Adapter
    client_class = AppleOAuth2Client
    callback_url = f'https://{settings.APP_DOMAIN}/signup/'

I have some extra stuff on the BaseLoginView as well as the serializer defined there. I had to define callback_url though as I was getting errors without it.

blablacio avatar May 16 '22 11:05 blablacio

Ok, so maybe the case is your custom BaseLoginView or Serialiser. Because on that point, all is the same, and for me, it's not working.

benrychter avatar May 16 '22 14:05 benrychter

Not much going on in BaseLoginView -- just handling some custom data passed. It inherits from SocialLoginView and overrides post method:

    def post(self, request, *args, **kwargs):
        self.serializer = self.get_serializer(data=self.request.data)
        self.serializer.is_valid(raise_exception=True)

        # Login user
        self.login()

        return self.get_response()

as opposed to the original SocialLoginView->LoginView:post:

    def post(self, request, *args, **kwargs):
        self.request = request
        self.serializer = self.get_serializer(data=self.request.data)
        self.serializer.is_valid(raise_exception=True)

        self.login()
        return self.get_response()

By the way, I'm using dj-rest-auth, maybe that's also something worth mentioning.

blablacio avatar May 16 '22 14:05 blablacio

Yeah, I've got dj-rest-auth as well. I don't know what's going on here 😄 I'm definitely missing something.

benrychter avatar May 17 '22 04:05 benrychter

What is the error you're getting? Might be able to help if you post more details.

blablacio avatar May 17 '22 12:05 blablacio

[OAuth2Error]
Error retrieving access token: b'{"error":"invalid_grant","error_description":"client_id mismatch. The code was not issued to com.app.test.login."}'

That is the situation where com.app.test.login is SERVICE_ID and I want to login via mobile app, which should use com.app.test ID. Both of ID's looks like this in settings:

com.app.test.login, com.app.test

Web first, mobile second. If I reverse order, then I can login on mobile but not on web.

benrychter avatar May 18 '22 05:05 benrychter

[OAuth2Error]
Error retrieving access token: b'{"error":"invalid_grant","error_description":"client_id mismatch. The code was not issued to com.app.test.login."}'

That is the situation where com.app.test.login is SERVICE_ID and I want to login via mobile app, which should use com.app.test ID. Both of ID's looks like this in settings:

com.app.test.login, com.app.test

Web first, mobile second. If I reverse order, then I can login on mobile but not on web.

I (am) was facing the exact same issue! This is how I solved it:

I tried to override AppleOAuth2Adapter, but the get_client_id I override never gets called.

However, if I override the AppleOAuth2Client.get_client_id() instead, it gets called. This is how I did it

class CustomAppleOAuth2Client(AppleOAuth2Client):
    def get_client_id(self):
        mobile_client_id = os.environ.get('APPLE_CLIENT_ID').split(",")[1].strip()
        return mobile_client_id

class ApiAppleLoginView(SocialLoginView):
    adapter_class = AppleOAuth2Adapter
    client_class = CustomAppleOAuth2Client
    callback_url = f'{settings.APP_SITE_HOST}/accounts/apple/login/callback/' 

This way you can use both client_ids in your env file (or settings), just keep the web before the mobile, like you have already.

yandiro avatar May 25 '22 13:05 yandiro

Weird, still using the default client and adapter.

In any case, seems like your solution should work @yandiro.

blablacio avatar May 25 '22 14:05 blablacio

There is a new issue now :man_facepalming:, users that sign in with the mobile app are not recognised by the web app. I think it may be because they are using different client_ids, since the web app uses a service ID and the mobile app uses an App ID. How are you dealing with this, @blablacio ? Would you mind sharing?

Cheers!

yandiro avatar May 30 '22 23:05 yandiro

@yandiro That's very weird behavior. I have a very similar solution to yours - custom AppleOAuth2Client and AppleOAuth2Adapter. Everything is working great now.

class WebAppleOAuth2Client(AppleOAuth2Client):
    def get_client_id(self):
        return settings.APPLE_CLIENT_ID


class WebAppleOAuth2Adapter(AppleOAuth2Adapter):
    def get_client_id(self, provider):
        return settings.APPLE_CLIENT_ID

Are you sure that on developer.apple.com in Web Authentication Configuration you have selected the correct Primary App ID?

benrychter avatar May 31 '22 05:05 benrychter

Thanks, @brychter-merix!

There is only one issue remaining, when the user is created using the mobile app, the name doesn't seem to come. The mobile team say they are sending the correct scope, though.

So the remaining question is, what does my SocialLoginView Class expect to receive in the request? The mobile app is only sending this to the backend:

{
  "code": <authorizationCode returned by Apple>
}

Do they need to send the name too?

yandiro avatar May 31 '22 12:05 yandiro

On the mobile app, on the request, we are including beside code - idToken which is nonce variable coming from Apple Native response object. But we are using React Native and react-native-apple-authentication package for that.

benrychter avatar May 31 '22 12:05 benrychter

@yandiro and @brychter-merix I was thinking that the issue might be the data you supply from native and/or web.

So here's what we're doing:

  • Using react-apple-signin-auth on web and passing code and id_token to backend endpoint
  • Using react-native-apple-authentication on native and passing access_token (coming as authorizationCode from library) and id_token (coming as identityToken from library) to backend endpoint
  • Only notable change we had to make is list the web client ID first before the native one (no real need for overriding client/adapter/view classes I believe)

Hope that helps you to figure it out.

blablacio avatar May 31 '22 12:05 blablacio

Is the latest advice here working stably for everyone?

aehlke avatar Jul 16 '23 23:07 aehlke

Please guys... I have a question. Must I open a membership account at apple developer site to obtain client secret and all the necessary IDs to implement apple sign-in in my django api project ? They are asking me to pay the sum of $99 to able to register a developer account. Is there a way I can override this payment ?

JohnsonMasino avatar Jan 12 '24 09:01 JohnsonMasino

@JohnsonMasino https://developer.apple.com/support/compare-memberships/ looks like it's required. Apple is a trash monopolist company sadly.

aehlke avatar Jan 12 '24 16:01 aehlke

@aehlke Yes it is ! I made my research and figured I must enroll. Its fine.

JohnsonMasino avatar Jan 12 '24 16:01 JohnsonMasino