Apple Sign-In: Custom Servers and an Expiry Conundrum

Should we adopt Apple sign-in for server-side use?

Christopher G. Prince
Better Programming

--

Photo by Medhat Dawoud on Unsplash

[July 2020: See also Apple Sign-In: Custom Servers and an Expiry Conundrum (Part 2)]

Well, I’m making good headway on integrating Apple sign-in with my app, but it hasn’t been easy. And I still have a question — which seems like a show stopper.

My purpose for writing this article is to share my learnings and hopefully get some feedback. I kind of feel like I’m out in the wilderness despite my belief that my use case is not that unusual.

I want to use the OAuth tokens that result from Apple sign-in to access the HTTP REST API on my server.

First, let me elaborate on my system from a sign-in perspective. My system is perhaps a bit unusual in that it has no native sign-in type. It allows sign-ins with a variety of social sign-ins and cloud service providers: Dropbox, Google, and Facebook.

I’ve recently integrated Microsoft (OneDrive) server-side and am working on Apple sign-in. The focus is actually on cloud service providers. My app (Neebla on the Apple App Store, and SyncServerII on the back end) has a BYOCS — “Bring Your Own Cloud Storage” concept.

Data uploaded by users is generally stored in their own cloud storage system and that data can be safely shared with other users. Social sign-ins (including Apple sign-in) occur by invitation only by a user with cloud storage — and a social user’s data storage is delegated to the inviting user’s storage.

An example of a typical sign-in and REST API usage on Neebla and SyncServerII is Google sign-in. Google provides a client-side iOS framework. The user signs in with their Google username and credentials and several things result from this:

  1. OAuth tokens — an authorization code and access tokens.
  2. The access token gets automatically refreshed client-side by the sign-in framework.
  3. These tokens can be sent to my custom server and the access token is easily validated, using Google endpoints.
  4. Initial token verification on the custom server can take place early in endpoint request processing, without access to the custom server’s database (as in the style here).

I mention these points to create a framework of expectations for what I’d like to see in other sign-in providers (not because I’m a die-hard Google fan).

Now, I’m going to move on to Apple sign-in and describe how this process works from an implementation perspective. I’ll consider this in three steps, which correspond to the mechanisms in my system.

1. Client-Side Sign-In

My experience of integrating Apple Sign In in my iOS client app was pretty much as described in the Apple developer video.

With a couple of exceptions. I used their demo app. The two problems I encountered seem to have to do with the iOS simulator, at least as of Xcode 11.1.

The account is not retained from launch to launch of the app (e.g., using their getCredentialState method), and the app locks up fairly easily.

The first issue is mostly an annoyance. You just have to keep signing in. To fix the second issue, I either switch to a different device simulator or I sign out of my Apple account in the settings app.

I’m currently using the results of the sign in as inputs to my further development. Specifically, I’m using the five items resulting below.

I’ll focus on three of these. The user property provides a unique key to identify the user, to be utilized rather than the email address. The authorizationCode and identityToken are for OAuth.

The authorizationCode can be passed to your server to create a refresh token. More on this follows in part 3 below. The identityToken (or just ID token) is a JWT (JSON web token) that expires in 10 minutes. You can also pass this to your server.

This token has been a source of continuing frustration for me, so I’m going to say a few more things.

The ID token is not refreshed in the iOS app. For example, see this thread on the Apple developer forum. This goes against my typical expectations for an OAuth sign-in mechanism.

Further, it does not seem possible to create another ID token, server-side, that extends the expiry date of the ID token. Specifically, the refresh token (again, see 3 below) can be used for validation, not for creating a new ID token. See the Apple docs.

The ID token, it turns out, is only created and provided to your app when the user signs in. So, basically expect to get it only once.

This starts the custom server REST API conundrum.

What should be sent to a custom server when an iOS app is using Apple sign-in to authenticate with the server?

In the reference case I’m considering, Google sign-in, I send the OAuth access token. Its expiry date updates periodically as the Google sign-in framework updates the access token periodically on the iOS app.

Seemingly, there are two tokens we could use for sending from the client iOS app to the server:

  1. The ID token.
  2. The refresh token (sorry, keep reading).

I’ve outlined some considerations with these strategies here. One further consideration with the refresh token is that some people think it should not be given to clients — e.g., MSAL for iOS doesn’t allow the iOS client access to a refresh token.

Generally, refresh tokens are powerful, and a rule of thumb is that clients are not to be trusted.

For these reasons, I chose to send the ID token to my server for endpoint authentication. I will be persisting this token in the Keychain on my client. I also send the authorization code to my server, but don’t persist this on the client (e.g., it never gets sent back to the client).

2. Server-Side Initial Credentials Verification

My custom server, written in Swift, uses IBM’s Kitura Credential framework of plugins to do initial authorization for each endpoint request.

For example, for Google, this checks to see if the user (represented via an OAuth access token) is a Google user. It does not check to see if the user has an account on the system.

I’m creating an Apple sign-in Kitura Credentials plugin. This plugin uses the ID token to do initial authorization for a user. It does not check the expiry of the ID token — for reasons as stated above.

