如何在Go中使用JWK验证JWT签名?

问题描述 投票:0回答:3

我一直在寻找一个我可以理解的示例,了解如何使用 Go 语言验证 JWT 的签名。

这可能特别棘手,因为我使用的是 Okta,并且它使用 JWK,所以它不是特别直接。

当我收到 JWT 时,我可以毫无问题地对其进行解码。我只是卡在如何验证签名上。

下面我包含了 JWT 和 JWK 详细信息。谁能提供签名验证的例子吗?

您可以在 https://oktaproxy.com/oidcgenerator.php 获取所有这些信息 — 该网站将从 Okta 生成 JWT,并且可以在 https://companyx.okta.com 获取密钥/oauth2/v1/keys.

这是智威汤逊:

eyJhbGciOiJSUzI1NiIsImtpZCI6Ind5TXdLNEE2Q0w5UXcxMXVvZlZleVExMTlYeVgteHlreW1ra1h5Z1o1T00ifQ.eyJzdWIiOiIwMHUxOGVlaHUzNDlhUzJ5WDFkOCIsIm5hbWUiOiJva3RhcHJveHkgb2t0YXByb3h5IiwidmVyIjoxLCJpc3MiOiJodHRwczovL2NvbXBhbnl4Lm9rdGEuY29tIiwiYXVkIjoidlpWNkNwOHJuNWx4ck45YVo2ODgiLCJpYXQiOjE0ODEzODg0NTMsImV4cCI6MTQ4MTM5MjA1MywianRpIjoiSUQuWm9QdVdIR3IxNkR6a3RUbEdXMFI4b1lRaUhnVWg0aUotTHo3Z3BGcGItUSIsImFtciI6WyJwd2QiXSwiaWRwIjoiMDBveTc0YzBnd0hOWE1SSkJGUkkiLCJub25jZSI6Im4tMFM2X1d6QTJNaiIsInByZWZlcnJlZF91c2VybmFtZSI6Im9rdGFwcm94eUBva3RhLmNvbSIsImF1dGhfdGltZSI6MTQ4MTM4ODQ0MywiYXRfaGFzaCI6Im1YWlQtZjJJczhOQklIcV9CeE1ISFEifQ.OtVyCK0sE6Cuclg9VMD2AwLhqEyq2nv3a1bfxlzeS-bdu9KtYxcPSxJ6vxMcSSbMIIq9eEz9JFMU80zqgDPHBCjlOsC5SIPz7mm1Z3gCwq4zsFJ-2NIzYxA3p161ZRsPv_3bUyg9B_DPFyBoihgwWm6yrvrb4rmHXrDkjxpxCLPp3OeIpc_kb2t8r5HEQ5UBZPrsiScvuoVW13YwWpze59qBl_84n9xdmQ5pS7DklzkAVgqJT_NWBlb5uo6eW26HtJwHzss7xOIdQtcOtC1Gj3O82a55VJSQnsEEBeqG1ESb5Haq_hJgxYQnBssKydPCIxdZiye-0Ll9L8wWwpzwig

这是钥匙:

{
   "keys":[
      {
         "alg":"RS256",
         "e":"AQAB",
         "n":"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw",
         "kid":"wyMwK4A6CL9Qw11uofVeyQ119XyX-xykymkkXygZ5OM",
         "kty":"RSA",
         "use":"sig"
      },
      {
         "alg":"RS256",
         "e":"AQAB",
         "n":"nXv6FSAcMjuanQ2hIIUb8Vkqe94t98kPh2T8-0j6-Jq8pOclgKdtVeIZcBE9F_XiuJvg4b6WVs-uvA-pS8mmMvQ21xU5Q_37Cojv8v_QlHWETHwEJdXXiY2Xq5LgXDSwEhhdDZHSMQYDuvhp_P6nl2LNqqUvJkjoFWcnn2btgSIUQROIaDdxtx7_2h4oUi5u11BGSF2SZZiEpDAKT08Htv3uwXdwDA6ll99fbi8X8RmH5oY_tIZTeIzu50qHxElPewoYO8QrJYsO9oFcCPMHGxYWjXQEa-QZYgo0wS9zRIkeJc5kshc4-9Uhv2DVIjk_-ofGlML9ieggGyillBKptw",
         "kid":"GRF55Lbzgg4sANCmER-sm55eX_qUOpY8UTptDmDG_6U",
         "kty":"RSA",
         "use":"sig"
      }
   ]
}
go jwt jwk jwt-go
3个回答
52
投票

