Sign in with Apple

Thanks so much @vazul, following your advice I was able to get this working on Keycloak 8.0.1.

Has anyone figured out a clean way to create the user with their full name? Even when requesting the .fullName scope, that information does not appear to be exchanged with the ID token.

Hi @antonio!

Maybe keycloak version issue, we are still using version 7.0.1, there we left it empty, and is working.

Best regards,

– Vazul

1 Like

Hi @antonio!

Just got that NullPointerException… so yes, we have to fill in the Issuer in Apple ID IDP settings, otherwise we cannot do token exchange with facebook.
Keycloak iterates over the IDP list, checks the IDP Alias with the given subject_token_issuer in the token exchange request, OR with the Issuer. And if facebook IDP is before AppleID in the list, the facebook token exchange will fail with that NullPointerException in TokenEndpoint.exchangeExternalToken

This exception is raised in an environment, where we added AppleID to and already running Keycloak instance, and not in a demo env with realm import…

Best regards,

– Vazul

1 Like

Thank you @vazul ! I owe you one!

Hi, I followed your instructions but I get this error:

invalid_request: response_mode must be form_post when name or email scope is requested.

I’m trying to specify response_mode=form_post inside Forwarded Query Parameters, but is not added to the request. Has any of you encountered the same issue? I’m using Keycloak 7.0.1

Hey,
you have to change the Authorization URL to “https://appleid.apple.com/auth/authorize?response_mode=form_post”. Then you are able to request the email and name scopes.

But then we ran into another problem: The browser tries to post the form data to our keycloak via the authorization URL: https://myDomain/auth/realms/{realm}/broker/apple/endpoint. This results in 405 Method Not allowed error.

We checked the openid-configuration (/auth/realms/{realm-name}/.well-known/openid-configuration) and “form_post” was advertised as supported response mode.

What are we missing? Anybody have an idea?

Best regards and thanks

Apple Login on IOS 13 with Keycloak 9.0.0 via Token Exchange

I have collect and enriched all the information (from above props to @vazul). Maby this will help someone.

We will request a JWT from Apple und use this token via token exchange as authentication.
Additionally we need a backend component do validate if this Token is issued for the correct app.

iOS App

Create a App and receive a JWT from Apple:


    let appleIDProvider = ASAuthorizationAppleIDProvider()
    let request = appleIDProvider.createRequest()
    request.requestedScopes = [.email, .fullName]


    extension LoginViewController: ASAuthorizationControllerDelegate {
        
        func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
            guard let credentials = authorization.credential as? ASAuthorizationAppleIDCredential else {
                print("Could not cast credentials to ASAuthorizationAppleIDCredential.")
                return
            }
            // Send credentials.identityToken to backend for verification
        }
        
        func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
            guard let authError = error as? ASAuthorizationError else {
                print(error.localizedDescription)
                return
            }
            
            // Handle Error       
            // Cancelling will also result in an error
        }

        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
                
        // This is not required in all setups
        authorizationController.presentationContextProvider = self
              
        authorizationController.delegate = self
        authorizationController.performRequests()
    }

Configure identity provider

Since we dont request a JWT from apple but only validate if the JWT send by the APP is valid, we do not need most information

  1. Enable the token exchange feature in Keycloak. Just add "-Dkeycloak.profile.feature.token_exchange=enabled" and "-Dkeycloak.profile=preview" as start up parameter.
    For example overwrite the dockerimage cmd in a Dockerfile with

    CMD ["-b", "0.0.0.0" ,"-Dkeycloak.profile.feature.token_exchange=enabled","-Dkeycloak.profile=preview"]

  2. Create a “OpenId Connect v1.0” Identity Provide with the Following Configuration:

    • Fill a alias e.G. apple
    • Hide on Login Page (We only allow the login throw the app)
    • Fill the Authorization URL with a random URL e.g. https://example.com (we dont need it, but it does not to be blank)
    • Fill the Token URL with a random URL e.g. https://example.com (also here a value must be present but we do not need it)
    • Select a arbitrary Client Authentication from the drop down e.g. Client secret sent as basic auth (must be set, but me do not need it)
    • Choose arbitrary client credentials e.g Client ID = hello and Client Secret = world (this is not needed but does not to be empty YOU DO NOT NEED A CRON JOB!!! )
    • Enable Validate Signatures
    • Keep Use JWKS URL enabled
    • Set https://appleid.apple.com/auth/keys as JWKS URL
    • Set https://appleid.apple.com as Issuer
  3. Create a Client for the App. It might be a public client lets say with the Client id ios

  4. Create a Client for the backend component the does the real token exchange

    • choose a Cient id e.g apple-client
    • set Access Type confidedntial
    • set a arbitrary redirect url (we do not need it during the token exchange) e.g. https://example.com
    • under Credential remember the secret
  5. set allow the Client for backend component to request tokens for the app Client

    • got the the apple Identity Provider
    • got to the Permissions tab
    • enable Permissions Enabled
    • select the appearing token-exchange permission
    • select the Create Policy dropdown and choose client
    • fill name e.g apple-client-permission
    • in the client dropdown choose apple-client
    • click save
    • select the new created Policy in the dropdown from the permission tab of the Identity Provider.
    • go to the App Client ios und under Permission also enable Permissions Enabled and add the new created Permission in the Apply Policy Dropdown

