简体   繁体   English

如何在 Go 中验证来自 AWS Cognito 的 JWT 令牌?

[英]How to verify a JWT Token from AWS Cognito in Go?

How can I validate and get info from a JWT received from Amazon Cognito?如何验证从 Amazon Cognito 收到的 JWT 并从中获取信息?

I have setup Google authentication in Cognito, and set the redirect uri to to hit API Gateway, I then receive a code which I POST to this endpoint:我在 Cognito 中设置了 Google 身份验证,并将重定向 uri 设置为命中 API 网关,然后我收到一个代码,我将其发布到此端点:

https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html

To receive the JWT token, in a RS256 format.以 RS256 格式接收 JWT 令牌。 I am now struggling to validate, and parse the token in Golang.我现在正在努力验证和解析 Golang 中的令牌。 I've tried to parse it using jwt-go, but it appears to support HMAC instead by default and read somewhere that they recommend using frontend validation instead.我尝试使用 jwt-go 对其进行解析,但它似乎默认支持 HMAC,并在他们推荐使用前端验证的地方读取。 I tried a few other packages and had similar problems.我尝试了其他一些软件包并遇到了类似的问题。

I came across this answer here: Go Language and Verify JWT but assume the code is outdated as that just says panic: unable to find key .我在这里遇到了这个答案: Go Language and Verify JWT ,但假设代码已经过时,因为它只是说panic: unable to find key

jwt.io can easily decode the key, and probably verify too. jwt.io 可以轻松解码密钥,并且可能也可以验证。 I'm not sure where the public/secret keys are as Amazon generated the token, but from what I understand I need to use a JWK URL to validate too?我不确定亚马逊生成令牌时公钥/密钥在哪里,但据我了解,我也需要使用 JWK URL 进行验证? I've found a few AWS specific solutions, but they all seem to be hundreds of lines long.我找到了一些 AWS 特定的解决方案,但它们似乎都有数百行长。 Surely it isn't that complicated in Golang is it? Golang 肯定没那么复杂吧?

Public keys for Amazon Cognito Amazon Cognito 的公钥

As you already guessed, you'll need the public key in order to verify the JWT token.正如您已经猜到的,您需要公钥来验证 JWT 令牌。

https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-step-2 https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-第2步

Download and store the corresponding public JSON Web Key (JWK) for your user pool.为您的用户池下载并存储相应的公共 JSON Web 密钥 (JWK)。 It is available as part of a JSON Web Key Set (JWKS).它作为 JSON Web 密钥集 (JWKS) 的一部分提供。 You can locate it at https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json您可以在 https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json 找到它

Parse keys and verify token解析密钥并验证令牌

That JSON file structure is documented in the web, so you could potentially parse that manually, generate the public keys, etc.该 JSON 文件结构记录在 Web 中,因此您可以手动解析它,生成公钥等。

But it'd probably be easier to just use a library, for example this one: https://github.com/lestrrat-go/jwx但是只使用一个库可能会更容易,例如这个: https ://github.com/lestrrat-go/jwx

And then jwt-go to deal with the JWT part: https://github.com/dgrijalva/jwt-go然后 jwt-go 处理 JWT 部分: https ://github.com/dgrijalva/jwt-go

You can then:然后您可以:

  1. Download and parse the public keys JSON using the first library使用第一个库下载并解析公钥 JSON

     keySet, err := jwk.Fetch(THE_COGNITO_URL_DESCRIBED_ABOVE)
  2. When parsing the token with jwt-go, use the "kid" field from the JWT header to find the right key to use使用 jwt-go 解析令牌时,使用 JWT 标头中的“kid”字段来查找要使用的正确密钥

     token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodRS256); !ok { 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 !ok { return nil, fmt.Errorf("key with specified kid is not present in jwks") } var publickey interface{} err = keys.Raw(&publickey) if err != nil { return nil, fmt.Errorf("could not parse pubkey") } return publickey, nil

The type assertion in the code provided by eugenioy and Kevin Wydler did not work for me: *jwt.SigningMethodRS256 is not a type . eugenioy 和 Kevin Wydler 提供的代码中的类型断言对我不起作用: *jwt.SigningMethodRS256 is not a type

*jwt.SigningMethodRS256 was a type in the initial commit. *jwt.SigningMethodRS256是初始提交中的一种类型。 From the second commit on (back in July 2014) it was abstracted and replaced by a global variable (see here ).从第二次提交(早在 2014 年 7 月)开始,它被抽象并替换为全局变量(参见此处)。

This following code works for me:以下代码适用于我:

func verify(tokenString string, keySet *jwk.Set) {
  tkn, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    if token.Method.Alg() != "RSA256" { // jwa.RS256.String() works as well
      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)
  })
}