不幸的是,

lestrrat-go/jwx/v2
破坏了兼容性,Materialize()方法不再存在。键集中的键现在是
*jwk.rsaPublicKey
,但我们需要
*rsa.PublicKey
才能使用 dgrijalva 的
jwt.Parse()


下面是JWT解码和验证的示例。它同时使用 jwt-gojwk 包:

package main

import (
    "errors"
    "fmt"

    "github.com/dgrijalva/jwt-go"
    "github.com/lestrrat-go/jwx/jwk"
)

const token = `eyJhbGciOiJSUzI1NiIsImtpZCI6Ind5TXdLNEE2Q0w5UXcxMXVvZlZleVExMTlYeVgteHlreW1ra1h5Z1o1T00ifQ.eyJzdWIiOiIwMHUxOGVlaHUzNDlhUzJ5WDFkOCIsIm5hbWUiOiJva3RhcHJveHkgb2t0YXByb3h5IiwidmVyIjoxLCJpc3MiOiJodHRwczovL2NvbXBhbnl4Lm9rdGEuY29tIiwiYXVkIjoidlpWNkNwOHJuNWx4ck45YVo2ODgiLCJpYXQiOjE0ODEzODg0NTMsImV4cCI6MTQ4MTM5MjA1MywianRpIjoiSUQuWm9QdVdIR3IxNkR6a3RUbEdXMFI4b1lRaUhnVWg0aUotTHo3Z3BGcGItUSIsImFtciI6WyJwd2QiXSwiaWRwIjoiMDBveTc0YzBnd0hOWE1SSkJGUkkiLCJub25jZSI6Im4tMFM2X1d6QTJNaiIsInByZWZlcnJlZF91c2VybmFtZSI6Im9rdGFwcm94eUBva3RhLmNvbSIsImF1dGhfdGltZSI6MTQ4MTM4ODQ0MywiYXRfaGFzaCI6Im1YWlQtZjJJczhOQklIcV9CeE1ISFEifQ.OtVyCK0sE6Cuclg9VMD2AwLhqEyq2nv3a1bfxlzeS-bdu9KtYxcPSxJ6vxMcSSbMIIq9eEz9JFMU80zqgDPHBCjlOsC5SIPz7mm1Z3gCwq4zsFJ-2NIzYxA3p161ZRsPv_3bUyg9B_DPFyBoihgwWm6yrvrb4rmHXrDkjxpxCLPp3OeIpc_kb2t8r5HEQ5UBZPrsiScvuoVW13YwWpze59qBl_84n9xdmQ5pS7DklzkAVgqJT_NWBlb5uo6eW26HtJwHzss7xOIdQtcOtC1Gj3O82a55VJSQnsEEBeqG1ESb5Haq_hJgxYQnBssKydPCIxdZiye-0Ll9L8wWwpzwig`

const jwksURL = `https://companyx.okta.com/oauth2/v1/keys`

func getKey(token *jwt.Token) (interface{}, error) {

    // TODO: cache response so we don't have to make a request every time
    // we want to verify a JWT
    set, err := jwk.FetchHTTP(jwksURL)
    if err != nil {
        return nil, err
    }

    keyID, ok := token.Header["kid"].(string)
    if !ok {
        return nil, errors.New("expecting JWT header to have string kid")
    }

    if key := set.LookupKeyID(keyID); len(key) == 1 {
        return key[0].Materialize()
    }

    return nil, fmt.Errorf("unable to find key %q", keyID)
}

func main() {
    token, err := jwt.Parse(token, getKey)
    if err != nil {
        panic(err)
    }
    claims := token.Claims.(jwt.MapClaims)
    for key, value := range claims {
        fmt.Printf("%s\t%v\n", key, value)
    }
}

8
投票

我最近有一个非常相似的用例,所以我阅读了一些 RFC 并编写了这个包:

github.com/MicahParks/keyfunc

它允许您使用最流行的 JWT 包

github.com/golang-jwt/jwt/v4
(以前的
github.com/dgrijalva/jwt-go
)来解析令牌。它还可以在后台 goroutine 中自动实时重新加载 JWKS 的内容。

使用 JWKS 和 JWT,这里有两个示例。第一个将通过 HTTPS 从远程 URL 加载 JWKS。第二个将从静态 JSON 加载它。

来自通过 HTTPS 托管的 JWKS

package main

import (
    "context"
    "log"
    "time"

    "github.com/golang-jwt/jwt/v4"

    "github.com/MicahParks/keyfunc"
)

