Wie überprüfe ich JWT von AWS Cognito im API-Backend?

78

Ich baue ein System auf, das aus einer Angular2-App für einzelne Seiten und einer REST-API besteht, die auf ECS ausgeführt wird. Die API läuft auf .Net / Nancy , aber das könnte sich ändern.

Ich möchte Cognito ausprobieren und so stellte ich mir den Authentifizierungsworkflow vor:

  1. SPA meldet sich beim Benutzer an und erhält eine JWT
  2. SPA sendet JWT bei jeder Anforderung an die REST-API
  3. Die REST-API überprüft, ob das JWT authentisch ist

Meine Frage bezieht sich auf Schritt 3. Wie kann mein Server (oder besser gesagt: meine zustandslosen, automatisch skalierten Docker-Container mit Lastenausgleich) überprüfen, ob das Token authentisch ist? Da der "Server" das JWT selbst nicht ausgegeben hat, kann er kein eigenes Geheimnis verwenden (wie im grundlegenden JWT-Beispiel hier beschrieben ).

Ich habe die Cognito-Dokumente gelesen und viel gegoogelt, aber ich kann keine gute Richtlinie darüber finden, was mit dem JWT auf der Serverseite zu tun ist.

EagleBeak
quelle
2
Wenn Sie eine Node / Express-App verwenden, habe ich ein npm-Paket namens cognito-express erstellt, das genau das tut, was Sie möchten - die JWKs aus Ihrem Cognito-Benutzerpool herunterladen und die Signatur der JWT der ID überprüfen Token oder Zugriffstoken.
Ghdna
@ghdna Ich habe kürzlich cognito-express heruntergeladen und auf meinem Server installiert, aber von Cognito auf meiner Clientseite erhalte ich nur accessKey, secretKey, sessionKey und expiration. Ich kann kein ID-Token oder Zugriffstoken finden, das von irgendwoher zurückgegeben wird. Irgendwo da draußen gibt es auch ein Aktualisierungs-Token. Alles, was ich im Moment von cogito-express in meine Konsole bekomme, ist, dass Access Token im Header fehlt oder kein gültiger JWT ist. Irgendwelche Hinweise?
Elarcoiris
Ich hoffe, Sie hätten ein klares Codebeispiel für die JWT-Validierung geben können, da gemäß einem aws-Schnellstartprojekt die JWT dekodiert wird (base64-Konvertierung), um das "Kind" zu erhalten, dann die JWK von der URL abzurufen, in PEM zu konvertieren und dann zu validieren. Ich stecke in der PEM-Konvertierung fest.
Abdeali Chandanwala

Antworten:

43

Es stellte sich heraus, dass ich die Dokumente nicht richtig gelesen habe. Dies wird hier erklärt (scrollen Sie nach unten zu "Verwenden von ID-Token und Zugriffstoken in Ihren Web-APIs").

Der API-Dienst kann die Geheimnisse von Cognito herunterladen und zur Überprüfung der empfangenen JWTs verwenden. Perfekt.

Bearbeiten

@ Groadys Kommentar ist auf den Punkt gebracht: Aber wie validieren Sie die Token? Ich würde sagen, verwenden Sie dafür eine kampferprobte Bibliothek wie jose4j oder nimbus (beide Java) und implementieren Sie die Überprüfung nicht von Grund auf selbst.

Hier ist eine Beispielimplementierung für Spring Boot mit nimbus, mit der ich angefangen habe, als ich dies kürzlich im Java / Dropwizard-Dienst implementieren musste.

EagleBeak
quelle
63
Die Dokumentation ist bestenfalls Mist. In Schritt 6 heißt es "Überprüfen Sie die Signatur des entschlüsselten JWT-Tokens" ... ja ... WIE!?!? Entsprechend diesem Blog-Beitrag müssen Sie die JWK in eine PEM konvertieren. Könnten sie nicht in den offiziellen Dokumenten vermerken, wie das geht?!
Wird
Ein Follow-up zu Groady, während ich das durchmache. Abhängig von Ihrer Bibliothek sollten Sie nicht in pem konvertieren müssen. Zum Beispiel bin ich bei Elixir und Joken nimmt die RSA-Schlüsselkarte genau so, wie sie von Amazon bereitgestellt wird. Ich habe viel Zeit damit verbracht, meine Räder zu drehen, als ich dachte, der Schlüssel müsse eine Schnur sein.
Gesetz
Danke für den Beispiellink! Hat mir sehr geholfen zu verstehen, wie man die Nimbus-Bibliothek benutzt. Irgendwelche Ideen, ob ich das Remote-JWK-Set als externen Cache extrahieren kann? Ich möchte stattdessen das JWKSet in Elasticache einfügen.
Eric B.
32