Using the following dependency versions:使用以下依赖版本:

github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
github.com/lestrrat-go/jwx v1.0.4

This is what I did with only the latest ( v1.0.8 ) github.com/lestrrat-go/jwx .这就是我只使用最新的 ( v1.0.8 ) github.com/lestrrat-go/jwx的。 Note that github.com/dgrijalva/jwt-go does not seem to be maintained anymore and people are forking it to make the updates they need.请注意, github.com/dgrijalva/jwt-go似乎不再维护,人们正在分叉它以进行他们需要的更新。

package main

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

    keyset, err := jwk.Fetch("https://cognito-idp." + region + ".amazonaws.com/" + userPoolID + "/.well-known/jwks.json")

    parsedToken, err := jwt.Parse(
        bytes.NewReader(token), //token is a []byte
        jwt.WithKeySet(keyset),
        jwt.WithValidate(true),
        jwt.WithIssuer(...),
        jwt.WithClaimValue("key", value),
    )

    //check err as usual
    //here you can call methods on the parsedToken to get the claim values
    ...

Token claim methods 令牌索取方法

eugenioy's answer stopped working for me because of this refactor .由于这个重构,eugenioy 的回答不再为我工作。 I ended up fixing with something like this我最终解决了这样的问题

token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    if _, ok := token.Method.(*jwt.SigningMethodRS256); !ok {
        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)
    }
    // keys[0].Materialize() doesn't exist anymore
    var raw interface{}
    return raw, keys[0].Raw(&raw)
})

A newer method to achieve verification and access the token is to use Gin Cognito JWT Authentication Middleware :实现验证和访问令牌的新方法是使用Gin Cognito JWT Authentication Middleware

package main

import (
    jwtCognito "github.com/akhettar/gin-jwt-cognito"
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt"
    "log"
)

func main() {
    r := gin.Default()
    
    // Create the authentication middleware
    mw, err := jwtCognito.AuthJWTMiddleware(<iss>, <user_pool_id>, <region>)
    if err != nil {
        panic(err)
    }   
  
    r.GET("/someGet", mw.MiddlewareFunc(), func(c *gin.Context) {
        // Get the token
        tokenStr, _ := c.Get("JWT_TOKEN")
        token := tokenStr.(*jwt.Token)

        // Cast the claims
        claims := token.Claims.(jwt.MapClaims)

        log.Printf("userCognitoId=%v", claims["cognito:username"])
        log.Printf("userName=%v", claims["name"])
        
        c.Status(http.StatusOK)
    })

    // By default it serves on :8080
    r.Run()
}

This is what worked for me:这对我有用:

import (
    "errors"
    "fmt"
    "github.com/dgrijalva/jwt-go"
    "github.com/gin-gonic/gin"
    "github.com/lestrrat-go/jwx/jwk"
    "net/http"
    "os"
)

func verifyToken(token *jwt.Token) (interface{}, error) {
    // make sure to replace this with your actual URL
    // https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-step-2
    jwksURL := "COGNITO_JWKS_URL" 
    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")
    }

    keys := set.LookupKeyID(keyID)
    if len(keys) == 0 {
        return nil, fmt.Errorf("key %v not found", keyID)
    }

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

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

I am calling it like this (using AWS Lambda gin ) in my case.在我的情况下,我这样称呼它(使用 AWS Lambda gin )。 If you are using a different way of managing requests, make sure to replace that with http.Request or any other framework that you might be using:如果您使用不同的方式管理请求,请确保将其替换为http.Request或您可能正在使用的任何其他框架:

