如何在API后端验证来自AWS Cognito的JWT?

时间:2016-10-28 09:42:51

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

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

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

  1. SPA登录用户并收到JWT
  2. SPA会根据每个请求将JWT发送到REST API
  3. REST API验证JWT是真实的
  4. 我的问题是关于第3步。我的服务器(或者更确切地说:我的无状态,自动扩展,负载平衡的Docker容器)如何验证令牌是否可信?自“服务器”以来没有发布JWT本身,它不能使用自己的秘密(如基本的JWT示例here中所述)。

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

10 个答案:

答案 0 :(得分:28)

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

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

修改

@ Groady的评论很明确:但如何验证令牌?我会说使用经过实战考验的库,如jose4jnimbus(两者都是Java),并且不要自己从头开始实施验证。

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

答案 1 :(得分:20)

这是验证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 

答案 2 :(得分:8)

我有类似的问题,但没有使用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 request)。

我试图强调评论中的一些问题,因为当我试图解决这个问题时,我主要是做正确的事,但有一些细微差别,如p​​ython dict排序,或者缺少json和json表示。

希望它可以帮助某个人。

答案 3 :(得分: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
}

答案 4 :(得分:2)

执行授权码授予流程

假设您:

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

  • Authorization标头设置为Basic,并在AWS Cognito中配置的每个应用程序客户端使用username=<app client id>password=<app client secret>
  • 在请求正文中设置以下内容:
    • 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'
}

现在,根据documentation,您的后端应该通过以下方式验证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, function(err, decodedToken) {
        if (err) {
            console.error(err)
        } else {
            console.log(decodedToken)
        }
    })
}

function decodeTokenHeader(token) {
    const [headerEncoded] = token.split('.')[0]
    const buff = new Buffer(headerEncoded, 'base64')
    const text = buff.toString('ascii')
    return JSON.parse(text)
}

func 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'] }, function(err, decodedToken) {
        return clbk(err, decodedToken)
    })
}

validateToken('xxxxxxxxx.XXXXXXXX.xxxxxxxx')

答案 5 :(得分:2)

答案 6 :(得分:1)

简短答案:
您可以从以下端点获取用户池的公钥:
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

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

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

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

答案 7 :(得分:1)

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

答案 8 :(得分:0)

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

答案 9 :(得分:0)

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

?>