Sign in with Apple launched earlier this year. Developers can use it to have users log in using their Apple accounts. This post outlines validating the authorizationCode
received after the user signs in with Apple, generating JWT ES256 signature, verifying JWT signature using RS256 and using the refresh token to get an access token from Apple with implementation details and code samples in Golang.
Authorization Code
Once the initial authorization request to Apple returns with a success, among other things, it contains an authorizationCode
. This code is something that a webserver can use to fetch user data directly from Apple and keep the refresh token stowed on the side to get a new access token in the future. The authorization code is valid for 5 min and can only be used once.
Preparing the request
To verify the authorizationCode
, the first step is to put together a validation request. It is a POST to https://appleid.apple.com/auth/token with following parameters set as form data in the body:
client_id
- This is the bundle identifier for the app and looks likecom.company.product_name
.client_secret
- This is a JWT token signed with the.p8
file generated while creating the Sign in with Apple key.code
- This is where you put theauthorizationCode
. It is valid for 5 min and can only be used once. This field is skipped while using the refresh token.grant_type
- This is"authorization_code"
while using the authorization code or"refresh_token"
while using the refresh token.refresh_token
- This is set when using the refresh token to get a new access token.redirect_uri
- This is the redirect URI where the authorization code was sent. If you are not using it. This can be set to an empty string.
Most of these are pretty straight forward, except the client_secret
. The following section contains more details on how to generate it.
client_secret for Sign in with Apple
Generating client_secret
requires making a JWT token and then signing it using the Elliptic Curve Digital Signature Algorithm (ECDSA) with the P-256 curve and the SHA-256 hash algorithm. Essentially, the ES256 Algorithm for JWT. There are a lot of JWT libraries out there pretty much for every language implementing this. https://jwt.io contains a nice list of them.
The following section describes how the signature is generated. It can help to get a better understanding of what is happening under the hood or to roll out your own implementation.
The client_secret
consists of 3 strings that are joined together with a .
- base64 url encoded JWT header.
- base64 url encoded JWT body.
- Signed SHA-256 of the header and body with Elliptic Curve Digital Signature Algorithm (ECDSA) with the P-256 curve.
It looks like this:
base64Url(json header) + "." + base64Url(json body) + "." + sign(sha256(base64Url(json header) + "." + base64Url(json body)), p8Key)
base64 url encoding
For all the tokens, base64 url encoding is used pretty exhaustively. It is a variation of base64 encoding, outlined in rfc3548. This format is URL safe and substitutes certain characters in base64 encoding to make it safe for transmission on the web. Almost all languages support it, but you can also get a base 64 url encoded string from a standard base64 encoded string by following steps:
- Generate base64 string, eg:
dU/+dA==
. - Remove padding - remove the trailing
=
, eg:dU/+dA
. - Substitute
+
with-
, eg:dU/-dA
. - Substitute
/
with_
, eg:dU_-dA
.
This helps to remove the URL unsafe characters /
,+
and =
. Replacing them with URL safe characters.
client_secret - JWT Header
The JWT header contains 2 fields:
alg
- This is the signing algorithm:ES256
.kid
- This is the key id for the Sign in with Apple key.
The JWT Header looks like this -
{
"alg": "ES256",
"kid": "3UHT5POLK9"
}
To generate the header for the client_secret, base64 url encode the JSON string representation of the header.
eg: base64Url('{"alg":"ES256","kid":"3UHT5POLK9"}') = eyJhbGciOiJFUzI1NiIsImtpZCI6IjNVSFQ1UE9MSzkifQ
client_secret - JWT Body
The body contains 5 fields
iss
- This is the team id. The id of the team for which the key was generated. This can be found inCertificates, Identifiers & Profiles
>Keys
on the apple developer website, under the heading Configuration (something likeJSFD9L6MCB.com.company.product_name
) after selecting the key. It is the first section of the Configuration:JSFD9L6MCB
.iat
- This can be the current Unix time stamp on the server.exp
- This is generallyiat + seconds
seconds can have any value, but is capped at 15777000 (6 months in seconds) from the Current Unix Time on the server, as per the documentation.aud
- This is set to"https://appleid.apple.com"
.sub
- This is the bundle identifier for your app, eg:com.company.product_name
.
The JWT Body looks like this -
{
"iss": "JSFD9L6MCB",
"iat": 1576248290,
"exp": 1577717090,
"aud": "https://appleid.apple.com",
"sub": "com.company.product_name"
}
To generate the Body for the client_secret, base64 url encode the json string representation of the body.
eg: base64Url('{"iss":"JSFD9L6MCB","iat":1576248290,"exp":1577717090,"aud":"https://appleid.apple.com","sub":"com.company.product_name"}') = eyJpc3MiOiJKU0ZEOUw2TUNCIiwiaWF0IjoxNTc2MjQ4MjkwLCJleHAiOjE1Nzc3MTcwOTAsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJjb20uY29tcGFueS5wcm9kdWN0X25hbWUifQ
Signing
After the header and the body are generated, the third piece of the puzzle is the signature. Generating the ES256 signature involves 2 steps:
- Calculate SHA-256 for
encodedHeader+"."+encodedBody
. - Sign the SHA-256 with the
p8
key.
SHA-256
For this step, you need to calculate SHA-256 of the string eyJhbGciOiJFUzI1NiIsImtpZCI6IjNVSFQ1UE9MSzkifQ.eyJpc3MiOiJKU0ZEOUw2TUNCIiwiaWF0IjoxNTc2MjQ4MjkwLCJleHAiOjE1Nzc3MTcwOTAsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJjb20uY29tcGFueS5wcm9kdWN0X25hbWUifQ
which comes out to be 12360b7fd43028376acc13070faede11e8b32d6a92a2c1803ca0efb34b415cd7
In Golang this can be done by using the crypto/sha256
package
hash = sha256.Sum256([]byte(header+"."+body))
Signing
The .p8
file that Apple provides is a PEM encoded file containing the ecdsa private key. It looks something like this:
-----BEGIN PRIVATE KEY-----
jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve
jkfweshjdjkhjsbjvguybje/vuewkvbbhjdjbdhbjhbvjhbvjhbvbjvbvjvagcve
jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve
jkfweshj
-----END PRIVATE KEY-----
Following are the steps to calculate the signature from the SHA-256:
- Generate
r
,s
by signing usingecdsa
. - Append
r
ands
, meaning join ther
ands
together. - base64 url encode the appended result.
Implementation details in Golang:
The crypto/ecdsa
package can be used to sign the SHA-256. It has a function called Sign
that can be used to compute the signature. But to use this function, you need to have the PEM key from .p8
file converted into an ecdsa.PrivateKey
object.
The first step for this is to decode it from PEM format. This can be achieved by the pem.Decode
function from encoding/pem
package. This function takes in the pem encoded contents as []byte
and returns a pointer to pem.Block
object. The .Bytes
property of the pem.Block
object contains the decoded key.
Once you have the pem decoded key, the next step will be to generate an ecdsa.PrivateKey
object that can be used to compute the signature. To do that you will need to convert it from x509 format using the function x509.ParsePKCS8PrivateKey
from crypto/x509
package. This function returns interface{}
which can be cast into ecdsa.PrivateKey
.
Here is a sample function to accomplish this. More code illustrations can be found at https://github.com/pagnihotry/siwago.
//generate private key from pem encoded string
func getPrivKey(pemEncoded []byte) (*ecdsa.PrivateKey, error) {
var block *pem.Block
var x509Encoded []byte
var err error
var privateKeyI interface{}
var privateKey *ecdsa.PrivateKey
var ok bool
//decode the pem format
block, _ = pem.Decode(pemEncoded)
//check if its is private key
if block == nil || block.Type != "PRIVATE KEY" {
return nil, errors.New("Failed to decode PEM block containing private key")
}
//get the encoded bytes
x509Encoded = block.Bytes
//generate the private key object
privateKeyI, err = x509.ParsePKCS8PrivateKey(x509Encoded)
if err != nil {
return nil, errors.New("Private key decoding failed. " + err.Error())
}
//cast into ecdsa.PrivateKey object
privateKey, ok = privateKeyI.(*ecdsa.PrivateKey)
if !ok {
return nil, errors.New("Private key is not ecdsa key")
}
return privateKey, nil
}
After generating the ecdsa.PrivateKey
you can call the ecdsa.Sign
method. This method returns 2 byte arrays - r
and s
. The signature is r
appended to s
and then base64 url encoded.
r, s, err = ecdsa.Sign(rand.Reader, privKey, hash[:])
//join r and s
hashBytes = append(r.Bytes(), s.Bytes()...)
//base64urlencode the bytes
ecdsaHash = base64.RawURLEncoding.EncodeToString(hashBytes)
This ecdsaHash
can then be appended to the string that was computed earlier to get the final result:
"eyJhbGciOiJFUzI1NiIsImtpZCI6IjNVSFQ1UE9MSzkifQ.eyJpc3MiOiJKU0ZEOUw2TUNCIiwiaWF0IjoxNTc2MjQ4MjkwLCJleHAiOjE1Nzc3MTcwOTAsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJjb20uY29tcGFueS5wcm9kdWN0X25hbWUifQ"+"."+ecdsaHash
At this point, you should have all the fields needed to make the POST request to https://appleid.apple.com/auth/token endpoint. As per the documentation, the response can be Token Response or Error Response.
Token Response
A successful POST returns refresh token, access token, id token, etc. The full response is documented here: Apple Token Response. It is a json response consisting of following fields:
access_token
- (Reserved for future use) A token used to access allowed data. Currently, no data set has been defined for access. Valid for an hour.expires_in
- The amount of time, in seconds, before the access token expires.id_token
- A JSON Web Token that contains the user’s identity information.refresh_token
- The refresh token used to regenerate new access tokens. Store this token securely on your server.token_type
- The type of access token. It will always be"bearer"
.
Errors
The REST call to https://appleid.apple.com/auth/token can return certain errors. They can be as follows:
- invalid_request
- invalid_client
- invalid_grant
- unauthorized_client
- unsupported_grant_type
- invalid_scope
More details at - Apple Error Response Object
In the siwago package at https://github.com/pagnihotry/siwago, it is set in the Token.Error
property.
Validating Identity token
In the token response, there is a field called id_token
that contains the user information. It is a signed JWT Token using the RS256 algorithm. It looks similar to the client_secret
discussed earlier. It also has 3 parts separated by a .
.
The first one is the JWT Header, the second is JWT Body, and the third is the signature. The header and the body are both base64 url encoded so they can be decoded using base64 url decode function. In Golang, you can use the function base64.RawURLEncoding.DecodeString
.
To make sure id_token
is valid and is not tampered with, it needs to be validated. Apple recommends the following steps for validation:
- Verify the JWS E256 signature using the server’s public key
- Verify the nonce for the authentication
- Verify that the iss field contains https://appleid.apple.com
- Verify that the aud field is the developer’s client_id
- Verify that the time is earlier than the exp value of the token
Most of it is pretty straight forward. iss
should be https://appleid.apple.com, exp
should be in the future, aud
should be the same as the bundle id of the app, eg: com.company.product_name
. nonce
and signature verification are interesting and discussed in the following sections.
Nonce
The nonce
field is present only when a nonce is set while making the initial authorization request. If it is not set at that point, it is absent from the JWT body of the id_token
.
What it means is that when you set up the authorization request you can add a nonce to the request at that step. The code returned from that call will have the nonce as a part of the id_token
response. If a nonce was not set, it will be absent in the id_token
.
For example, the following is how you can set nonce in swift. Once you get the authorizationCode
back, the id_token
received on exchanging it will have the field "nonce":"test"
.
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = "test"
id_token
signature verification
To verify the id token you will need the following
- Signature
- Content to verify
- Public key to verify
As discussed earlier, id_token
looks something like this - base64urlencode(<json JWT Header>) + "." + base64urlencode(<json JWT Body>) + "." + Signature
Signature
To extract the signature, you can take the content to the right of the last .
in the id_token
Content to verify
The content is the SHA-256 sum of base64 url eccoded JWT Header and JWT Body. Which implies SHA-256 of the content to the left of last .
in the id_token
. It can be represented as sha256(base64urlencode(<json JWT Header>) + "." + base64urlencode(<json JWT Body>))
.
Public Key
Apple’s public key is located at https://appleid.apple.com/auth/keys. At the time of writing, it looks 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 is an RSA public key with modulo and exponent. To verify the signature, you need to get the content computed in the earlier step and verify that it matches the signature present in the id_token
using this key.
Signature verification implementation in Golang
To verify the signature in Golang, you can use the function rsa.VerifyPKCS1v15
from the crypto/rsa
package (https://golang.org/pkg/crypto/rsa/#VerifyPKCS1v15). To use it, you will need:
rsa.PublicKey
object- hash algorithm -
crypto.SHA256
- hashed content -
sha256.Sum256("base64url Encdoded JWTHeader"+"."+"base64url Encdoded JWTBody")
- signature -
base64.RawURLEncoding.DecodeString(idTokenSignatureString)
To create rsa.PublicKey
object you will need to load the n
and e
- the modulo and exponent into the object. The n
and e
are base64 url encoded. So the first step will be to base64 url decode them. After they are decoded, you can set n
by using the SetBytes
function on big.Int
. e
is an int on the rsa.PublicKey
object. For this, you can use the bitwise operator to set its value.
Here is a function that generates *rsa.PublicKey
object from base64 url encoded e
and n
. You can find the complete implementation at https://github.com/pagnihotry/siwago/blob/master/helpers.go
//function to generate rsa.PublicKey object from encoded modulo and exponent
func getPublicKeyObject(base64urlEncodedN string, base64urlEncodedE string) *rsa.PublicKey {
var pub rsa.PublicKey
var decE, decN []byte
var eInt int
var err error
//get the modulo
decN, err = base64.RawURLEncoding.DecodeString(base64urlEncodedN)
if err != nil {
return nil
}
pub.N = new(big.Int)
pub.N.SetBytes(decN)
//get exponent
decE, err = base64.RawURLEncoding.DecodeString(base64urlEncodedE)
if err != nil {
return nil
}
//convert the bytes into int
for _, v := range decE {
eInt = eInt << 8
eInt = eInt | int(v)
}
pub.E = eInt
return &pub
}
Refreshing Tokens
Once you have the refresh_token
it can be stored securely on the server-side and later, can be used to fetch a new access token. The access tokens do not have any data defined for access yet, but that may change in the future. If you want to implement this in your service, you can make the same request that you made earlier using authorization code with some changes.
You will need to make a POST request to https://appleid.apple.com/auth/token with following form data
grant_type
- set to"refresh_token"
.refresh_token
- set to the refresh token received with authorization code.client_secret
- generated using the same method discussed earlier in the post.client_id
- set to bundle idcom.company.product_name
.redirect_uri
- same as in authorization code.
Implementation in Golang
I have put together a Golang package at https://github.com/pagnihotry/siwago which implements the following:
- Exchanging
authorizationCode
for the Token Response. - Validating
id_token
. - Exchanging
refresh_token
for the access token.
You can include it in your project using go get github.com/pagnihotry/siwago
.