web-dev-qa-db-ja.com

APIバックエンドでAWS CognitoからJWTを検証する方法は?

Angular2シングルページアプリとECSで実行されるREST AP​​Iで構成されるシステムを構築しています。 APIは.Net/ Nancy で実行されますが、変更される可能性があります。

Cognitoを試してみたいと思いますが、これが認証ワークフローを想像した方法です。

  1. SPAはユーザーにサインインし、JWTを受け取ります
  2. SPAはリクエストごとにREST AP​​IにJWTを送信します
  3. REST APIは、JWTが本物であることを確認します

私の質問はステップ3についてです。サーバー(または:ステートレス、自動スケーリング、負荷分散されたDockerコンテナー)がトークンが本物であることを確認するにはどうすればよいですか? "サーバー"はJWT自体を発行していないため、独自のシークレットを使用できません(基本的なJWTの例で説明されているように here )。

Cognitoのドキュメントをよく読んでグーグルで調べましたが、サーバー側でJWTをどうするかについての良いガイドラインが見つかりません。

51
EagleBeak

私はドキュメントを正しく読んでいませんでした。 here (「Web APIでのIDトークンとアクセストークンの使用」までスクロールダウン)について説明しています。

APIサービスはCognitoのシークレットをダウンロードし、受信したJWTの検証に使用できます。パーフェクト。

編集

@Groadyのコメントは要点です:しかしhowトークンを検証しますか? jose4j または nimbus (両方ともJava)のようなバトルテスト済みのライブラリを使用し、自分で検証を最初から実装しないでください。

ここ は、Java/dropwizardサービスで最近これを実装する必要があったときに開始した、nimbusを使用したSpring Bootの実装例です。

28
EagleBeak

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 
22
FacePalm

同様の問題がありましたが、API Gatewayを使用していません。私の場合、AWS Cognito Developer Authenticated IDルートを介して取得したJWTトークンの署名を検証したかったのです。

さまざまなサイトの多くのポスターのように、AWS JWTトークンの署名を外部、つまりサーバー側またはスクリプト経由で検証するために必要なビットを正確につなぎ合わせるのに苦労しました

私は考え出してGistを AWS JWTトークン署名を検証する に当てはめたと思います。 PyCryptoのCrypto.SignatureのpyjwtまたはPKCS1_v1_5cのいずれかを使用してAWS JWT/JWSトークンを検証します

したがって、これは私の場合はpythonでしたが、ノードでも簡単に実行できます(npm install jsonwebtoken jwk-to-pemリクエスト)。

私はコメントでいくつかの落とし穴を強調しようとしましたが、これを理解しようとしていたとき、私はほとんど正しいことをしていましたが、python dictの順序や欠如、json表現などの微妙なニュアンスがありました。

誰かがどこかで役立つことを願っています。

8
David Kierans

認証コード付与フローの実行

次のことを前提としています:

  • 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は各ユーザープールに対して2組のRSA暗号キーを生成するため、トークンの暗号化に使用されたキーを把握する必要があります。

以下は、JWTの検証を示すNodeJSスニペットです。

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')
3
Derek Soike

短い答え:
次のエンドポイントからユーザープールの公開キーを取得できます。
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
この公開鍵を使用してトークンを正常にデコードした場合、トークンは有効です。それ以外は偽造されます。


長答:
cognitoによる認証に成功すると、アクセストークンとIDトークンを取得します。次に、このトークンが改ざんされているかどうかを検証します。従来、これらのトークンを認証サービス(最初にこのトークンを発行した)に送り返して、トークンが有効かどうかを確認していました。これらのシステムは、HMACなどのsymmetric key encryptionアルゴリズムを使用して、秘密鍵を使用してペイロードを暗号化するため、このシステムのみがこのトークンが有効かどうかを判断できます。
従来の認証JWTトークンヘッダー:

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

ここで使用される暗号化アルゴリズムは対称的であることに注意してください-HMAC + SHA256

しかし、Cognitoのような最新の認証システムは、RSAなどのasymmetric key encryptionアルゴリズムを使用して、公開鍵と秘密鍵のペアを使用してペイロードを暗号化します。ペイロードは秘密鍵を使用して暗号化されますが、公開鍵を介してデコードできます。このようなアルゴリズムを使用する主な利点は、トークンが有効かどうかを判断するために単一の認証サービスを要求する必要がないことです。誰もが公開キーにアクセスできるため、誰でもトークンの有効性を検証できます。検証の負荷はかなり分散されており、単一障害点はありません。
Cognito JWTトークンヘッダー:

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

この場合に使用される非対称暗号化アルゴリズム-RSA + SHA256

1
Gautam Jain

これはドットネット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
Arvind Krmar