Validating "Sign in with Apple" Authorization Code

https://res.cloudinary.com/pagnihotry/image/upload/v1576343105/pagnihotry/sign-in-with-apple.jpg

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:

  1. client_id - This is the bundle identifier for the app and looks like com.company.product_name.
  2. client_secret - This is a JWT token signed with the .p8 file generated while creating the Sign in with Apple key.
  3. code - This is where you put the authorizationCode. It is valid for 5 min and can only be used once. This field is skipped while using the refresh token.
  4. grant_type - This is "authorization_code" while using the authorization code or "refresh_token" while using the refresh token.
  5. refresh_token - This is set when using the refresh token to get a new access token.
  6. 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 .

  1. base64 url encoded JWT header.
  2. base64 url encoded JWT body.
  3. 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:

  1. Generate base64 string, eg: dU/+dA==.
  2. Remove padding - remove the trailing =, eg: dU/+dA.
  3. Substitute + with -, eg: dU/-dA.
  4. 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:

  1. alg - This is the signing algorithm: ES256.
  2. 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

  1. iss - This is the team id. The id of the team for which the key was generated. This can be found in Certificates, Identifiers & Profiles > Keys on the apple developer website, under the heading Configuration (something like JSFD9L6MCB.com.company.product_name) after selecting the key. It is the first section of the Configuration: JSFD9L6MCB.
  2. iat - This can be the current Unix time stamp on the server.
  3. exp - This is generally iat + 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.
  4. aud - This is set to "https://appleid.apple.com".
  5. 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:

  1. Calculate SHA-256 for encodedHeader+"."+encodedBody.
  2. 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:

  1. Generate r,s by signing using ecdsa.
  2. Append r and s, meaning join the r and s together.
  3. 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:

  1. invalid_request
  2. invalid_client
  3. invalid_grant
  4. unauthorized_client
  5. unsupported_grant_type
  6. 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

  1. Signature
  2. Content to verify
  3. 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:

  1. rsa.PublicKey object
  2. hash algorithm - crypto.SHA256
  3. hashed content - sha256.Sum256("base64url Encdoded JWTHeader"+"."+"base64url Encdoded JWTBody")
  4. 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 id com.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.