func JWTVerify() gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenString := c.GetHeader("AccessToken")
        _, err := jwt.Parse(tokenString, verifyToken)
        if err != nil {
            c.AbortWithStatus(http.StatusUnauthorized)
        }
    }
}

This is my go.mod :这是我的go.mod

module MY_MODULE_NAME
go 1.12

require (
    github.com/aws/aws-lambda-go v1.20.0
    github.com/aws/aws-sdk-go v1.36.0
    github.com/awslabs/aws-lambda-go-api-proxy v0.9.0
    github.com/dgrijalva/jwt-go v3.2.0+incompatible
    github.com/gin-gonic/gin v1.6.3
    github.com/google/uuid v1.1.2
    github.com/lestrrat-go/jwx v0.9.2
    github.com/onsi/ginkgo v1.14.2 // indirect
    github.com/onsi/gomega v1.10.3 // indirect
    golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)

Here's an example using github.com/golang-jwt/jwt , (formally known as github.com/dgrijalva/jwt-go ,) and a JWKs like the one AWS Cognito provides.这是一个使用github.com/golang-jwt/jwt (正式名称为github.com/dgrijalva/jwt-go )和 AWS Cognito 提供的 JWK 的示例。

It'll refresh the AWS Cognito JWKs once every hour, refresh when a JWT signed with an unknown kid comes in, and have a global rate limit of 1 HTTP request to refresh the JWKs every 5 minutes.它将每小时刷新一次 AWS Cognito JWK,当与未知kid签署的 JWT 进入时刷新,并且具有 1 个 HTTP 请求的全局速率限制,每 5 分钟刷新一次 JWK。

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/golang-jwt/jwt"

    "github.com/MicahParks/keyfunc"
)

func main() {

    // Get the JWKs URL from your AWS region and userPoolId.
    //
    // See the AWS docs here:
    // https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
    regionID := ""   // TODO Get the region ID for your AWS Cognito instance.
    userPoolID := "" // TODO Get the user pool ID of your AWS Cognito instance.
    jwksURL := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", regionID, userPoolID)

    // 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.
    refreshInterval := time.Hour
    refreshRateLimit := time.Minute * 5
    refreshTimeout := time.Second * 10
    refreshUnknownKID := true
    options := keyfunc.Options{
        RefreshErrorHandler: func(err error) {
            log.Printf("There was an error with the jwt.KeyFunc\nError:%s\n", err.Error())
        },
        RefreshInterval:   &refreshInterval,
        RefreshRateLimit:  &refreshRateLimit,
        RefreshTimeout:    &refreshTimeout,
        RefreshUnknownKID: &refreshUnknownKID,
    }

    // 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\n", err.Error())
    }

    // Get a JWT to parse.
    jwtB64 := "eyJraWQiOiJmNTVkOWE0ZSIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJLZXNoYSIsImF1ZCI6IlRhc2h1YW4iLCJpc3MiOiJqd2tzLXNlcnZpY2UuYXBwc3BvdC5jb20iLCJleHAiOjE2MTkwMjUyMTEsImlhdCI6MTYxOTAyNTE3NywianRpIjoiMWY3MTgwNzAtZTBiOC00OGNmLTlmMDItMGE1M2ZiZWNhYWQwIn0.vetsI8W0c4Z-bs2YCVcPb9HsBm1BrMhxTBSQto1koG_lV-2nHwksz8vMuk7J7Q1sMa7WUkXxgthqu9RGVgtGO2xor6Ub0WBhZfIlFeaRGd6ZZKiapb-ASNK7EyRIeX20htRf9MzFGwpWjtrS5NIGvn1a7_x9WcXU9hlnkXaAWBTUJ2H73UbjDdVtlKFZGWM5VGANY4VG7gSMaJqCIKMxRPn2jnYbvPIYz81sjjbd-sc2-ePRjso7Rk6s382YdOm-lDUDl2APE-gqkLWdOJcj68fc6EBIociradX_ADytj-JYEI6v0-zI-8jSckYIGTUF5wjamcDfF5qyKpjsmdrZJA"

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

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

    log.Println("The token is valid.")
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM