You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
141 lines
2.9 KiB
141 lines
2.9 KiB
package apple_auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rsa"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"math/big"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
type appleKey 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"`
|
|
}
|
|
type appleKeyResponse struct {
|
|
Keys []appleKey `json:"keys"`
|
|
}
|
|
|
|
func newSys(options Options) (sys *Google, err error) {
|
|
sys = &Google{
|
|
options: options,
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
type Google struct {
|
|
options Options
|
|
}
|
|
|
|
func (this *Google) Auth(ctx context.Context, identityToken string) (info string, err error) {
|
|
// 获取 token header 部分
|
|
parts := strings.Split(identityToken, ".")
|
|
if len(parts) < 2 {
|
|
return "", errors.New("invalid token")
|
|
}
|
|
|
|
// 解码 header,提取 kid
|
|
headerSegment, err := base64.RawURLEncoding.DecodeString(parts[0])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var header map[string]interface{}
|
|
if err := json.Unmarshal(headerSegment, &header); err != nil {
|
|
return "", err
|
|
}
|
|
kid := header["kid"].(string)
|
|
|
|
// 获取 apple 公钥列表
|
|
keys, err := fetchApplePublicKeys()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var pubKey *rsa.PublicKey
|
|
for _, key := range keys {
|
|
if key.Kid == kid {
|
|
pubKey, err = buildRSAPublicKey(key.N, key.E)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if pubKey == nil {
|
|
return "", errors.New("public key not found")
|
|
}
|
|
|
|
// 验证 JWT
|
|
token, err := jwt.Parse(identityToken, func(t *jwt.Token) (interface{}, error) {
|
|
return pubKey, nil
|
|
})
|
|
if err != nil || !token.Valid {
|
|
return "", errors.New("invalid token")
|
|
}
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return "", errors.New("invalid claims")
|
|
}
|
|
|
|
// 额外校验 iss/aud(推荐做)
|
|
if claims["iss"] != "https://appleid.apple.com" {
|
|
return "", errors.New("invalid issuer")
|
|
}
|
|
// 如果你知道前端用的 client_id,也可以校验 aud:
|
|
// if claims["aud"] != "com.your.bundle.id" {
|
|
// return "", errors.New("invalid audience")
|
|
// }
|
|
|
|
sub := claims["sub"].(string) // Apple 的唯一用户 ID
|
|
return sub, nil
|
|
}
|
|
|
|
// 获取 Apple 公钥列表
|
|
func fetchApplePublicKeys() ([]appleKey, error) {
|
|
resp, err := http.Get("https://appleid.apple.com/auth/keys")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
var keyResp appleKeyResponse
|
|
if err := json.Unmarshal(body, &keyResp); err != nil {
|
|
return nil, err
|
|
}
|
|
return keyResp.Keys, nil
|
|
}
|
|
|
|
// 将 base64 N 和 E 转换成 RSA 公钥
|
|
func buildRSAPublicKey(nStr, eStr string) (*rsa.PublicKey, error) {
|
|
nBytes, err := base64.RawURLEncoding.DecodeString(nStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
eBytes, err := base64.RawURLEncoding.DecodeString(eStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
e := 0
|
|
for _, b := range eBytes {
|
|
e = e<<8 + int(b)
|
|
}
|
|
|
|
return &rsa.PublicKey{
|
|
N: new(big.Int).SetBytes(nBytes),
|
|
E: e,
|
|
}, nil
|
|
}
|
|
|