Sign in with Apple

I used microprofile with keycloak to manage authentication and authorization;
what is the best way to integrate “Sign in with Apple” as social provider for Keycloak?

4 Likes

If Apple if OIDC compliant (haven’t checked) it should be possible to simply setup the generic OIDC identity provider in Keycloak. If that doesn’t work then you would need to implement an Apple IdentityProvider using the proper SPI.

I’d like to cheer in on @matty’s request: Since Apple forces Apps to implement “Sign In with Apple” if they also offer an (OAuth-type) third party login, having an authentication solution that supports it might become critical for apps / app updates to be approved in the app store.

I’ve got to admit I’m only getting started with Keycloak and Sign-in-with-Apple, but a hurdle I’ve found is the following:

The client_secret must be a JWT token signed with a key you get from Apple. In Keycloak v8 there seems to be a mechanism for using this authentication (“JWT signed with private key”) but the help popover says “the realm private key is used”, which doesn’t fit the Apple use-case of an externally provided key for that particular identity provider.

Any input would be highly appreciated :slight_smile:

1 Like

Although Apple doesn’t make it very clear, their API is close enough to the OpenID Connect standard that Keycloak can use Sign In with Apple as an Identity Broker. There is a good guide here.

On the Keycloak side, a new OpenID Connect provider can be added (and the resulting Redirect URI should be used when registering your app in the Apple developer console).

The rest of the configuration options should be similar to the following…

Authorization URL: https://appleid.apple.com/auth/authorize
Token URL: https://appleid.apple.com/auth/token
Client ID: equivalent to the Service ID
Client Secret: generated using the private key obtained from Apple (has an expiry date)
Issuer: https://appleid.apple.com

For the record: How Sign In with Apple differs from OpenID Connect

2 Likes

Thanks for the clarification and links, @anthony-dj @jangaraj!

This JWT expires in 6 months, which is the maximum lifetime Apple will allow. If you’re generating a new client secret JWT every time a user authenticates, then you should use a much shorter expiration date.

While it looks like it would technically be possible to integrate Sign In With Apple as a generic OIDC identity provider, how practical is it to renew the client secret every 6 months or less? While this, in turn, might be automated, it seems like a workaround. The intended use seems to be generating a fresh JWT from the secret key for every sign in request. This is precisely the mechanism I am missing in Keycloak for a practical use of Sign In With Apple.

I checked the Keycloak code for AbstractOAuth2IdentityProvider.java and OIDCIdentityProvider.java and realized the user info url is mandatory for the token exchange. Anyone can confirm this? That is it true? Because in my scenario I will exchange a token.

@antonio I ended up with the same conclusion.
I’ve set up a custom oidc identity provider within keycloak and tried to exchange an apple token. Keycloak is always complaining about the user info: ‘user info service disabled’.

This log is triggered around line 596 in the OIDCIdentityProvider. That means the user info cannot be fetched because the endpoint has not been defined or because it has been disabled in the provider configuration tab.

What I’ve done:

I believe there is a way to tweak the OIDCIdentityProvider to handle the apple sign_in by using the ID_TOKEN from apple to get the user info instead of calling the apple missing endpoint. (cf. Auth0 tutorial where they provide a new endpoint to get the user profile)

Conclusion to this post :

  1. I think I’ve been far enough in this direction to say we can do it within keycloak, but it requires a whole dev setup.

  2. The other solution would be to create the user by decoding the ID_TOKEN and posting the user to the admin api rather than exchanging the token. And then to use the impersonation feature of keycloak to log the user in.

1 Like

Just adding a new third solution to the puzzle a more like a modification to you second solution:

Instead of using the impersonation feature, use token-exchange.
The received token can be refreshed.

@IvancoN Yes token-exchange is the solution we (at least @antonio and I) want to use.

