如何在 API 后端从 AWS Cognito 验证 JWT?

How to verify JWT from AWS Cognito in the API backend?

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

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

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

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

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

原来我没看对文档。已解释 here(向下滚动至 "Using ID Tokens and Access Tokens in your Web APIs")。

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

编辑

@Groady 的评论很中肯:但是如何验证令牌?我会说为此使用像 jose4j or nimbus(两者都是 Java)这样经过实战检验的库,不要自己从头开始实施验证。

Here 是 Spring 使用灵气启动的示例实现,当我最近不得不在 java/dropwizard 服务中实现它时,它让我开始了。

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

像各种网站上的许多发帖人一样,我无法准确拼凑出在外部(即服务器端或通过脚本)验证 AWS JWT 令牌签名所需的位

我想我明白了,并把要点写给 verify an AWS JWT token signature。它将使用 PyCryptoCrypto.Signature 中的 pyjwt 或 PKCS1_v1_5c 验证 AWS JWT/JWS 令牌

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

我试图在评论中强调一些陷阱,因为当我试图解决这个问题时,我大部分时间都在做正确的事情,但也有一些细微差别,比如 python 字典排序,或者没有,并且json 代表。

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

这是一种在 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 

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


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

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

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

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

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

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

这在 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;
    }

执行授权码授予流程

假设您:

  • 已在 AWS Cognito 中正确配置用户池,并且
  • 能够signup/login并通过以下方式获取访问代码:

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

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


现在您需要将该代码传递给您的 back-end 并让它为您请求令牌。

POST https://<your-domain>.auth.us-west-2.amazoncognito.com/oauth2/token

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

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

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

现在,根据 documentation,您的 back-end 应通过以下方式验证 JWT 签名:

  1. 解码 ID 令牌
  2. 将本地密钥 ID (kid) 与 public kid
  3. 进行比较
  4. 使用 public 密钥通过您的 JWT 库验证签名。

由于 AWS Cognito 为每个用户池生成两对 RSA 加密密钥,您需要弄清楚使用哪个密钥来加密令牌。

这是一个 NodeJS 片段,演示了验证 JWT。

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')

这是基于 Derek () 的详尽解释。我已经能够为 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");
}

?>

您可以在此处从 Lambda 代码中获得见解

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

https://github.com/awslabs/aws-support-tools/tree/master/Cognito/decode-verify-jwt

在戈兰 https://gist.github.com/tmaiaroto/e2ee5e88fc6ae035307d7c5ee71a99cf

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

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

  • 最小依赖性
  • 框架无关
  • JWKS(public 个键)缓存
  • 测试覆盖率

用法(有关更详细的示例,请参阅 github 存储库):

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
}

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

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

该库具有与此处提到的其他库类似的机制,例如自动下载和缓存 JWKS(可用于验证 Cognito JWT 的 public 密钥)。它是用纯 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!");
}

(顺便说一下,该库还包括一个 class,它适用于 Cognito 以外的其他身份提供者)

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