【问题标题】:How to verify JWT from AWS Cognito in the API backend?如何在 API 后端从 AWS Cognito 验证 JWT?
【发布时间】:2017-03-11 04:26:28
【问题描述】:

我正在构建一个由 Angular2 单页应用程序和在 ECS 上运行的 REST API 组成的系统。 API 在 .Net/Nancy 上运行,但这种情况很可能会改变。

我想尝试一下 Cognito,这就是我想象的身份验证工作流程:

  1. SPA 登录用户并接收 JWT
  2. SPA 在每个请求中都将 JWT 发送到 REST API
  3. REST API 验证 JWT 是真实的

我的问题是关于第 3 步的。我的服务器(或者更确切地说:我的无状态、自动缩放、负载平衡的 Docker 容器)如何验证令牌是真实的? 因为“服务器”还没有发布 JWT 本身,它不能使用自己的 secret(如基本 JWT 示例here 中所述)。

我已阅读 Cognito 文档并在 Google 上搜索了很多内容,但我找不到任何关于如何在服务器端处理 JWT 的好的指南。

【问题讨论】:

  • 如果您使用的是 Node/Express 应用程序,我创建了一个名为 cognito-express 的 npm 包,它几乎可以满足您的需求 - 从您的 Cognito 用户池下载 JWK并验证 ID Token 或 Access Token 的 JWT 签名。
  • @ghdna 我最近下载了 cognito-express 并将其安装在我的服务器上,但从我客户端的 Cognito 中,我只能获得 accessKey、secretKey、sessionKey 和过期时间。我找不到从任何地方返回的 ID 令牌或访问令牌。那里也有一个刷新令牌。因此,我现在从 cogito-express 控制台中得到的只是标题中缺少访问令牌或不是有效的 JWT。有什么指点吗?
  • 我希望你能给出一个清晰的 JWT 验证代码示例,根据 aws quickstart 项目,JWT 被解码(base64 转换)以获取“孩子”,然后从 url 获取 JWK,转换为 PEM,然后验证。我被困在 PEM 转换中。

标签: authentication amazon-ec2 jwt amazon-cognito amazon-ecs


【解决方案1】:

原来我没有正确阅读文档。解释了here(向下滚动到“在您的 Web API 中使用 ID 令牌和访问令牌”)。

API 服务可以下载 Cognito 的机密并使用它们来验证收到的 JWT。完美。

编辑

@Groady 的评论是正确的:但是如何您验证令牌?我会说使用像jose4jnimbus(都是Java)这样久经考验的库,并且不要自己从头开始实施验证。

Here 是一个使用 nimbus 的 Spring Boot 示例实现,当我最近不得不在 java/dropwizard 服务中实现它时,它让我开始了。

【讨论】:

  • 文档充其量是垃圾。第 6 步说“验证解码的 JWT 令牌的签名”...是的...如何!?!?根据此this blog post,您需要将 JWK 转换为 PEM。他们不能把如何做到这一点放在官方文档上吗?!
  • Groady 的后续行动,因为我正在经历这个。根据您的库,您不需要转换为 pem。例如,我在 Elixir 上,Joken 完全按照亚马逊提供的 RSA 密钥映射。当我认为钥匙必须是一根绳子时,我花了很多时间转动我的轮子。
  • 感谢示例链接!对理解如何使用 nimbus 库有很大帮助。但是,如果我可以将远程 JWK 集提取为外部缓存,有什么想法吗?我想将 JWKSet 放在 Elasticache 中。
【解决方案2】:

我遇到了类似的问题,但没有使用 API 网关。就我而言,我想验证通过 AWS Cognito Developer Authenticated 身份路由获得的 JWT 令牌的签名。

与各个网站上的许多海报一样,我无法准确拼凑出我需要在外部(即服务器端或通过脚本)验证 AWS JWT 令牌签名的位

我想我想通了并向verify an AWS JWT token signature 提了一个要点。它将使用 PyCrypto 中 Crypto.Signature 的 pyjwt 或 PKCS1_v1_5c 验证 AWS JWT/JWS 令牌

所以,是的,在我的情况下这是 python,但它在节点中也很容易实现(npm install jsonwebtoken jwk-to-pem 请求)。

我试图强调 cmets 中的一些陷阱,因为当我试图弄清楚这一点时,我大部分时间都在做正确的事情,但存在一些细微差别,例如 python dict ordering,或缺少它,以及 json 表示。

希望它可以帮助某个地方的人。