Hier ist eine Möglichkeit, die Signatur auf NodeJS zu überprüfen:

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 
FacePalm
quelle
Danke, hat meinen Tag gerettet!
Nirojan Selvanathan
2
Danke dafür! Es gab auch eine Reihe von Details, die ich bei der Konvertierung des JWK in eine PEM berücksichtigen musste
redgeoff
1
Sollten wir den Inhalt von JWKs in der lokalen Konfiguration zur Wiederverwendung speichern? Läuft dieser Inhalt ab oder wird er in Zukunft ungültig?
Nghia
@Nghia "Anstatt das JWK-Set direkt von Ihrer Lambda-Funktion herunterzuladen, können Sie es einmal manuell herunterladen, die Schlüssel in PEMs konvertieren und sie mit Ihrer Lambda-Funktion hochladen." von aws.amazon.com/blogs/mobile/…
R.Cha
20

Führen Sie einen Berechtigungscode-Grant-Flow aus

Angenommen, Sie:

  • einen Benutzerpool in AWS Cognito korrekt konfiguriert haben und
  • können sich anmelden / anmelden und erhalten einen Zugangscode über:

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

Ihr Browser sollte zu umleiten <your-redirect-uri>?code=4dd94e4f-3323-471e-af0f-dc52a8fe98a0


Jetzt müssen Sie diesen Code an Ihr Back-End übergeben und ein Token für Sie anfordern lassen.

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

  • stellen Sie Ihren AuthorizationKopf Basicund Verwendung username=<app client id>und password=<app client secret>pro App - Client konfiguriert in AWS Cognito
  • Stellen Sie in Ihrem Anfragetext Folgendes ein:
    • grant_type=authorization_code
    • code=<your-code>
    • client_id=<your-client-id>
    • redirect_uri=<your-redirect-uri>

Bei Erfolg sollte Ihr Back-End einen Satz von Base64-codierten Token erhalten.

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

Gemäß der Dokumentation sollte Ihr Back-End nun die JWT-Signatur überprüfen durch:

  1. ID-Token dekodieren
  2. Vergleich der lokalen Schlüssel-ID (Kind) mit dem öffentlichen Kind
  3. Verwenden des öffentlichen Schlüssels zum Überprüfen der Signatur mithilfe Ihrer JWT-Bibliothek.

Da AWS Cognito für jeden Benutzerpool zwei Paare von RSA-Kryptografieschlüsseln generiert, müssen Sie herausfinden, welcher Schlüssel zum Verschlüsseln des Tokens verwendet wurde.

Hier ist ein NodeJS- Snippet, das die Überprüfung eines JWT demonstriert.

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 Soike
quelle
Ist <app client id>das gleiche wie <your-client-id>?
Zach Saucier
Beantwortung meiner obigen Frage: Es ist aber im Körper nicht notwendig, wenn Sie ein Geheimnis in der Kopfzeile angeben.
Zach Saucier
new Buffer(headerEncoded, 'base64')sollte jetzt seinBuffer.from(headerEncoded, 'base64')
Zach Saucier
9

Ich hatte ein ähnliches Problem, aber ohne das API-Gateway zu verwenden. In meinem Fall wollte ich die Signatur eines JWT-Tokens überprüfen, das über die authentifizierte Identitätsroute von AWS Cognito Developer erhalten wurde.

Wie bei vielen Postern auf verschiedenen Websites hatte ich Probleme, genau die Bits zusammenzusetzen, die ich zum Überprüfen der Signatur eines AWS JWT-Tokens extern, dh serverseitig oder per Skript, benötige

Ich glaube, ich habe es herausgefunden und einen Kern erstellt, um eine AWS JWT-Token-Signatur zu überprüfen . Es wird ein AWS JWT / JWS-Token mit pyjwt oder PKCS1_v1_5c von Crypto.Signature in PyCrypto überprüft

Also, ja, das war in meinem Fall Python, aber es ist auch einfach im Knoten machbar (npm install jsonwebtoken jwk-to-pem request).

