AppleAuthentication & Expo App Public Key

Please provide the following:

  1. SDK Version: 35
  2. Platforms(Android/iOS/web/all): iOS

Was just implementing AppleAuthentication and have the entire flow working well. However I can’t test the validation of the JWT token because that would require the public key of the Apple ID Key.

Would it be possible for you to provide that for users testing using the Expo app? Basically you take the private p8 file that you would have downloaded when setting up your key in the apple console and you would run this:

openssl ec -in AuthKey_123ABC4567.p8 -pubout -out AuthKey_123ABC4567_Public.p8

That key can then be used to validate the identityToken with that on the server side.

Thoughts?

Also happy to help anyone who’s struggling through implementing it as I figure out a lot today.

Things I figured out so far:

  • You have to give the button a width and height in a style tag or it doesn’t show up
  • That openssl line above is how you generate your public key which is used to validate the token

Josh

1 Like

Hi - We’ll take a look at this. It should be secure to share the public key (after all, it’s public) but we may rotate the p8 key over time and without any warning since we might not be able to anticipate needing to rotate it, so it’s not something you should rely on in production services. Of course, you can rely on your own p8 key for your own standalone apps.

I investigated this some more and found that there are two different types of key pairs: one that you create under your Apple Developer account to sign requests to Apple, and one that Apple uses to sign authentication JWTs. You want the second’s public key in this scenario.

In short, use Apple’s own public key to verify “Sign In with Apple” JWTs. The public key is here: https://appleid.apple.com/auth/keys. There is a JWT debugger on the front page of https://jwt.io/ that lets you paste in a JWT and a key – copy & paste your JWT and just one of the keys (not the whole response) from Apple into that form. I tested it and verified the signature correctly. This has nothing specific to do with Expo so you can use whatever approach and tools you’d use otherwise.

1 Like

@ide

I’m so sorry. I had written this reply to myself but I somehow didn’t post it. Again, so sorry, but you are totally correct. I was able to validate with the Apple JWK.

Follow up to myself.

You don’t need the public key specifically from your Apple Key, you need Apple’s public key. They provide a JWK set (JSON Web Key Set) here: https://appleid.apple.com/auth/keys

You can use that to validate your payload.

1 Like

Hi, @theorytank. Sorry to hijack your thread but I’m completely blocked with Sign in with Apple at the moment and I’m really struggling to find some help.

Right now, I followed the Expo docs and set up what was needed in my Apple Developer account and when I try to sign in (with Apple), I get the expected credential (authorizationCode, name, email, identityToken, etc.). I sent this to my server and from there I want to validate the authorizationCode with Apple before signing up the user or signing them in.

In Apple’s docs, they mention using their auth endpoint but despite apparently creating the secret correctly, I still get a 400 Bad Request (“invalid_grant”) back from Apple. Is this how you’re validating the code? If not, could you please help me out as I’ve been stuck on this for a few days now. I know that Apple expect a redirect_uri when validating on that endpoint and I’ve seen some people using this when setting up Apple auth on a website but I haven’t seen it with an Expo app. Any advice would be welcome. Thanks. :slight_smile:

Apples docs aren’t very clear but you’re looking at “AppleID using OAuth” documentation which is only relevant when you use “Sign In With Apple” from a browser, not from a native app.

You’re doing everything right so far. Send the payload to your server as you’re doing. Then get the JWK (JSON Web Key) from this endpoint https://appleid.apple.com/auth/keys (probably best to cache the result for a while if you have high traffic, it really should never change, but it might).

Now you have everything you need to validate the original payload. No more REST calls required. Everything can be done locally from your server.

I’m not sure what you’re using on the server side as a platform but I was using PHP. I used this JWT package https://github.com/fproject/php-jwt/blob/master/src/JWK.php which seems to be a fork of google’s own JWT package https://github.com/firebase/php-jwt, with the added JWK class.

So, TL;DR; Use Apple’s JWK to validate your JWT payload on the server side using just those two piece of info. Useful google would be using a jwk to validate a jwt and include your language of choice.

My PHP Code is basically this.

	    // TODO: cache this
            $appleKey = file_get_contents('https://appleid.apple.com/auth/keys');

            $keys = JWK::parseKeySet($appleKey); // AIDOPK1
            
            if( count($keys) == 0 ) {
                TTLog::info('Invalid Apple Public Key');
                return false;
            }
            
	        $key = $keys[array_keys($keys)[0]];
            
            $jwt = $applePayload['identityToken'];

            $parts = explode(".", $jwt);

            if( count($parts) != 3 ) {
                TTLog::info('Invalid identityToken');
                return false;
            }

            $header = json_decode(base64_decode($parts[0]));
            TTLog::info("Header",$header); 
            
            $payload = json_decode(base64_decode($parts[1]));
            TTLog::info("Payload",$payload);

            try {
                $decoded = JWT::decode($jwt, $key, [$header->alg]);
                TTLog::info('Decoded',$decoded);
            } catch(Exception $e) {
                TTLog::info('Decode Exception',$e->getMessage());
                return false;
            }

            // Check to make sure aud matches either our own BundleID or Expo's
            if( !in_array($decoded->aud,Config::inst()->get(self::class,'validAuds')) ) {
                TTLog::info('Invalid AUD');
                return false;
            }
            
            if( ($decoded->sub == $payload->sub) && ($decoded->sub == $applePayload['user']) ) {
                // NOOP
            } else {
                TTLog::info('Invalid User/Sub');
                return false;
            }

            return true;

Josh

2 Likes

Thank you, Josh. That was really helpful.

What I found when trying it the other way – hitting Apple’s auth endpoint – is that the invalid_grant was very likely related to me testing in Expo as opposed to in a standalone build. That is, the app id was being sent as host.exp.Exponent as opposed to that of my app. I read somewhere that this is the case with Expo and to test properly you should push a build to TestFlight but I’m not ready to do that.

If you look in this article about Sign in with Apple on an Expo app (and you check the comments between myself and the author as we go back and forth about it a bit), he seems to have it working via the endpoint and he doesn’t do any of the manual checking using Apple’s Public Key. He mentions having to use TestFlight and that it definitely doesn’t work in the Expo app.

Regardless, I’ve also actually tried a way similar to the one you suggested (checking the token using Apple’s JWK) and when comparing the aud, it’s not matching my id (let’s call it .com.myApp.app); it’s actually getting host.exp.Exponent (which doesn’t match my id) so just like the first approach, it seems like this is related to running the app in Expo, right? I guess, for now, like you, I’ll also allow it to match host.exp.Exponent.

Thanks again for your comprehensive reply, by the way, and apologies for the delayed response. Have a nice day and week ahead.

I’ve used Josh’s implementation above - and Im able to get an email from the JWT which I then use to validate against the DB, I wanted to ask about caching the $keys response from apple.

At the moment I get something similar to this AIDOPK1 => resource id='17' type='OpenSSL key' is that expected?

Second if I saved that data to a database and used the database data instead of fetching from apple, I might run into a situation where users were using updated apple keys and the decoding would fail right - what would that look like what kind of error would I get and how could I simulate that?

further to the above, say the decoding failed, would I then use another tryCatch statement with the new details fetched from apple and persist those alongside my existing data?

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.