django-allauth
django-allauth copied to clipboard
Apple SSO: switch between service ID and bundle ID as the client ID?
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?
Any chance you figured anything out here?
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 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
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')
}
}
}
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
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.
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.
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.
Yeah, I've got dj-rest-auth
as well. I don't know what's going on here 😄 I'm definitely missing something.
What is the error you're getting? Might be able to help if you post more details.
[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.
[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
isSERVICE_ID
and I want to login via mobile app, which should usecom.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.
Weird, still using the default client and adapter.
In any case, seems like your solution should work @yandiro.
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_id
s, 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 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
?
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?
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.
@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 passingcode
andid_token
to backend endpoint - Using
react-native-apple-authentication
on native and passingaccess_token
(coming asauthorizationCode
from library) andid_token
(coming asidentityToken
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.
Is the latest advice here working stably for everyone?
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 https://developer.apple.com/support/compare-memberships/ looks like it's required. Apple is a trash monopolist company sadly.
@aehlke Yes it is ! I made my research and figured I must enroll. Its fine.