基于Golang的Azure AD SSO中RSA JWT令牌验证问题排查
问题分析与解决方案
核心错误:验证错了令牌类型
你现在在验证Azure AD返回的AccessToken,但这个令牌是用来调用Microsoft Graph等API的,不应该用于用户身份验证。身份验证应该用返回的ID Token——它才是专门用来做身份校验、包含用户信息的JWT令牌。
其他代码问题
- 手动解析JWK时忽略错误:
base64.RawURLEncoding.DecodeString的错误被你忽略了,如果解码失败会导致公钥无效。 - 未处理找不到匹配JWK的情况:遍历JWKS时如果没找到对应kid的密钥,会继续用空的
AzurePublicKey去生成公钥,必然验证失败。 - 重复解析令牌头获取kid:
jwt.Parse的回调函数里的token参数已经包含了kid,不需要额外调用getKidFromToken解析,直接取token.Header["kid"].(string)即可。 - 未利用已有的go-oidc库:你已经引入了
go-oidc,这个库可以自动处理OIDC的令牌验证、JWK获取与缓存,不用手动实现这些容易出错的逻辑。
修正后的代码示例
推荐方案:用go-oidc自动处理验证
m.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { fmt.Println("Login Callback Route Hit!") // 检查state有效性 if r.URL.Query().Get("state") != "state-token" { http.Error(w, fmt.Sprintf("Invalid Callback State: %s", r.URL.Query().Get("state")), http.StatusBadRequest) return } // 交换授权码获取令牌 code := r.URL.Query().Get("code") oauthToken, err := AZURE_SSO.Oauth.Exchange(r.Context(), code) if err != nil { http.Error(w, fmt.Sprintf("Failed to exchange code for token:\n\tCode: %s\n\tError: %s\n", code, err), http.StatusInternalServerError) return } // 从oauthToken中提取ID Token(这才是身份验证用的令牌) idTokenRaw, ok := oauthToken.Extra("id_token").(string) if !ok { http.Error(w, "Failed to extract ID Token", http.StatusInternalServerError) return } // 使用go-oidc验证ID Token,自动处理签名、JWK、声明校验 verifier := PROVIDER.Verifier(&oidc.Config{ ClientID: configData["clientid"], // 必须与你的应用ClientID一致 }) idToken, err := verifier.Verify(r.Context(), idTokenRaw) if err != nil { http.Error(w, fmt.Sprintf("Failed to verify ID Token: %s", err), http.StatusUnauthorized) return } // 解析用户信息 var userInfo struct { Email string `json:"email"` Name string `json:"name"` } if err := idToken.Claims(&userInfo); err != nil { http.Error(w, fmt.Sprintf("Failed to parse user claims: %s", err), http.StatusInternalServerError) return } fmt.Printf("Account Email: %s\n", userInfo.Email) fmt.Printf("Login Successful!\n") http.Redirect(w, r, "/home/", http.StatusFound) })
手动验证方案(不推荐,仅作参考)
如果坚持手动实现JWT验证,修正后的验证逻辑如下:
// 解析ID Token而非AccessToken jwtToken, err := jwt.Parse(idTokenRaw, func(token *jwt.Token) (interface{}, error) { // 验证签名算法 if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } // 从令牌头获取kid kid, ok := token.Header["kid"].(string) if !ok { return nil, errors.New("kid not found in token header") } // 获取Azure AD的JWKS resp, err := http.Get("https://login.microsoftonline.com/common/discovery/v2.0/keys") if err != nil { return nil, err } defer resp.Body.Close() var jwks Authentication.JWKS if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil { return nil, err } // 查找匹配的密钥 var targetKey Authentication.JWK found := false for _, key := range jwks.Keys { if key.Kid == kid && key.Use == "sig" && key.Kty == "RSA" { targetKey = key found = true break } } if !found { return nil, fmt.Errorf("no matching key found for kid: %s", kid) } // 解码模数和指数,处理错误 nBytes, err := base64.RawURLEncoding.DecodeString(targetKey.N) if err != nil { return nil, fmt.Errorf("failed to decode modulus: %w", err) } eBytes, err := base64.RawURLEncoding.DecodeString(targetKey.E) if err != nil { return nil, fmt.Errorf("failed to decode exponent: %w", err) } n := new(big.Int).SetBytes(nBytes) e := int(new(big.Int).SetBytes(eBytes).Int64()) if e <= 0 { return nil, errors.New("invalid public key exponent") } return &rsa.PublicKey{N: n, E: e}, nil }) if err != nil { http.Error(w, fmt.Sprintf("Failed to parse/verify token: %s", err), http.StatusUnauthorized) return } // 额外校验令牌声明(audience、issuer等) claims, ok := jwtToken.Claims.(jwt.MapClaims) if !ok || !jwtToken.Valid { http.Error(w, "Invalid token claims", http.StatusUnauthorized) return } // 校验audience必须是你的应用ClientID if claims["aud"] != configData["clientid"] { http.Error(w, "Invalid token audience", http.StatusUnauthorized) return } // 校验issuer与配置一致 expectedIssuer := configData["issuer"] if claims["iss"] != expectedIssuer { http.Error(w, "Invalid token issuer", http.StatusUnauthorized) return }
关键提醒
- 优先使用
go-oidc库处理OIDC流程,它会自动处理令牌验证、JWK缓存、声明校验等逻辑,避免手动实现的错误。 - 永远用ID Token做身份验证,AccessToken仅用于调用下游API。
内容的提问来源于stack exchange,提问作者maplelm
相关产品推荐
相关产品推荐

