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


78

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

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

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

我的问题是关于步骤3的。我的服务器(或更确切地说:我的无状态,自动缩放,负载平衡的Docker容器)如何验证令牌是真实的?由于“服务器”还没有出台的智威汤逊本身,它不能使用自己的秘密(如在基本JWT例如描述这里)。

我已经阅读了Cognito文档并在Google上进行了大量搜索,但是我找不到在服务器端处理JWT的任何很好的指南。


2
如果您使用的是Node / Express应用程序,那么我已经创建了一个名为cognito-express的npm软件包,该软件包几乎可以完成您想要做的事情-从您的Cognito用户池中下载JWK,并验证ID的JWT的签名令牌或访问令牌。
ghdna

@ghdna我最近下载了cognito-express并将其安装在我的服务器上,但是从客户端的Cognito中,我仅获得accessKey,secretKey,sessionKey和到期时间。我找不到从任何地方返回的ID令牌或访问令牌。那里也有一个刷新令牌。因此,目前我从cogito-express进入控制台的唯一原因是标头中缺少访问令牌,或者不是有效的JWT。有指针吗?
elarcoiris

我希望您可以为JWT验证提供一个清晰的代码示例,按照aws quickstart项目,对JWT进行解码(base64转换)以获取“孩子”,然后从URL中获取JWK,转换为PEM,然后进行验证。我被困在PEM转换中。
Abdeali Chandanwala

Answers:


43

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

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

编辑

@Groady的评论很重要:但是您如何验证令牌?我想说的是,使用经过久经考验的库,例如jose4jnimbus(均为Java),不要自己从头开始进行验证。

是使用nimbus的Spring Boot的示例实现,当我最近不得不在java / dropwizard服务中实现它时,这使我开始着手。


63
该文档充其量是废话。步骤6说“验证已解码的JWT令牌的签名” ...是的。根据此博客文章,您需要将JWK转换为PEM。他们能不能在官方文档中添加如何执行此操作?

我正在研究Groady。根据您的库,您无需转换为pem。例如,我在Elixir上,Joken完全按照Amazon提供的方式获取RSA密钥映射。当我以为钥匙必须是琴弦时,我花了很多时间旋转轮子。
法律

感谢您的示例链接!帮助您了解如何使用nimbus库。但是,如果可以将远程JWK集提取为外部缓存,有什么想法吗?我想将JWKSet放在Elasticache中。
Eric B.

32

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

谢谢,保存了我的一天!
Nirojan Selvanathan

2
谢谢你!将JWK转换为PEM时,还需要考虑很多细节:aws.amazon.com/blogs/mobile/…–
redgeoff

1
我们是否应该将JWK的内容保存在本地配置中以供重复使用?此内容将来会过期还是变得无效?
Nghia

@Nghia“与其直接从Lambda函数下载JWK Set,还可以手动下载一次,将密钥转换为PEM,然后使用Lambda函数上传它们。” 从aws.amazon.com/blogs/mobile/...
R.Cha

20

执行授权码授予流程

假设您:

  • 在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>
    

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


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

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

  • 您的设置AuthorizationBasic和使用username=<app client id>以及password=<app client secret>每个在AWS Cognito配置您的应用程序客户端
  • 在请求正文中设置以下内容:
    • grant_type=authorization_code
    • code=<your-code>
    • client_id=<your-client-id>
    • redirect_uri=<your-redirect-uri>

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

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

现在,根据文档,您的后端应通过以下方法验证JWT签名:

  1. 解码ID令牌
  2. 将本地密钥ID(孩子)与公共孩子进行比较
  3. 使用公共密钥通过您的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')

<app client id>一样的<your-client-id>吗?
Zach Saucier

在上面回答我的问题:如果您在标头中提供了一个秘密,则是必须的,但这不是必需的。
扎克·索西耶

new Buffer(headerEncoded, 'base64')现在应该是Buffer.from(headerEncoded, 'base64')
Zach Saucier

9

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

就像各个站点上的许多海报一样,我在将外部验证(即服务器端或通过脚本)AWS JWT令牌的签名所需的位准确地拼凑在一起时遇到了麻烦

我想我想出了要点来验证AWS JWT令牌签名。它将使用来自Crypto的pyjwt或PKCS1_v1_5c验证一个AWS JWT / JWS令牌.PyCrypto中的签名

所以,是的,在我的情况下,这是python,但在节点中也很容易做到(npm install jsonwebtoken jwk-to-pem request)。

我试图在注释中突出显示一些陷阱,因为当我试图弄清楚这一点时,我基本上是在做正确的事情,但是还是有一些细微差别,例如python dict order或缺少json表示。

希望它可以对某人有所帮助。


9

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


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

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

请注意,此处使用的加密算法是对称的-HMAC + SHA256,

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

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

在这种情况下使用的非对称加密算法-RSA + SHA256


5

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
}


1

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


0

这是基于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");
}

?>
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.