func main() {
    // Get the JWKS URL.
    //
    // This is a sample JWKS service. Visit https://jwks-service.appspot.com/ and grab a token to test this example.
    jwksURL := "https://jwks-service.appspot.com/.well-known/jwks.json"

    // Create a context that, when cancelled, ends the JWKS background refresh goroutine.
    ctx, cancel := context.WithCancel(context.Background())

    // Create the keyfunc options. Use an error handler that logs. Refresh the JWKS when a JWT signed by an unknown KID
    // is found or at the specified interval. Rate limit these refreshes. Timeout the initial JWKS refresh request after
    // 10 seconds. This timeout is also used to create the initial context.Context for keyfunc.Get.
    options := keyfunc.Options{
        Ctx: ctx,
        RefreshErrorHandler: func(err error) {
            log.Printf("There was an error with the jwt.Keyfunc\nError: %s", err.Error())
        },
        RefreshInterval:   time.Hour,
        RefreshRateLimit:  time.Minute * 5,
        RefreshTimeout:    time.Second * 10,
        RefreshUnknownKID: true,
    }

    // Create the JWKS from the resource at the given URL.
    jwks, err := keyfunc.Get(jwksURL, options)
    if err != nil {
        log.Fatalf("Failed to create JWKS from resource at the given URL.\nError: %s", err.Error())
    }

    // Get a JWT to parse.
    jwtB64 := "eyJraWQiOiJlZThkNjI2ZCIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJXZWlkb25nIiwiYXVkIjoiVGFzaHVhbiIsImlzcyI6Imp3a3Mtc2VydmljZS5hcHBzcG90LmNvbSIsImlhdCI6MTYzMTM2OTk1NSwianRpIjoiNDY2M2E5MTAtZWU2MC00NzcwLTgxNjktY2I3NDdiMDljZjU0In0.LwD65d5h6U_2Xco81EClMa_1WIW4xXZl8o4b7WzY_7OgPD2tNlByxvGDzP7bKYA9Gj--1mi4Q4li4CAnKJkaHRYB17baC0H5P9lKMPuA6AnChTzLafY6yf-YadA7DmakCtIl7FNcFQQL2DXmh6gS9J6TluFoCIXj83MqETbDWpL28o3XAD_05UP8VLQzH2XzyqWKi97mOuvz-GsDp9mhBYQUgN3csNXt2v2l-bUPWe19SftNej0cxddyGu06tXUtaS6K0oe0TTbaqc3hmfEiu5G0J8U6ztTUMwXkBvaknE640NPgMQJqBaey0E4u0txYgyvMvvxfwtcOrDRYqYPBnA"

    // Parse the JWT.
    token, err := jwt.Parse(jwtB64, jwks.Keyfunc)
    if err != nil {
        log.Fatalf("Failed to parse the JWT.\nError: %s", err.Error())
    }

    // Check if the token is valid.
    if !token.Valid {
        log.Fatalf("The token is not valid.")
    }
    log.Println("The token is valid.")

    // End the background refresh goroutine when it's no longer needed.
    cancel()

    // This will be ineffectual because the line above this canceled the parent context.Context.
    // This method call is idempotent similar to context.CancelFunc.
    jwks.EndBackground()
}

从 JWKS 作为 JSON

package main

import (
    "encoding/json"
    "log"

    "github.com/golang-jwt/jwt/v4"

    "github.com/MicahParks/keyfunc"
)

