Як перевірити JWT з AWS Cognito у серверній частині API?


80

Я будую систему, що складається з односторінкової програми Angular2 та REST API, що працює на ECS. API працює на .Net / Nancy , але це цілком може змінитися.

Я хотів би спробувати Cognito, і ось як я собі уявив робочий процес аутентифікації:

  1. SPA підписує користувача та отримує JWT
  2. SPA надсилає JWT API REST з кожним запитом
  3. REST API підтверджує автентичність JWT

Моє запитання стосується кроку 3. Як мій сервер (а точніше: мої бездержавні, автоматично масштабовані, збалансовані навантаження контейнери Docker) може перевірити, що маркер справжній? Оскільки "сервер" не видав JWT сам, він не може використовувати власний секрет (як описано в базовому прикладі JWT тут ).

Я багато читав документи Cognito і багато гуглив, але я не можу знайти жодної хорошої настанови щодо того, що робити з JWT на стороні сервера.


2
Якщо ви використовуєте додаток Node / Express, я створив пакет npm з назвоюgnito-express, який в значній мірі робить те, що ви хочете зробити - завантажує JWK з вашого пулу користувачів Cognito і перевіряє підпис JWT ідентифікатора Токен або маркер доступу.
ghdna

@ghdna Я нещодавно завантажив когніто-експрес і встановив його на своєму сервері, але з Cognito на моїй стороні клієнта я отримую лише accessKey, secretKey, sessionKey та закінчення терміну дії. Я не можу знайти маркер посвідчення особи або маркер доступу, який повертається звідки завгодно. Там десь теж є Refresh Token. Отже, все, що я зараз отримую в консолі від cogito-express, - це маркер доступу, відсутній у заголовку, або не дійсний JWT. Будь-які вказівники?
elarcoiris

Сподіваюся, ви могли б дати чіткий зразок коду для перевірки JWT, відповідно до проекту швидкого запуску aws, JWT декодується (перетворення base64), щоб отримати "дитину", потім отримати JWK з URL-адреси, перетворити на PEM, а потім перевірити. Я застряг у перетворенні PEM.
Абдеалі Чанданвала

Відповіді:


44

Виявляється, я не правильно прочитав документи. Це пояснюється тут (прокрутіть униз до розділу "Використання маркерів ідентифікаторів та маркерів доступу у ваших веб-API").

Служба API може завантажувати секрети Cognito і використовувати їх для перевірки отриманих JWT. Ідеально

Редагувати

Коментар @ Groady суттєвий: але як ви перевіряєте маркери? Я б сказав , що використовувати бойові випробування бібліотеки як jose4j або віночком (як Java) для цього і не здійснювати перевірку з нуля самостійно.

Ось приклад реалізації для Spring Boot за допомогою nimbus, який мене почав, коли мені нещодавно довелося реалізувати це в службі java / dropwizard.


64
Документація в найкращому випадку є лайною Крок 6 говорить "Перевірте підпис декодованого маркера JWT" ... так ... ЯК!?!? Відповідно до цього повідомлення в блозі вам потрібно перетворити JWK на PEM. Чи не могли вони розмістити, як це зробити, на офіційних документах ?!
Буде

Продовження Гроді, коли я переживаю це. Залежно від вашої бібліотеки, вам не потрібно конвертувати в pem. Наприклад, я на Elixir, і Джокен бере карту ключів RSA точно так, як це надає Amazon. Я витратив багато часу, крутячись на колесах, коли думав, що ключем повинна бути струна.
Закон

Дякую за приклад посилання! Дуже допомогло зрозуміти, як користуватися бібліотекою nimbus. Будь-які ідеї, однак, чи можу я витягти віддалений набір JWK як зовнішній кеш? Натомість я хотів би помістити JWKSet в Elasticache.
Ерік Б.

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 "Замість того, щоб завантажувати набір JWK безпосередньо з вашої функції Лямбда, ви можете завантажити його один раз вручну, перетворивши ключі на PEM і завантаживши їх за допомогою своєї функції Лямбда." з aws.amazon.com/blogs/mobile/…
R.Cha

23

Виконайте Потік надання коду авторизації

Припускаючи, що ви:

  • правильно налаштували пул користувачів в 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і використовувати 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. Розшифровка маркера ідентифікатора
  2. Порівняння локального ідентифікатора ключа (kid) із загальнодоступним kid
  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>?
Зак Сосьє,

Відповідаючи на моє запитання вище: це є, але це не обов’язково в тілі, якщо ви надаєте секрет у заголовку.
Зак Сосьє,

new Buffer(headerEncoded, 'base64')тепер має бутиBuffer.from(headerEncoded, 'base64')
Зак Сосьє

9

У мене була подібна проблема, але без використання шлюзу API. У моєму випадку я хотів перевірити підпис маркера JWT, отриманого за допомогою аутентифікованого маршруту ідентифікації розробника AWS Cognito.

Як і у багатьох плакатів на різних сайтах, у мене були проблеми зі складанням саме тих бітів, які мені потрібні для перевірки підпису маркера AWS JWT зовні, тобто на стороні сервера або за допомогою сценарію

Здається, я розібрався і склав суть для перевірки підпису маркера AWS JWT . Він перевірить маркер AWS JWT / JWS за допомогою pyjwt або PKCS1_v1_5c від Crypto. Підпис у PyCrypto

Так, так, це був python у моєму випадку, але це також легко зробити у вузлі (npm встановити jsonwebtoken jwk-to-pem запит).

Я спробував виділити деякі помилки в коментарях, тому що коли я намагався це зрозуміти, я в основному робив правильно, але були деякі нюанси, такі як впорядкування піктонів на python, або відсутність у них, і представлення json.

Сподіваємось, це може десь комусь допомогти.


9

Коротка відповідь:
Ви можете отримати відкритий ключ для вашого пулу користувачів із наступної кінцевої точки:
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
Якщо ви успішно декодуєте маркер за допомогою цього відкритого ключа, тоді маркер є дійсним, оскільки він підроблений.


Довга відповідь:
Після успішної автентифікації через когніто ви отримуєте маркери доступу та ідентифікатора. Тепер ви хочете перевірити, чи було підроблено цей маркер чи ні. Традиційно ми відправляли ці маркери назад до служби автентифікації (яка видала цей маркер спочатку), щоб перевірити, чи правильний маркер. Ці системи використовують symmetric key encryptionтакі алгоритми, як HMACшифрування корисного навантаження за допомогою секретного ключа, тому лише ця система здатна визначити, чи правильний цей маркер чи ні.
Традиційний заголовок токена JWT:

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

Тут зауважте, що алгоритм шифрування, що використовується тут, симетричний - HMAC + SHA256.

Але сучасні системи автентифікації, такі як Cognito, використовують asymmetric key encryptionтакі алгоритми, як RSAшифрування корисного навантаження за допомогою пари відкритого та приватного ключа. Корисне навантаження шифрується за допомогою приватного ключа, але може бути декодовано за допомогою відкритого ключа. Основною перевагою використання такого алгоритму є те, що нам не потрібно запитувати одну службу автентифікації, щоб визначити, чи правильний маркер. Оскільки кожен має доступ до відкритого ключа, кожен може перевірити дійсність маркера. Навантаження для перевірки справедливо розподілено, і немає жодної точки відмови.
Заголовок маркера Cognito JWT:

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

В даному випадку використовується асиметричний алгоритм шифрування - RSA + SHA256


6

gnito-jwt-verifier - це крихітний пакет npm для перевірки ідентифікатора та доступу до маркерів JWT, отриманих з AWS Cognito у вашому вузлі / лямбда-сервері, з мінімальними залежностями.

Застереження: Я автор цього. Я придумав це, тому що не міг знайти нічого, перевіряючи всі поля для мене:

  • мінімальні залежності
  • агностичний фреймворк
  • Кешування JWKS (відкриті ключі)
  • покриття тесту

Використання (див. Репозиторій 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
}

2

Awslabs - хороший ресурс, хоча приклад реалізації - для Lambda. Вони використовують python-joseдля декодування та перевірки JWT.
Jernej Jerin

1

це працює для мене в точковій мережі 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;
    }

1

Хтось також написав пакет пітонів під назвоюOGOGITOJWT, який працює в режимі асинхронізації / синхронізації для декодування та перевірки Amazon Cognito JWT.


0

Це базується на детальному поясненні Дерека ( відповідь ). Я зміг створити робочий зразок для 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");
}

?>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.