Apple login, verify identityToken method
Overview
After Apple authorizes the login, it returns the user ID, authorizationCode and identityToken, among which:
- authorizationCode corresponds to a verification method that interacts with Apple servers. Domestic three-party logins, such as WeChat login, are all this verification method, but this article does not cover it.
- identityToken uses the public key obtained from the Apple server to decrypt and verify the token. It seems that the key is rotated, and it is also very clever to verify that the token is valid. At the same time, it can ensure the correctness of the information by comparing the information.
reference
https://blog.csdn.net/we_are_the_world_123/article/details/114943076
https://xyccstudio.cn/blogs/xblog/ios/login.html
Verification process
Get Apple's public key
The address is as follows: click to view
https://appleid.apple.com/auth/keys
The content obtained is as follows:
{
"keys": [
{
"kty": "RSA",
"kid": "fh6Bs8C",
"use": "sig",
"alg": "RS256",
"n": "u704gotMSZc6CSSVNCZ1d0S9dZKwO2BVzfdTKYz8wSNm7R_KIufOQf3ru7Pph1FjW6gQ8zgvhnv4IebkGWsZJlodduTC7c0sRb5PZpEyM6PtO8FPHowaracJJsK1f6_rSLstLdWbSDXeSq7vBvDu3Q31RaoV_0YlEzQwPsbCvD45oVy5Vo5oBePUm4cqi6T3cZ-10gr9QJCVwvx7KiQsttp0kUkHM94PlxbG_HAWlEZjvAlxfEDc-_xZQwC6fVjfazs3j1b2DZWsGmBRdx1snO75nM7hpyRRQB4jVejW9TuZDtPtsNadXTr9I5NjxPdIYMORj9XKEh44Z73yfv0gtw",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "YuyXoY",
"use": "sig",
"alg": "RS256",
"n": "1JiU4l3YCeT4o0gVmxGTEK1IXR-Ghdg5Bzka12tzmtdCxU00ChH66aV-4HRBjF1t95IsaeHeDFRgmF0lJbTDTqa6_VZo2hc0zTiUAsGLacN6slePvDcR1IMucQGtPP5tGhIbU-HKabsKOFdD4VQ5PCXifjpN9R-1qOR571BxCAl4u1kUUIePAAJcBcqGRFSI_I1j_jbN3gflK_8ZNmgnPrXA0kZXzj1I7ZHgekGbZoxmDrzYm2zmja1MsE5A_JX7itBYnlR41LOtvLRCNtw7K3EFlbfB6hkPL-Swk5XNGbWZdTROmaTNzJhV-lWT0gGm6V1qWAK2qOZoIDa_3Ud0Gw",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "W6WcOKB",
"use": "sig",
"alg": "RS256",
"n": "2Zc5d0-zkZ5AKmtYTvxHc3vRc41YfbklflxG9SWsg5qXUxvfgpktGAcxXLFAd9Uglzow9ezvmTGce5d3DhAYKwHAEPT9hbaMDj7DfmEwuNO8UahfnBkBXsCoUaL3QITF5_DAPsZroTqs7tkQQZ7qPkQXCSu2aosgOJmaoKQgwcOdjD0D49ne2B_dkxBcNCcJT9pTSWJ8NfGycjWAQsvC8CGstH8oKwhC5raDcc2IGXMOQC7Qr75d6J5Q24CePHj_JD7zjbwYy9KNH8wyr829eO_G4OEUW50FAN6HKtvjhJIguMl_1BLZ93z2KJyxExiNTZBUBQbbgCNBfzTv7JrxMw",
"e": "AQAB"
}
]
}
decryption token
Analyze token
An example of identityToken:
eyJraWQiOiJmaDZCczhDIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnRqZ2QuZGV2ZWxvcC5tb2JpbGV6Lldpc2VUViIsImV4cCI6MTY5MTY0Mzc3MSwiaWF0IjoxNjkxNTU3MzcxLCJzdWIiOiIwMDA0NDAuM2E5M2Y3MDY1NTE0NGY5Mjg5ZGJlY2Q2ODhhMmQxNzguMDc0NSIsImNfaGFzaCI6IjkyOEVmVEliSkVVUDBVQlhNRF9wZHciLCJlbWFpbCI6InRqZ2RpcHR2QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImF1dGhfdGltZSI6MTY5MTU1NzM3MSwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.gt1DjUwX5oAzmYyoVoFtqjWv8D3SXkBm327RnadilOQxYbLLM82l45EWozjykRMhMtH4bWsP47m6DtBvwfA13kQg9HNTFCYOdm7j2SnF_3S5m_6sHvaxkK0VDbuk3Z-aWOcMlHMj4kFOdM9EMC5-1Qr_k9y6i7RRmh_xkAoapoFwSlLCb41J9qnornTbZnRT3CMgMngY90uxvlowYj38YpFW5Q1YDEQVtpDTz55yVIN7yPZ5bUlOHgj2wFWp_2IVo2qKCVgC4-qtoxsRDlR_Wghmv5lh_uwMGAkwBcsTS0eP0fGuYPNoZYuYzpiaLV9j3v01N191xgfUzKVa584Zeg
jwt token passes. The token is divided into three parts. The first part of the header records the encryption method, the second part of the body is the information recorded by the token, and the third part is the signature.
The three parts of this token are as follows:
header
{
"kid": "fh6Bs8C",
"alg": "RS256"
}
body
{
"iss": "https://appleid.apple.com",
"aud": "com.xxxxxx",
"exp": 1691643771,
"iat": 1691557371,
"sub": "000440.3a93f70655144f9289dbecd688a2d178.0745",
"c_hash": "928EfTIbJEUP0UBXMD_pdw",
"email": "xxxxxx",
"email_verified": "true",
"auth_time": 1691557371,
"nonce_supported": true
}
sign
gt1DjUwX5oAzmYyoVoFtqjWv8D3SXkBm327RnadilOQxYbLLM82l45EWozjykRMhMtH4bWsP47m6DtBvwfA13kQg9HNTFCYOdm7j2SnF_3S5m_6sHvaxkK0VDbuk3Z-aWOcMlHMj4kFOdM9EMC5-1Qr_k9y6i7RRmh_xkAoapoFwSlLCb41J9qnornTbZnRT3CMgMngY90uxvlowYj38YpFW5Q1YDEQVtpDTz55yVIN7yPZ5bUlOHgj2wFWp_2IVo2qKCVgC4-qtoxsRDlR_Wghmv5lh_uwMGAkwBcsTS0eP0fGuYPNoZYuYzpiaLV9j3v01N191xgfUzKVa584Zeg
Verification token
Select a key through the kid parsed from the header and use the key for token verification.
Check payload
After there is no problem with the token, check whether the content in the above body is consistent with the apple-related content. That is to complete the verification of the overall process.
Overall process code
package login
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"io"
"math/big"
"net/http"
"strings"
"github.com/dgrijalva/jwt-go"
)
const (
PUBLIC_KEY_REQ_URL = "https://appleid.apple.com/auth/keys"
APPLE_URL = "https://appleid.apple.com"
APPLICATION_CLIENT_ID = "xxx"
)
type JwtClaims struct {
jwt.StandardClaims
}
type JwtHeader struct {
Kid string `json:"kid"`
Alg string `json:"alg"`
}
type JwtKeys struct {
Kty string `json:"kty"`
Kid string `json:"kid"`
Use string `json:"use"`
Alg string `json:"alg"`
N string `json:"n"`
E string `json:"e"`
}
func VerifyIdentityToken(cliToken string, cliUserID string) error {
cliTokenArr := strings.Split(cliToken, ".")
if len(cliTokenArr) < 3 {
return errors.New("cliToken Split err")
}
cliHeader, err := jwt.DecodeSegment(cliTokenArr[0])
if err != nil {
return err
}
var jHeader JwtHeader
err = json.Unmarshal(cliHeader, &jHeader)
if err != nil {
return err
}
token, err := jwt.ParseWithClaims(cliToken, &JwtClaims{
}, func(token *jwt.Token) (interface{
}, error) {
pk := GetRSAPublicKey(jHeader.Kid)
return pk, nil
})
if err != nil {
return err
}
if claims, ok := token.Claims.(*JwtClaims); ok && token.Valid {
if claims.Issuer != APPLE_URL || claims.Audience != APPLICATION_CLIENT_ID || claims.Subject != cliUserID {
return errors.New("verify token info fail, info is not match")
}
} else {
return errors.New("token claims parse fail")
}
return nil
}
func GetRSAPublicKey(kid string) *rsa.PublicKey {
var body []byte
resp, err := http.Get(PUBLIC_KEY_REQ_URL)
if err != nil {
return nil
} else {
defer resp.Body.Close()
_body, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
body = _body
}
var jKeys map[string][]JwtKeys
err = json.Unmarshal(body, &jKeys)
if err != nil {
return nil
}
var pubKey rsa.PublicKey
for _, data := range jKeys {
for _, val := range data {
if val.Kid == kid {
n_bin, _ := base64.RawURLEncoding.DecodeString(val.N)
n_data := new(big.Int).SetBytes(n_bin)
e_bin, _ := base64.RawURLEncoding.DecodeString(val.E)
e_data := new(big.Int).SetBytes(e_bin)
pubKey.N = n_data
pubKey.E = int(e_data.Uint64())
break
}
}
}
if pubKey.E <= 0 {
return nil
}
return &pubKey
}
Use cases
package main
import (
"fmt"
"xyccstudio/login"
)
func main() {
err := login.VerifyIdentityToken("eyJraWQiOiJmaDZCczhDIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnRqZ2QuZGV2ZWxvcC5tb2JpbGV6Lldpc2VUViIsImV4cCI6MTY5MTY0Mzc3MSwiaWF0IjoxNjkxNTU3MzcxLCJzdWIiOiIwMDA0NDAuM2E5M2Y3MDY1NTE0NGY5Mjg5ZGJlY2Q2ODhhMmQxNzguMDc0NSIsImNfaGFzaCI6IjkyOEVmVEliSkVVUDBVQlhNRF9wZHciLCJlbWFpbCI6InRqZ2RpcHR2QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImF1dGhfdGltZSI6MTY5MTU1NzM3MSwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.gt1DjUwX5oAzmYyoVoFtqjWv8D3SXkBm327RnadilOQxYbLLM82l45EWozjykRMhMtH4bWsP47m6DtBvwfA13kQg9HNTFCYOdm7j2SnF_3S5m_6sHvaxkK0VDbuk3Z-aWOcMlHMj4kFOdM9EMC5-1Qr_k9y6i7RRmh_xkAoapoFwSlLCb41J9qnornTbZnRT3CMgMngY90uxvlowYj38YpFW5Q1YDEQVtpDTz55yVIN7yPZ5bUlOHgj2wFWp_2IVo2qKCVgC4-qtoxsRDlR_Wghmv5lh_uwMGAkwBcsTS0eP0fGuYPNoZYuYzpiaLV9j3v01N191xgfUzKVa584Zeg", "000440.3a93f70655144f9289dbecd688a2d178.0745")
fmt.Printf("check result %+v", err)
}