Set up a Backend component

We have a Spring Boot application within we have written the stuff.

Write a Controller:


    @Data
    public static class TokenRequestDTO {
        private String token;
    }

    @PostMapping
    @PreAuthorize("permitAll()")
    public Map<String, Object> exchange(@RequestBody TokenRequestDTO requestDTO) {
        return keycloakExchange.exchange(requestDTO);
    }

then validate the given token if the audience (aud) field in the body is a known der bundle identifier
(i use this library for decoding https://mvnrepository.com/artifact/com.auth0/java-jwt)

   private static final List<String> ALLOWED_AUDIENCES = Arrays.asList("a","b","c");
 ...
        DecodedJWT decode = JWT.decode(requestDTO.getToken());
        HashSet<String> strings = new HashSet<>(ALLOWED_AUDIENCES);
        strings.retainAll(decode.getAudience());
        if (strings.isEmpty()) {
            throw new UnauthorizedException("invalid aud");
        }

after the you can request from Keycloak the token and return it to your apps


        RestTemplate restClient = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange");
        map.add("subject_token_type", "urn:ietf:params:oauth:token-type:id_token");
        map.add("subject_issuer", "apple"); //identity provider id (see 2.1)
        map.add("subject_token", requestDTO.getToken());
        map.add("audience", "ios"); // the (public) client for the app (see 3)
        map.add("client_id", "apple-client"); // the client id for the backend component (see 4.1)
        map.add("client_secret", "abd"); // the client secret (see 4.3) 

        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);

        return restClient.exchange(keycloakConfig.getAuthServerUrl() + "/auth/realms/" + keycloakConfig.getRealm() + "/protocol/openid-connect/token",
                HttpMethod.POST,
                entity,
                new ParameterizedTypeReference<Map<String, Object>>() {
                }).getBody();

Remark

As @vazul mentioned, if you have multiple Identity Provider, keycloak will iterate through them and with bad luck one other will no filled issuer is the and you will get a Nullpointer. Just set in all other Identity Provider with not an issuer a blank String.

4 Likes

Thanks for your reply. It is very detailed and useful. There is one thing that is not clear to me. My login page will be hosted by keycloak and will offer the option to use you username and password or login with google, facebook, apple. If for apple I have to do the token exchange, that means that I cannot use keycloak login page. Or I have to create another page under my domain to redirect the user directly to apple to start the token exchange or to keycloak for the classic PKCE flow. And also, once the mobile app gets the access_token from the token exchange, will have to do the token exchange again in order to refresh it?

1 Like

Indeed, I have gone over to not support login with Apple over the Webapp, just because I am not able to retrieve the first/last name und e-mail. This is my revenge to apple to not having a user info endpoint :blush:

With a (sub) Page only for apple, that will do the PKCE flow after it received the JWT seems possible for me. But for that you have define valid redirect URLs and so on for apple.

You can of course login multiple time with a JWT from apple (You will revive a keycloak access).

Also, as far as I understand, with token exchange I will not be able to link an existing account with the apple account (if that was never linked before). Is that correct?

It seems you have a point. I will evaluate this.

For me this is a no go. I would rather implement the classic flow and ask my user to fill the update user account.
But at the same time I see that they have an open ticket for the native apple id provider https://issues.redhat.com/browse/KEYCLOAK-13171
I hope it’s gonna be released soon

1 Like