Ich habe versucht, einige Fallstricke in den Kommentaren hervorzuheben, weil ich, als ich versuchte, dies herauszufinden, meistens das Richtige tat, aber es gab einige Nuancen wie die Reihenfolge der Python-Diktate oder das Fehlen einer solchen und die json-Darstellung.

Hoffentlich kann es jemandem irgendwo helfen.

David Kierans
quelle
9

Kurze Antwort:
Sie können den öffentlichen Schlüssel für Ihren Benutzerpool vom folgenden Endpunkt abrufen:
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
Wenn Sie das Token mit diesem öffentlichen Schlüssel erfolgreich dekodieren, ist das Token gültig, andernfalls wird es gefälscht.


Lange Antwort:
Nachdem Sie sich erfolgreich über Cognito authentifiziert haben, erhalten Sie Ihre Zugriffs- und ID-Token. Jetzt möchten Sie überprüfen, ob dieses Token manipuliert wurde oder nicht. Normalerweise senden wir diese Token an den Authentifizierungsdienst zurück (der dieses Token an erster Stelle ausgestellt hat), um zu überprüfen, ob das Token gültig ist. Diese Systeme verwenden symmetric key encryptionAlgorithmen HMAC, um die Nutzdaten mit einem geheimen Schlüssel zu verschlüsseln. Nur dieses System kann daher feststellen, ob dieses Token gültig ist oder nicht.
Traditioneller Auth JWT-Token-Header:

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

Beachten Sie hier, dass der hier verwendete Verschlüsselungsalgorithmus symmetrisch ist - HMAC + SHA256.

Moderne Authentifizierungssysteme wie Cognito verwenden jedoch asymmetric key encryptionAlgorithmen RSA, um die Nutzdaten mit einem Paar aus öffentlichem und privatem Schlüssel zu verschlüsseln. Payload wird mit einem privaten Schlüssel verschlüsselt, kann jedoch mit einem öffentlichen Schlüssel dekodiert werden. Der Hauptvorteil der Verwendung eines solchen Algorithmus besteht darin, dass wir keinen einzigen Authentifizierungsdienst anfordern müssen, um festzustellen, ob ein Token gültig ist oder nicht. Da jeder Zugriff auf den öffentlichen Schlüssel hat, kann jeder die Gültigkeit des Tokens überprüfen. Die Last für die Validierung ist fair verteilt und es gibt keinen einzelnen Fehlerpunkt.
Cognito JWT-Token-Header:

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

In diesem Fall verwendeter asymmetrischer Verschlüsselungsalgorithmus - RSA + SHA256

Gautam Jain
quelle
5

cognito-jwt-verifier ist ein winziges npm-Paket zum Überprüfen der ID und zum Zugriff auf JWT-Token, die von AWS Cognito in Ihrem Knoten- / Lambda-Backend mit minimalen Abhängigkeiten erhalten wurden.

Haftungsausschluss: Ich bin der Autor davon. Ich habe es mir ausgedacht, weil ich nichts gefunden habe, das alle Kästchen für mich überprüft hat:

  • minimale Abhängigkeiten
  • Rahmen agnostisch
  • JWKS-Caching (öffentliche Schlüssel)
  • Testabdeckung

Verwendung (siehe Github Repo für ein detaillierteres Beispiel):

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
}
Max Ivanov
quelle
2

Hier erhalten Sie Einblicke in den Lambda-Code

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

In Golang https://gist.github.com/tmaiaroto/e2ee5e88fc6ae035307d7c5ee71a99cf

Montaro
quelle
Awslabs ist eine gute Ressource, obwohl die Beispielimplementierung für Lambda ist. Sie dienen python-josezum Dekodieren und Überprüfen von JWT.
Jernej Jerin
1

das funktioniert bei mir in 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;
    }
Arvind Krmar
quelle
0

Dies basiert auf der ausführlichen Erklärung von Derek ( Antwort ). Ich konnte ein funktionierendes Beispiel für PHP erstellen.

Ich habe https://github.com/firebase/php-jwt für die PEM-Erstellung und Codeüberprüfung verwendet.

Dieser Code wird verwendet, nachdem Sie einen Satz von Base64-codierten Token erhalten haben.

<?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");
}

?>
Floris Groenendijk
quelle