Sign in with Apple Tutorial, Part 3: Backend – Token verification
Table of Contents
The third part of a series Sign in with Apple. In this part, we will see how backend can use the token to sign up/sign in users.
- Sign in with Apple, Part 1: Apps
- Sign in with Apple, Part 2: Private Email Relay Service
- Sign in with Apple, Part 3: Backend – Token verification
- Sign in with Apple, Part 4: Web and Other Platforms
If you are an iOS developer, you shouldn't need to read this part. Backend folk should do all the hassle setting up users' accounts or sign them in for you. Still, if you are a solo developer who does everything yourself or just curious about what your colleagues are doing, this article might benefit you.
The Basic
Let's first talk about single sign-on flow. Most single sign-on flows are the same. It can sum up as follow:
Client
- Users directed to provider, e.g., Facebook, Twitter, Apple
- Users grant/deny permissions that application requested
- Provider direct users back to the application along with token
- The application then uses this token to sign in (or create an account)
Backend
- Use the token from the client to retrieve information from the provider, e.g., id, email
- Use that information sign in (or create an account if this is the first time)
That's it.
For Sign in with Apple, the backend steps are a little bit different but serve the same purpose. What you get after authenticate is JSON Web Tokens(JWT)[1]. It contains most of the data you need in the payload part, so you don't need to make another request for that information. The only thing we need to do is verify that the payload wasn't changed along the way.
Sign in with Apple flow looks something like this:
Client
- Users directed to Apple for Android and Web. Users presented with a sign-in dialog for iOS
- Users grant/deny permissions that application requested
- Apple directs users back to the application along with token. The delegate (
authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization)
) get called with JWT (.identityToken
) - Application send this JWT to the application server
Backend
- Verify JWT
- Use that information sign in (or create an account if this is the first time)
You can easily support sarunw.com by checking out this sponsor.
Debug 10x faster with Proxyman: Your ultimate tool to capture HTTPs requests/ responses, natively built for your iPhone and macOS. Special deal for Black Friday: Get 30% off for all Proxyman licenses with code “BLACKFRIDAY2024”.
How to verify the token
Before we use the token, we need to make sure that it was signed by Apple's private key. To do that, we need Apple's public key to verify the signature.
You can get the public key from the following endpoint:
GET https://appleid.apple.com/auth/keys
More information here Fetch Apple's public key for verifying token signature.
The response would look like this:
{
"keys": [
{
"kty": "RSA",
"kid": "AIDOPK1",
"use": "sig",
"alg": "RS256",
"n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w",
"e": "AQAB"
}
]
}
This might look alienate to you. Believe it or not, you can generate public key from this information. From Apple's document, this weird JSON is JSON Web Key Set (JWKS)[2], and its contain everything you need for generating a public key.
If successful, the HTTP status code is 200 (OK) and the JWKSet.Keys object contains Apple's public key.
It quite difficult to write specific detail on how to get the public key from this JWKS, but every framework and language should have one or two libraries for this job. What you need to do is Google something like, JWKS to RSA in [programming language] and JWKS to public key in [programming language] and grab the most famous one in your language of choice.
Once you found your library, you can skip to next section, but if you are interested to know what are these weird gibberish mean, I can share what I found while Googling with you.
What is JWKS
JWKS stand for JSON Web Key (JWK) Set. It is a set of keys containing the public keys that use to verify JWT. In this case, it is a set of one key (Apple might add a new one in the future, so don't hard code it).
How to select the right key from the set
If you decode JWT token that you got Apple (put it in https://jwt.io/), in the header you will see something like this:
{
"kid": "AIDOPK1",
"alg": "RS256"
}
kid
is what you needed to identify the key.
From the spec
4.1.4. "kid" (Key ID) Header Parameter
The "kid" (key ID) Header Parameter is a hint indicating which key was used to secure the JWS. This parameter allows originators to explicitly signal a change of key to recipients. The structure of the "kid" value is unspecified. Its value MUST be a case-sensitive string. Use of this Header Parameter is OPTIONAL.When used with a JWK, the "kid" value is used to match a JWK "kid" parameter value.
You can see that both the JWT and JWKS got the same kid
, AIDOPK1
.
Now that you know which key to use, the next question is how to get the public key from this JSON.
Retrieve key from JWK
The public key of the RSA algorithm is made of the modulus (n
) and the exponent (e
)[3].
So, what we interest here are n
and e
value. We use these two numbers to generate a public key using any OpenSSL-backed libraries out there.
Some libraries might be able to pass string as-is for n
and e
, some might need an integer. To convert these string into an integer, you need to know what format it is represented.
n
and e
are Base64URL-encoded[4] Big Endian byte array representations of numbers.[5].
As an example, I will show you how to decode e
(AQAB
) into a number.
Base64URL
Base64URL is Base64 with differs in the following:
- Replaces "+" by "-" (minus)
- Replaces "/" by "_" (underline)
- Does not require a padding character
- Forbids line separators
Here is a sample code of how to do it:
extension String {
static func base64urlToBase64(_ base64url: String) -> String {
var base64 = base64url
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
if base64.count % 4 != 0 {
base64.append(String(repeating: "=", count: 4 - base64.count % 4))
}
return base64
}
}
AQAB
is the same in both Base64URL and Base64 format.
Base64 to decimal
Each Base64[4:1] digit represents 6 bits of data. AQAB
contains four digits (of 6 bits, a total of 24 bits) which can represent with three bytes (a total of 24 bits).
A | Q | A | B |
---|---|---|---|
000000 | 010000 | 000000 | 000001 |
Convert to a three-byte array.
[00000001, 00000000, 00000001]
In Swift, you can convert it with following code:
let base64 = String.base64urlToBase64("AQAB")
let data = Data(base64Encoded: base64)!
print(data)
Output of the data will be the following:
po data
▿ 3 bytes
- count : 3
▿ pointer : 0x00007ffee8250e30
- pointerValue : 140732793163312
▿ bytes : 3 elements
- 0 : 1
- 1 : 0
- 2 : 1
People usually present this in the form of a hexadecimal string, since that aligns at the byte boundaries
three bytes array [00000001, 00000000, 00000001]
Becomes [0000, 0001, 0000, 0000, 0000, 0001] (0x010001)
Then we can convert this to decimal.
(0 x 165) + (1 x 164) + (0 x 163) + (0 x 162) + (0 x 161) + (1 x 160) = 65537
To sum it up:
Base64: AQAB
6-bits array: [000000, 010000, 000000, 000001]
Byte array: [00000001, 00000000, 00000001]
Hexadecimal: 01 00 01
Decimal: 65537
That's all. You use this number to generate a public key and use that key to verify the token.
What to validate
After you verify that JWT hasn't tampered, its time to validate the data inside.
If you decode your JWT (You can copy your JWT token and decode it at https://jwt.io/), in payload section, you will see JSON with many fields.
{
"iss": "https://appleid.apple.com",
"aud": "com.sarunw.siwa",
"exp": 1577943613,
"iat": 1577943013,
"sub": "xxx.yyy.zzz",
"nonce": "nounce",
"c_hash": "xxxx",
"email": "xxxx@privaterelay.appleid.com",
"email_verified": "true",
"is_private_email": "true",
"auth_time": 1577943013
}
For simple validation, you might need to validate these four fields.
Key | Description | Note |
---|---|---|
iss (issuer) | The issuer registered claim key. | Should come from Apple (https://appleid.apple.com in this case) |
aud (audience) | The audience registered claim key. | This is your app's bundle id in this case |
exp (expiration) | The expiration time registered claim key. | Apple set this to 10 minutes. |
iat (issued at) | The issued at registered claim key, the value of which indicates the time at which the token was generated. | You can check elapsed time since this issued time if you need custom expiration duration. |
You use this information to validate that the token you received isn't too old, coming from Apple, and issued for your app.
Sign them in
Now that you make sure the data is trustworthy let's see what information you can use to create an account (or sign them in).
Key | Description | Note |
---|---|---|
sub (subject) | The subject registered claim key, the value of which identifies the principal that is the subject of the JWT. | This is a user id. You can use this to identify users. |
User's email | This can be their real email or a private one. Determine by is_private_email field |
|
email_verified | A Boolean value that indicates whether the service has verified the email. The value of this claim is always true because the servers only return verified email addresses. | Always true . |
is_private_email | Determine whether email is Apple private one or not. |
In my test, is_private_email key will be absent instead of false if users use a real email. |
You can use sub
to identify the existence of the account and create or sign them in accordingly.
Conclusion
This article covers only one scenario where you have an iOS app with an application server. If you want to implement Sign in with Apple in other platforms, there are extra steps you need to do to make it work. I might write about that in the future.
I think this simplest case should suffice for a simple app out there, and this article should give you a basic foundation that you can use for a more complex scenario.
You can easily support sarunw.com by checking out this sponsor.
Debug 10x faster with Proxyman: Your ultimate tool to capture HTTPs requests/ responses, natively built for your iPhone and macOS. Special deal for Black Friday: Get 30% off for all Proxyman licenses with code “BLACKFRIDAY2024”.
Related Resources
- Sign in with Apple, Part 1: Apps
- Sign in with Apple, Part 2: Private Email Relay Service
- Sign in with Apple, Part 4: Web and Other Platforms
- Apple's endpoint to Fetch Apple's public key for verifying token signature https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature
https://developer.apple.com/documentation/signinwithapplerestapi/jwkset/keys - JWT's fields. https://tools.ietf.org/html/rfc7519#section-4.1
- The characteristics of user identifier. https://developer.apple.com/documentation/signinwithapplerestapi/authenticating_users_with_sign_in_with_apple
- JSON Web Key (JWK)'s spec https://tools.ietf.org/html/rfc7517
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. https://jwt.io/ ↩︎
The JSON Web Key Set (JWKS) is a set of keys containing the public keys that should be used to verify any JSON Web Token (JWT) issued by the authorization server and signed using the RS256 signing algorithm. https://auth0.com/docs/jwks ↩︎
https://simple.wikipedia.org/wiki/RSA_algorithm, Here is an example I found while Googling. http://pajhome.org.uk/crypt/rsa/rsa.html ↩︎
https://stackoverflow.com/questions/39727230/what-kind-of-data-are-exponent-and-modulus-in-c-sharp-rsacryptoserviceprovider ↩︎
Read more article about Swift, Sign in with Apple, or see all available topic
Enjoy the read?
If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.
Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.
If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.
Become a patron Buy me a coffee Tweet SharePrint unescaped string output in Swift
How to print object (po) in a debugger (lldb) without escape special characters.