Instead, it assumes that additional validation will take place on the server (see section 3, below).

Checking the validity of an ID token is not exactly easy. Apple didn’t talk about it in the two WWDC 2019 videos I’ve seen. And their online docs are sparse.

A few early adopters have blogged about this though, and that helps. For example, see Curtis Herbert and Okta.

The code for my Apple sign-in Kitura Credentials plugin is perhaps the best I can offer for checking the validity of an ID token, but I’ll give an outline of the steps you need to follow to summarize:

  1. Get Apple’s JSON web key (JWK), via an HTTP endpoint request to Apple.
  2. Reconstitute that JWK into a PEM public key.
  3. Verify the signature of the ID token using that public key (remember, the ID token is a JWT).
  4. Decode the payload of the ID token.
  5. Validate, excluding the expiry date, the claims of the payload.

I’m using two frameworks to do the heavy lifting for the above steps:

I’m leaving you to dig into my code for details. I give some more web links there too.

One further detail I’ll give here. These steps do not check with Apple to see if your app is currently authorized with this user. More specifically, the only HTTP/network call above is in step 1, which is a simple HTTP GET where you provide no information to Apple.

3. Server-Side Final Verification

OK, you are still reading. One more “step” to go. And it’s a doozy. Sorry.

What remains? Well, we did initial server-side verification of the user in step 2 above. However, we haven’t actually checked with Apple to see if this user is currently valid. (And you need to check if the user is actually a user in your system, but that’s out of scope of this blog).

Again, Apple provides no videos and sparse documentation on this most tricky aspect of Apple sign-in if you are doing final verification for a custom server.

The overall strategy I’m using to do this final credentials verification is to periodically use the refresh token to do validation.

This passes information about the user and the app to an Apple endpoint in an HTTP POST call and I’m assuming it will fail should the user have revoked permissions for the app. By periodically, I’m doing this no more than once every 24 hours — since Apple states they may throttle you if you do it more than this often.

A refresh token is created via the authorization code and some work on your part. In my system, the authorization code gets passed up to my server from the iOS app when the user creates an account.

It turns out that the program code to (a) create the refresh token, and (b) use the refresh token to do validation is very similar. I’ll give the main steps below.

I’m again going to refer you to my program code for the full-blown details of how to exchange an authorization code for a refresh token. This time, refer to my SyncServerII repo and the AppleSignInCreds+Refresh.swift and AppleSignInCreds+ClientSecret.swift files specifically.

There are a set of parameters you will need to create the refresh token and to validate the refresh token. This is the struct on my server that holds these parameters:

The bottom four (keyId, teamId, privateKey, and clientId) are the main parameters you need to generate the client secret. In other systems (e.g., Google sign-in), a client secret for OAuth is simply a string that you copy and paste.

For Apple sign-in, the client secret is more complicated. It is actually a JWT that you have to create and sign.

The teamId comes from the Apple Developer website (see the next figure). The clientId is your bundle ID or app ID for your iOS app, e.g. biz.SpasticMuffin.SharedImages.

To get the keyId and privateKey, you have to go through steps on the Apple Developer website. Here is the URL to do this currently and it looks like:

This is also how you download the private key. Keep the private key in a private place! For my server, this goes in a private JSON server configuration file.

Once you have these parameters, you have to use them in a JWT header and payload and sign that to create a JWT string. For details, see the file I reference above — AppleSignInCreds+ClientSecret.swift. Again, I’m using Swift JWT for the heavy lifting.

Now you have the JWT string, i.e., the client secret. You also need a redirect URI. For this it seems you need your own TLS secured (HTTPS) web domain. Yark. I told you this last step was a doozy.

You set up this redirect URI using a Service Id on the Apple Developer web site. And, when you go there and tap on the big + icon, it looks like:

I found this step confusing. Not only do you have to put in a web domain (e.g., yourdomain.com) and a “Return URL” (e.g., webddomain.com/callback) — and I’m still not sure what these get used for — and upload a special file to a special place on your web server, you also have to give an “Identifier”:

Which has some poorly stated constraints/properties. As far as I can tell, it cannot be the same as your iOS app ID. So, I used biz.SpasticMuffin.SharedImages.AppleSignIn, which seems to be working so far.

Remember, the main point of this services ID exercise is to get a redirect URL, which you need to create the refresh token. Remember the refresh token?

Given all of the above, you ought to be able to put it all together and create the refresh token from the authorization code. See the method generateRefreshToken in the file I reference above — AppleSignInCreds+Refresh.swift.

Deep breath. All going well, now you have a refresh token. In my server, I save that to a user record in my server’s database and later, no more than every 24 hours, do a refresh token validation step.

This validation step is very similar to the process above to generate the refresh token. See my method validateRefreshToken in AppleSignInCreds+Refresh.swift.

In Closing

Well, either you read all of the above, or you just skimmed to the end. In any event, it’s not really pretty. Of the five OAuth-type sign-in systems I’ve incorporated into my server, this one has been the biggest struggle. And the one I’m the least satisfied with.

Why am I not satisfied? Let’s review using my starting point of the Google sign-in framework.

1. OAuth tokens — an authorization code and access tokens

We do get both of these with Apple sign-in. (And I blur a distinction, calling an ID token an access token, which is not quite correct.)

2. The access token gets automatically refreshed client-side by the sign-in framework

We do not get this. And in fact, we apparently cannot get an updated ID token.

This is my biggest single issue. I have no really good initial, per HTTP request, means to validate a user in my server REST API — because I seem to have to ignore the ID token expiry date.

3. These tokens can be sent to my custom server, and the access token is easily validated — using Google endpoints

I’ve been stating the difficulty here. The Apple recommended means to validate an ID token doesn’t use Apple endpoints. And they don’t talk about token expiry issues.

4. Initial token verification

Initial token verification on the custom server can take place early in the endpoint request processing — without access to the custom servers database (as in the style in Kitura Credentials).

Perhaps enough said. I’m not satisfied with the initial token verification.

I’m wondering if there is a security vulnerability where an attacker somehow gets the ID token for a user. Now they can perpetually get access to the server — because the token effectively doesn’t expire.

Hmmm. I think I need to use an additional strategy. Part of the work above, to create a client secret, involved signing my own JWT. What about sending back to the client, about every 24 hours, my own JWT with an expiry that can be checked? And throwing away Apple’s non-expiring ID token after the initial server interaction.

Hmmm. But no. That seems to open a bigger can of worms. An attacker that got the ID token might then just keep getting these additional or new JWT’s and keep having faked-valid access to the server.

It seems necessary to have the client able to send some ongoing, updated, credentials to the server. These ongoing, updated credentials could limit the damage done by an attack where someone obtained a single id token.

Summary

In summary, it may be too early to be adopting Apple sign-in for server-side use.

Their documentation for the main parts of server-side coding is sparse. And there seems to be a lack of support for a main server-side use case: That of securely authenticating to the REST API of a custom server.

So far, I can’t get around the conclusion that there ought to be a way of getting an updated ID token on the iOS client. Some guidance on these issues from Apple would be greatly appreciated.

Update (10/19/19)

I received a response from Apple Developer Technical Support. I asked:

My problem is that I can’t figure out a very good way to use Apple Sign In’s system in this manner [as described here]. My main issue is that the id tokens issued by Apple Sign In apparently cannot be refreshed — and they expire quickly (in 10 minutes).

Apple responded with:

I have reviewed your request and have concluded that there is no supported way to achieve the desired functionality given the currently shipping system configurations.

As this support person further advised, I’ll be making a request for a change via the Apple Feedback Assistant. Also, while I’ve not yet had this confirmed (I asked for confirmation in a follow-up question), I plan to use this as a reason to not yet support Apple Sign In in a next app release for Neebla.

Further Update (posted 4/4/20)

On 2/2/20, I had further email communication Apple Developer Technical Support (DTS). Unfortunately it has been of the nature of them just adding details to what I already know. The issue that I have has not yet been solved.

After the Apple DTS person went through his further response, adding details, I responded with this below (I’ve added some bolding that was not in my email to Apple).

Thanks for your email and continued support. However, I think we’re still not yet communicating fully about this issue.

I suspect what this boils down to are expectations about the use cases this kind (i.e., Apple Sign In) of OAuth-type system should support. I think Apple Sign In is supporting two main types of use cases:

(1) Initial connection from an iOS app (and perhaps other client apps) to a custom server when the user first signs in to the iOS client. The identityToken can be used for this because it should not have expired during the brief duration of this initial connection.

(2) At most once per day verification, on a custom server, of whether the particular user is still a valid user (“confirm that the user’s Apple ID on that device is still in good standing with Apple’s servers”). This can be done using the refresh token.

The use case that I expect to be present that I so far haven’t been able to resolve using Apple Sign In basically falls between (1) and (2) above in terms of timing. That use case is authenticating ongoing requests from the iOS client app to the custom server. These requests fall in the interval from after the identityToken has expired to the 24 hour mark. They also fall in the interval between each subsequent 24 hour period. Say for example, that the user revokes rights (e.g., via https://appleid.apple.com/account/manage) to access the app an hour after the identity token initially expires (or 25 hours after it expires, or 49 hours…). How can the custom server deal with this? It seems to me that leaving as much as 24 hour intervals between authentication possibilities on the server is a security problem. An attacker could have gotten access to the tokens used for authentication and get up to 24 hours worth of access to the server.

This for me is a typical main use case of request authentication on a custom server, and something I can do with other OAuth-type systems (e.g., Google Sign In, Facebook). I more typically expect that an access token can be refreshed client-side or some other mechanism is provided server side so that user validity can be checked more frequently (than once per 24 hours).

Final update (July 2020)

Apple is making an update to their Apple Sign In system. Perhaps they’ve been listening to folks like myself? :). See Apple Sign-In: Custom Servers and an Expiry Conundrum (Part 2).

--

--