【讨论】:

    【解决方案3】:

    这是在 NodeJS 上验证签名的一种方法:

    var jwt = require('jsonwebtoken');
    var jwkToPem = require('jwk-to-pem');
    var pem = jwkToPem(jwk);
    jwt.verify(token, pem, function(err, decoded) {
      console.log(decoded)
    });
    
    
    // Note : You can get jwk from https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json 
    

    【讨论】:

    • 谢谢,拯救了我的一天!
    • 谢谢!在将 JWK 转换为 PEM 时,我还需要考虑很多细节:aws.amazon.com/blogs/mobile/…
    • 我们是否应该将 JWK 的内容保存在本地配置中以供重复使用?此内容是否会过期或将来失效?
    • @Nghia “您可以手动下载一次,将密钥转换为 PEM 并使用您的 Lambda 函数上传,而不是直接从您的 Lambda 函数下载 JWK 集。”来自aws.amazon.com/blogs/mobile/…
    【解决方案4】:

    简答:
    您可以从以下端点获取用户池的公钥:
    https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
    如果您使用此公钥成功解码令牌,则该令牌是有效的,否则它是伪造的。


    长答案:
    通过 cognito 成功进行身份验证后,您将获得访问和 id 令牌。现在您要验证此令牌是否已被篡改。传统上,我们会将这些令牌发送回身份验证服务(首先颁发此令牌)以检查令牌是否有效。这些系统使用symmetric key encryption 算法(例如HMAC)使用密钥加密有效负载,因此只有该系统能够判断此令牌是否有效。
    传统认证 JWT 令牌标头:

    {
       "alg": "HS256",
       "typ": "JWT"
    }
    

    注意这里使用的加密算法是对称的 - HMAC + SHA256

    但是像 Cognito 这样的现代身份验证系统使用 asymmetric key encryption 算法(如 RSA)使用一对公钥和私钥来加密有效负载。有效载荷使用私钥加密,但可以通过公钥解码。使用这种算法的主要优点是我们不必请求单个身份验证服务来判断令牌是否有效。由于每个人都可以访问公钥,因此任何人都可以验证令牌的有效性。验证负载相当分散,没有单点故障。
    Cognito JWT 令牌标头:

    {
      "kid": "abcdefghijklmnopqrsexample=",
      "alg": "RS256"
    }
    

    本例使用的非对称加密算法——RSA + SHA256

    【讨论】:

      【解决方案5】:

      这在 dot net 4.5 中对我有用

          public static bool VerifyCognitoJwt(string accessToken)
          {
              string[] parts = accessToken.Split('.');
      
              string header = parts[0];
              string payload = parts[1];
      
              string headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
              JObject headerData = JObject.Parse(headerJson);
      
              string payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
              JObject payloadData = JObject.Parse(payloadJson);
      
              var kid = headerData["kid"];
              var iss = payloadData["iss"];
      
              var issUrl = iss + "/.well-known/jwks.json";
              var keysJson= string.Empty;
      
              using (WebClient wc = new WebClient())
              {
                  keysJson = wc.DownloadString(issUrl);
              }
      
              var keyData = GetKeyData(keysJson,kid.ToString());
      
              if (keyData==null)
                  throw new ApplicationException(string.Format("Invalid signature"));
      
              var modulus = Base64UrlDecode(keyData.Modulus);
              var exponent = Base64UrlDecode(keyData.Exponent);
      
              RSACryptoServiceProvider provider = new RSACryptoServiceProvider();
      
              var rsaParameters= new RSAParameters();
              rsaParameters.Modulus = new BigInteger(modulus).ToByteArrayUnsigned();
              rsaParameters.Exponent = new BigInteger(exponent).ToByteArrayUnsigned();
      
              provider.ImportParameters(rsaParameters);
      
              SHA256CryptoServiceProvider sha256 = new SHA256CryptoServiceProvider();
              byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(parts[0] + "." + parts[1]));
      
              RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(provider);
              rsaDeformatter.SetHashAlgorithm(sha256.GetType().FullName);
      
              if (!rsaDeformatter.VerifySignature(hash, Base64UrlDecode(parts[2])))
                  throw new ApplicationException(string.Format("Invalid signature"));
      
              return true;
          }
      
       public class KeyData
          {
              public string Modulus { get; set; }
              public string Exponent { get; set; }
          }
      
          private static KeyData GetKeyData(string keys,string kid)
          {
              var keyData = new KeyData();
      
              dynamic obj = JObject.Parse(keys);
              var results = obj.keys;
              bool found = false;
      
              foreach (var key in results)
              {
                  if (found)
                      break;
      
                  if (key.kid == kid)
                  {
                      keyData.Modulus = key.n;
                      keyData.Exponent = key.e;
                      found = true;
                  }
              }
      
              return keyData;
          }
      

      【讨论】:

        【解决方案6】:

        执行授权码授予流程

        假设你:

        • 已在 AWS Cognito 中正确配置了用户池,并且
        • 可以通过以下方式注册/登录并获得访问代码:

          https://<your-domain>.auth.us-west-2.amazoncognito.com/login?response_type=code&client_id=<your-client-id>&redirect_uri=<your-redirect-uri>
          

        您的浏览器应该重定向到&lt;your-redirect-uri&gt;?code=4dd94e4f-3323-471e-af0f-dc52a8fe98a0


        现在您需要将该代码传递到您的后端并让它为您请求令牌。

        POST https://&lt;your-domain&gt;.auth.us-west-2.amazoncognito.com/oauth2/token

        • 将您的 Authorization 标头设置为 Basic 并根据您在 AWS Cognito 中配置的应用程序客户端使用 username=&lt;app client id&gt;password=&lt;app client secret&gt;
        • 在您的请求正文中设置以下内容:
          • grant_type=authorization_code
          • code=&lt;your-code&gt;
          • client_id=&lt;your-client-id&gt;
          • redirect_uri=&lt;your-redirect-uri&gt;

        如果成功,您的后端应该会收到一组 base64 编码的令牌。

        {
            id_token: '...',
            access_token: '...',
            refresh_token: '...',
            expires_in: 3600,
            token_type: 'Bearer'
        }
        

        现在,根据documentation,您的后端应该通过以下方式验证 JWT 签名:

        1. 解码 ID 令牌
        2. 将本地密钥 ID(孩子)与公共孩子进行比较
        3. 使用公钥来验证使用您的 JWT 库的签名。

        由于 AWS Cognito 为每个用户池生成两对 RSA 加密密钥,因此您需要确定用于加密令牌的密钥。

        这是一个演示验证 JWT 的 NodeJS sn-p。

        import jsonwebtoken from 'jsonwebtoken'
        import jwkToPem from 'jwk-to-pem'
        
        const jsonWebKeys = [  // from https://cognito-idp.us-west-2.amazonaws.com/<UserPoolId>/.well-known/jwks.json
            {
                "alg": "RS256",
                "e": "AQAB",
                "kid": "ABCDEFGHIJKLMNOPabc/1A2B3CZ5x6y7MA56Cy+6ubf=",
                "kty": "RSA",
                "n": "...",
                "use": "sig"
            },
            {
                "alg": "RS256",
                "e": "AQAB",
                "kid": "XYZAAAAAAAAAAAAAAA/1A2B3CZ5x6y7MA56Cy+6abc=",
                "kty": "RSA",
                "n": "...",
                "use": "sig"
            }
        ]
        
        function validateToken(token) {
            const header = decodeTokenHeader(token);  // {"kid":"XYZAAAAAAAAAAAAAAA/1A2B3CZ5x6y7MA56Cy+6abc=", "alg": "RS256"}
            const jsonWebKey = getJsonWebKeyWithKID(header.kid);
            verifyJsonWebTokenSignature(token, jsonWebKey, (err, decodedToken) => {
                if (err) {
                    console.error(err);
                } else {
                    console.log(decodedToken);
                }
            })
        }
        
        function decodeTokenHeader(token) {
            const [headerEncoded] = token.split('.');
            const buff = new Buffer(headerEncoded, 'base64');
            const text = buff.toString('ascii');
            return JSON.parse(text);
        }
        
        function getJsonWebKeyWithKID(kid) {
            for (let jwk of jsonWebKeys) {
                if (jwk.kid === kid) {
                    return jwk;
                }
            }
            return null
        }
        
        function verifyJsonWebTokenSignature(token, jsonWebKey, clbk) {
            const pem = jwkToPem(jsonWebKey);
            jsonwebtoken.verify(token, pem, {algorithms: ['RS256']}, (err, decodedToken) => clbk(err, decodedToken))
        }
        
        
        validateToken('xxxxxxxxx.XXXXXXXX.xxxxxxxx')
        

        【讨论】:

        • &lt;app client id&gt;&lt;your-client-id&gt;一样吗?
        • 回答我上面的问题:如果您在标题中提供秘密,则在正文中没有必要。
        • new Buffer(headerEncoded, 'base64') 现在应该是 Buffer.from(headerEncoded, 'base64')
        • 这是一个绝妙的答案,为我节省了很多时间!我创建了一个工作示例,演示了使用下面的令牌验证程序包的完整流程。 gitlab.com/danderson00/cognito-srp-js
        【解决方案7】:

        这是基于Derek (answer) 的详细解释。我已经能够为 PHP 创建一个工作示例。

        我已使用https://github.com/firebase/php-jwt 进行 pem 创建和代码验证。

        此代码在您收到一组 base64 编码令牌后使用。

        <?php
        
        require_once(__DIR__ . '/vendor/autoload.php');
        
        use Firebase\JWT\JWT;
        use Firebase\JWT\JWK;
        use Firebase\JWT\ExpiredException;
        use Firebase\JWT\SignatureInvalidException;
        use Firebase\JWT\BeforeValidException;
        
        function debugmsg($msg, $output) {
            print_r($msg . "\n");
        }
        
        $tokensReceived = array(
            'id_token' => '...',
            'access_token' => '...',
            'refresh_token' => '...',
            'expires_in' => 3600,
            'token_type' => 'Bearer'
        );
        
        $idToken = $tokensReceived['id_token'];
        
        // 'https://cognito-idp.us-west-2.amazonaws.com/<pool-id>/.well-known/jwks.json'
        $keys = json_decode('<json string received from jwks.json>');
        
        $idTokenHeader = json_decode(base64_decode(explode('.', $idToken)[0]), true);
        print_r($idTokenHeader);
        
        $remoteKey = null;
        
        $keySets = JWK::parseKeySet($keys);
        
        $remoteKey = $keySets[$idTokenHeader['kid']];
        
        try {
            print_r("result: ");
            $decoded = JWT::decode($idToken, $remoteKey, array($idTokenHeader['alg']));
            print_r($decoded);
        } catch(Firebase\JWT\ExpiredException $e) {
            debugmsg("ExpiredException","cognito");
        } catch(Firebase\JWT\SignatureInvalidException $e) {
            debugmsg("SignatureInvalidException","cognito");
        } catch(Firebase\JWT\BeforeValidException $e) {
            debugmsg("BeforeValidException","cognito");
        }
        
        ?>
        

        【讨论】:

          【解决方案8】:

          【讨论】:

          • Awslabs 是一个很好的资源,即使示例实现适用于 Lambda。他们使用python-jose 来解码和验证 JWT。
          【解决方案9】:

          cognito-jwt-verifier 是一个小型 npm 包,用于验证 ID 并访问从您的节点/Lambda 后端中的 AWS Cognito 获得的 JWT 令牌,并且具有最小的依赖关系。

          免责声明:我是本文的作者。我想出了它,因为我找不到任何为我检查所有框的东西:

          • 最小的依赖关系
          • 与框架无关
          • JWKS(公钥)缓存
          • 测试覆盖率

          用法(更详细的例子见 github repo):

          const { verifierFactory } = require('@southlane/cognito-jwt-verifier')
           
          const verifier = verifierFactory({
            region: 'us-east-1',
            userPoolId: 'us-east-1_PDsy6i0Bf',
            appClientId: '5ra91i9p4trq42m2vnjs0pv06q',
            tokenType: 'id', // either "access" or "id"
          })
          
          const token = 'eyJraWQiOiI0UFFoK0JaVE...' // clipped 
           
          try {
            const tokenPayload = await verifier.verify(token)
          } catch (e) {
            // catch error and act accordingly, e.g. throw HTTP 401 error
          }
          

          【讨论】:

            【解决方案10】:

            有人还编写了一个名为 cognitojwt 的 python 包,它可以在异步/同步模式下解码和验证 Amazon Cognito JWT。

            【讨论】:

              【解决方案11】:

              AWS 专门为此发布了一个 NodeJS 库:https://github.com/awslabs/aws-jwt-verify

              该库与此处提到的其他库具有类似的机制,例如自动下载和缓存 JWKS(可以用来验证 Cognito JWT 的公钥)。它是用纯 TypeScript 编写的,并且有 0 个依赖项。

              import { CognitoJwtVerifier } from "aws-jwt-verify";
              
              // Verifier that expects valid access tokens:
              const verifier = CognitoJwtVerifier.create({
                userPoolId: "<user_pool_id>",
                tokenUse: "access",
                clientId: "<client_id>",
              });
              
              try {
                const payload = await verifier.verify(
                  "eyJraWQeyJhdF9oYXNoIjoidk..." // the JWT as string
                );
                console.log("Token is valid. Payload:", payload);
              } catch {
                console.log("Token not valid!");
              }
              

              (顺便说一句,该库还包括一个适用于除 Cognito 之外的其他身份提供者的类)

              免责声明:我是图书馆的作者之一。我们期待客户的反馈——请给我们留下一个 GitHub 问题。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2021-10-03
                • 2018-03-07
                • 2019-04-14
                • 2022-01-26
                • 2018-06-29
                • 2021-06-11
                • 2019-11-16
                • 2017-05-27
                相关资源
                最近更新 更多