func main() {
    // Get the JWKS as JSON.
    jwksJSON := json.RawMessage(`{"keys":[{"kty":"RSA","e":"AQAB","kid":"ee8d626d","n":"gRda5b0pkgTytDuLrRnNSYhvfMIyM0ASq2ZggY4dVe12JV8N7lyXilyqLKleD-2lziivvzE8O8CdIC2vUf0tBD7VuMyldnZruSEZWCuKJPdgKgy9yPpShmD2NyhbwQIAbievGMJIp_JMwz8MkdY5pzhPECGNgCEtUAmsrrctP5V8HuxaxGt9bb-DdPXkYWXW3MPMSlVpGZ5GiIeTABxqYNG2MSoYeQ9x8O3y488jbassTqxExI_4w9MBQBJR9HIXjWrrrenCcDlMY71rzkbdj3mmcn9xMq2vB5OhfHyHTihbUPLSm83aFWSuW9lE7ogMc93XnrB8evIAk6VfsYlS9Q"},{"kty":"EC","crv":"P-256","kid":"711d48d1","x":"tfXCoBU-wXemeQCkME1gMZWK0-UECCHIkedASZR0t-Q","y":"9xzYtnKQdiQJHCtGwpZWF21eP1fy5x4wC822rCilmBw"},{"kty":"EC","crv":"P-384","kid":"d52c9829","x":"tFx6ev6eLs9sNfdyndn4OgbhV6gPFVn7Ul0VD5vwuplJLbIYeFLI6T42tTaE5_Q4","y":"A0gzB8TqxPX7xMzyHH_FXkYG2iROANH_kQxBovSeus6l_QSyqYlipWpBy9BhY9dz"},{"kty":"RSA","e":"AQAB","kid":"ecac72e5","n":"nLbnTvZAUxdmuAbDDUNAfha6mw0fri3UpV2w1PxilflBuSnXJhzo532-YQITogoanMjy_sQ8kHUhZYHVRR6vLZRBBbl-hP8XWiCe4wwioy7Ey3TiIUYfW-SD6I42XbLt5o-47IR0j5YDXxnX2UU7-UgR_kITBeLDfk0rSp4B0GUhPbP5IDItS0MHHDDS3lhvJomxgEfoNrp0K0Fz_s0K33hfOqc2hD1tSkX-3oDTQVRMF4Nxax3NNw8-ahw6HNMlXlwWfXodgRMvj9pcz8xUYa3C5IlPlZkMumeNCFx1qds6K_eYcU0ss91DdbhhE8amRX1FsnBJNMRUkA5i45xkOIx15rQN230zzh0p71jvtx7wYRr5pdMlwxV0T9Ck5PCmx-GzFazA2X6DJ0Xnn1-cXkRoZHFj_8Mba1dUrNz-NWEk83uW5KT-ZEbX7nzGXtayKWmGb873a8aYPqIsp6bQ_-eRBd8TDT2g9HuPyPr5VKa1p33xKaohz4DGy3t1Qpy3UWnbPXUlh5dLWPKz-TcS9FP5gFhWVo-ZhU03Pn6P34OxHmXGWyQao18dQGqzgD4e9vY3rLhfcjVZJYNlWY2InsNwbYS-DnienPf1ws-miLeXxNKG3tFydoQzHwyOxG6Wc-HBfzL_hOvxINKQamvPasaYWl1LWznMps6elKCgKDc"},{"kty":"EC","crv":"P-521","kid":"c570888f","x":"AHNpXq0J7rikNRlwhaMYDD8LGVAVJzNJ-jEPksUIn2LB2LCdNRzfAhgbxdQcWT9ktlc9M1EhmTLccEqfnWdGL9G1","y":"AfHPUW3GYzzqbTczcYR0nYMVMFVrYsUxv4uiuSNV_XRN3Jf8zeYbbOLJv4S3bUytO7qHY8bfZxPxR9nn3BBTf5ol"}]}`)

    // Create the JWKS from the resource at the given URL.
    jwks, err := keyfunc.NewJSON(jwksJSON)
    if err != nil {
        log.Fatalf("Failed to create JWKS from JSON.\nError: %s", err.Error())
    }

    // Get a JWT to parse.
    jwtB64 := "eyJraWQiOiJlZThkNjI2ZCIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJXZWlkb25nIiwiYXVkIjoiVGFzaHVhbiIsImlzcyI6Imp3a3Mtc2VydmljZS5hcHBzcG90LmNvbSIsImlhdCI6MTYzMTM2OTk1NSwianRpIjoiNDY2M2E5MTAtZWU2MC00NzcwLTgxNjktY2I3NDdiMDljZjU0In0.LwD65d5h6U_2Xco81EClMa_1WIW4xXZl8o4b7WzY_7OgPD2tNlByxvGDzP7bKYA9Gj--1mi4Q4li4CAnKJkaHRYB17baC0H5P9lKMPuA6AnChTzLafY6yf-YadA7DmakCtIl7FNcFQQL2DXmh6gS9J6TluFoCIXj83MqETbDWpL28o3XAD_05UP8VLQzH2XzyqWKi97mOuvz-GsDp9mhBYQUgN3csNXt2v2l-bUPWe19SftNej0cxddyGu06tXUtaS6K0oe0TTbaqc3hmfEiu5G0J8U6ztTUMwXkBvaknE640NPgMQJqBaey0E4u0txYgyvMvvxfwtcOrDRYqYPBnA"

    // Parse the JWT.
    token, err := jwt.Parse(jwtB64, jwks.Keyfunc)
    if err != nil {
        log.Fatalf("Failed to parse the JWT.\nError: %s", err.Error())
    }

    // Check if the token is valid.
    if !token.Valid {
        log.Fatalf("The token is not valid.")
    }
    log.Println("The token is valid.")
}

0
投票

我遇到了一个非常相似的用例,我想验证/验证访问令牌并提取字段(例如:

iss
sub
aud
exp
iat
jti
)等..)在解析/解码后从中得到。对于我的用例,我使用了 jwxjwt-go lib。

我尝试过 @tim-cooper 示例,但它无法编译/使用最新的 API 版本,所以这里是对我有用的代码片段。

代码片段

go.mod
module my-go-module

go 1.16

require (
    github.com/dgrijalva/jwt-go v3.2.0+incompatible
    github.com/lestrrat-go/jwx v1.0.4
)
代码
package main

import (
    "errors"
    "fmt"

    "github.com/dgrijalva/jwt-go"
    "github.com/lestrrat-go/jwx/jwa"
    "github.com/lestrrat-go/jwx/jwk"
)

func main() {
    jwksURL := "https://your-tenant.auth0.com/.well-known/jwks.json"

    keySet, _ := jwk.Fetch(jwksURL)
    var accessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6Ind5TXdLNEE2Q0w5UXcxMXVvZlZleVExMTlYeVgteHlreW1ra1h5Z1o1T00ifQ.eyJzdWIiOiIwMHUxOGVlaHUzNDlhUzJ5WDFkOCIsIm5hbWUiOiJva3RhcHJveHkgb2t0YXByb3h5IiwidmVyIjoxLCJpc3MiOiJodHRwczovL2NvbXBhbnl4Lm9rdGEuY29tIiwiYXVkIjoidlpWNkNwOHJuNWx4ck45YVo2ODgiLCJpYXQiOjE0ODEzODg0NTMsImV4cCI6MTQ4MTM5MjA1MywianRpIjoiSUQuWm9QdVdIR3IxNkR6a3RUbEdXMFI4b1lRaUhnVWg0aUotTHo3Z3BGcGItUSIsImFtciI6WyJwd2QiXSwiaWRwIjoiMDBveTc0YzBnd0hOWE1SSkJGUkkiLCJub25jZSI6Im4tMFM2X1d6QTJNaiIsInByZWZlcnJlZF91c2VybmFtZSI6Im9rdGFwcm94eUBva3RhLmNvbSIsImF1dGhfdGltZSI6MTQ4MTM4ODQ0MywiYXRfaGFzaCI6Im1YWlQtZjJJczhOQklIcV9CeE1ISFEifQ.OtVyCK0sE6Cuclg9VMD2AwLhqEyq2nv3a1bfxlzeS-bdu9KtYxcPSxJ6vxMcSSbMIIq9eEz9JFMU80zqgDPHBCjlOsC5SIPz7mm1Z3gCwq4zsFJ-2NIzYxA3p161ZRsPv_3bUyg9B_DPFyBoihgwWm6yrvrb4rmHXrDkjxpxCLPp3OeIpc_kb2t8r5HEQ5UBZPrsiScvuoVW13YwWpze59qBl_84n9xdmQ5pS7DklzkAVgqJT_NWBlb5uo6eW26HtJwHzss7xOIdQtcOtC1Gj3O82a55VJSQnsEEBeqG1ESb5Haq_hJgxYQnBssKydPCIxdZiye-0Ll9L8wWwpzwig"
    token, err := verify(accessToken, keySet)
    if err != nil {
        fmt.Printf("Gor an error while verifiying access token: %v\n", err)
    }

    // Check if the token is valid.
    if !token.Valid {
        fmt.Println("The token is not valid.")
    }

    // Extract key value from the token and print them on console
    claims := token.Claims.(jwt.MapClaims)
    for key, value := range claims {
        fmt.Printf("%s\t%v\n", key, value)
    }
}

func verify(tokenString string, keySet *jwk.Set) (*jwt.Token, error) {
    tkn, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if token.Method.Alg() != jwa.RS256.String() { 
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        kid, ok := token.Header["kid"].(string)
        if !ok {
            return nil, errors.New("kid header not found")
        }
        keys := keySet.LookupKeyID(kid)
        if len(keys) == 0 {
            return nil, fmt.Errorf("key %v not found", kid)
        }
        var raw interface{}
        return raw, keys[0].Raw(&raw)
    })
    return tkn, err
}
© www.soinside.com 2019 - 2024. All rights reserved.