Hey @vazul I am relative new in keycloak,and i am trying to apply what you mention, but return always invalid_token. So i dont know if i need to create another “Client” in keycloak just to Apple.
Or if a just create a Secret in the wrong way.
My version of the keycloak is 9.

Same for me. The point should be add the login with Apple option to Keycloak login page (if the login is requested by a mobile app or a webapp it should make no difference).

@codemaker219 @vazul Thanks for this post and it really helped me. Though I have a small problem and that is I have to identify User Role when first time token get exchange.

I am using Custom User Storage Provider SPI based on this sample KeycloakCustomUserStorageProvider. As my business database is mysql and new user needs to be stored in mysql.

So I have debugged and found that this method called from UserRegistrationProvider interface.

@Override
public UserModel addUser(RealmModel realmModel, String username) {
    logger.info("Calling addUser (username: {})",username);
    return null;
}

But Problem is I cant find the way to identify about what role should be assigned. Because I have three types of users which can logged in by apple social login. Admins, Managers, Customers. For each this method called but I could not figure out how to identify the role?

I really grateful if you could hint me in right direction.

Work for me by step as bellow

  1. Create Apple Services Id as same as https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple
  2. Create Privider OpenID connect v1 with
    Select Client Authentication = Client secret send as Post
    Goodluck!!
1 Like

I also managed to make Sign-In With Apple work with Keycloak (v10.0.2). I did it without using the Token Exchange solution mentioned above, but using Apple as a “regular” OIDC identity provider, so I do have Apple as an option on my Keycloak Login screen. There still are some rough edges though as I’ll explain below. Here’s what I did to make it work:

  1. As others said, follow https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple to set up everything on Apple’s side.
    • Make sure you register your Domain and Keycloak’s redirect URI on the “Sign In With Apple” part of your Service ID. The redirect URI typically is: https://[domain]/auth/realms/[realm]/broker/apple/endpoint
  2. Generate your client’s secret. Apple uses a JWT as a client secret. This JWT must be signed with a private key downloaded from the Apple developer portal and renewed regularly. This one is a bit tricky, I did it manually for testing but in a production environment, this will have to be automated as Apple allows a max-validity of 6 months for that generated JWT. For now, you may follow the instructions from the Okta blog linked in Step 1 to generate your JWT.
  3. In Keycloak, create a new OIDC v1.0 Identity provider with the following configuration:
    • Alias: apple
    • Authorization URL: https://appleid.apple.com/auth/authorize
    • Token URL: https://appleid.apple.com/auth/token
    • Disable User Info: On
    • Client Authentication: Client secret sent as post
    • Client ID: [your Service ID, created at apple in step 1., usually something like com.yourcompany.app.auth]
    • Client Secret: [see step 2.]

This has been mentioned before in this thread, but I’d like to emphasise:

What I’d actually want is to specify name email as additional scopes, so that Keycloak could automatically use those when creating the new user account during first login. However, Apple forces the use of response_mode=form_post when those scopes are included. Keycloak’s endpoint, however, will throw a “Method not supported” if we add this query parameter to the authentication URL. So even though Keycloak seems to support response_mode=form_post when it is used as an identity provider by third party clients, it doesn’t seem to support it when it is acting as the client, talking to its upstream identity providers.

So the current trade-off is to require users to fill in their basic profile during first login (which Keycloak does anyway in its default first-broker-login-flow), but the user experience would be better if we could use Apple’s name and email scopes right away.

From a Keycloak point of view, the following improvements would be very-VERY-nice-to-have:

  • First and foremost: Implement response_mode=form_post for identity providers, as the lack of support for it actually negatively impacts the integration of Sign-In With Apple when using Keycloak.
  • Add Apple as dedicated identity provider. It will make the configuration easier and will even allow Keycloak to display the apple Icon in the login screen - yay! :wink:
  • When using that new Apple identity provider, let admins enter the private key (or upload the p8 file) received from Apple in OpenSSL format (just as they received it from Apple, without the need to generate anything else themselves). And set up a scheduler in Keycloak that periodically renews the client secret using that key. I would then disable the “Client Secret” password input field in the Admin Console so that it is clear that this one is managed by Keycloak.
3 Likes

It should be quite straight forward to support response_mode=form_post.

IMO, we could start from there and for now use the default OIDC compliant broker.

2 Likes

Hi, there is also an old ticket that seems to be relevant to the form_post issue KEYCLOAK-2153.

1 Like

A first draft is available here KEYCLOAK-13171