But as described above :wink: the token exchange needs an endpoint (user info url) which is absolutely not exposed by Apple (see the links I’ve posted + here in the official open letter from the OIDC foundation https://bitbucket.org/openid/connect/src/default/How-Sign-in-with-Apple-differs-from-OpenID-Connect.md).

That’s why we’re stuck and it forces us to 1- use another way or 2- work within keycloak to handle apple specificities.

2 Likes

I’m fully aware of the missing Apple (user info url) endpoint
I am just merely improving on your proposed solution:

by replacing the “impersonation feature” part and going, I guess, full circle on the token-exchange.
You have all the pieces once you create the user through the admin api.
By creating the user, you will get the its userId that will represent the requested_subject request param value. By my opinion it is a much nicer and shorter way to get the wanted token along with a refresh token.

I completely agree with you, it is definitely much nicer to use the token-exchange, but it seems we can’t do it without applying modifications to keycloak’s odic identity providers.

If you think that today there is a way to perform a clean apple sign in using the token-exchange feature from keycloak, feel free to share it :slight_smile:
I haven’t found it yet.

By my understanding there is no clean way to do it… Until then we can use these workarounds.

Hi,

we have managed to use token-exchange with the received id_token from mobile.
(first of all, we are using Keycloak 7.0)

We had to create an oidc identity provider, set the “Validate Signatures” to On, set “Use JWKS URL” to On, and use “https://appleid.apple.com/auth/keys” for “JWKS URL”.

Then in the token exchange, we have to use the “subject_token_type” parameter: “subject_token_type=urn:ietf:params:oauth:token-type:id_token”.

In this way, Keycloak was happy to exchange to token.

However, the id_token should contain the email claim for the first time, to be able to register the user. Later it is not necessary.

Best regards,

– Vazul

3 Likes

Hi @vazul ! What did you put in “Client Authentication”, “Client ID” and “Client Secret”? Can you give an example of your configuration, please!

Thank you!

Hi @antonio !
This is how our login works with Apple ID and Keycloak 7.0.1:

  • Our iOS app uses ASAuthorizationAppleIDProvider, with email and fullName scopes:
            let appleIDProvider = ASAuthorizationAppleIDProvider()
            let request = appleIDProvider.createRequest()
            request.requestedScopes = [.email, .fullName]
  • For the first call only, the received identityToken of ASAuthorizationAppleIDCredential will contain a JWT, which will contain the email claim. This is important, without email in the id token, token exchange did not work for us (if the user does not exist in keycloak). (The name of the user is not in the id token, so it will be sent after the token exchange to our server). The mobile app will send the id token to our server side component.
  • The server side of our application is responsible to validate the audience in the id token, as token exchange is not safe as is, because it will accept any valid token. We need to accept only tokens for specific clients (aud claim in id token). If it is ok, the server side component will make the token exchange request to keycloak.
  • A client is defined for this purpose in keycloak, with client id and client secret authentication type. The token exchange will use Basic authentication with the defined client id and secret, and have these arguments:
grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token_type=urn:ietf:params:oauth:token-type:id_token&subject_issuer=<oidc_identity_provider_ name_for_apple_id>&subject_token=<id_token_from_mobile_app>&audience=<our_client_id_defined_in_keycloak_for_mobile>
  • In keycloak, an OpenID Connect v1.0 identity provider is defined as mentioned above: set the Validate Signatures to On, set Use JWKS URL to On, and use https://appleid.apple.com/auth/keys for JWKS URL, and set Disable User Info to On. For Client ID, we had to create a Services ID in Apple Developer portal. For Client Secret we had to generate a signed JWT with the method mentioned in https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens, using a private key defined in Apple Developer portal. We have created a cron job, that will generate a new client secret every month, and use keycloak api to set the client secret, as this JWT should be valid less then 6 month only.

So this is our configuration, and just checked it, still working…
One think can be interesting is that we have Email as username set to On in Realm settings…

If the user does not exist in keycloak, the user will be registered with the email defined in the id token sent from the mobile. If the user already registered, and the id token does not contain the email claim, the token exchange will be successful also. If the email exists in keycloak, but not linked to Apple ID identity provider, a user already exists error will be raised.

Best regards,

– Vazul

4 Likes

I’m deeply grateful! Thank you @vazul

One more thing, we have to start keycloak with these arguments:

-Dkeycloak.profile.feature.token_exchange=enabled -Dkeycloak.profile=preview
1 Like

that is ok! thank you so much @vazul !

I also needed to configure the issuer.