You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Golang的Azure AD SSO中RSA JWT令牌验证问题排查

问题分析与解决方案

核心错误:验证错了令牌类型

你现在在验证Azure AD返回的AccessToken,但这个令牌是用来调用Microsoft Graph等API的,不应该用于用户身份验证。身份验证应该用返回的ID Token——它才是专门用来做身份校验、包含用户信息的JWT令牌。

其他代码问题

  1. 手动解析JWK时忽略错误:base64.RawURLEncoding.DecodeString的错误被你忽略了,如果解码失败会导致公钥无效。
  2. 未处理找不到匹配JWK的情况:遍历JWKS时如果没找到对应kid的密钥,会继续用空的AzurePublicKey去生成公钥,必然验证失败。
  3. 重复解析令牌头获取kid:jwt.Parse的回调函数里的token参数已经包含了kid,不需要额外调用getKidFromToken解析,直接取token.Header["kid"].(string)即可。
  4. 未利用已有的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 18:15:11