API 백엔드에서 AWS Cognito의 JWT를 확인하는 방법은 무엇입니까?


80

Angular2 단일 페이지 앱과 ECS에서 실행되는 REST API로 구성된 시스템을 구축하고 있습니다. API는 .Net / Nancy 에서 실행 되지만 변경 될 수 있습니다.

Cognito를 사용해보고 싶습니다. 이것이 인증 워크 플로를 상상 한 방법입니다.

  1. SPA가 사용자를 로그인하고 JWT를받습니다.
  2. SPA는 모든 요청과 함께 JWT를 REST API로 보냅니다.
  3. REST API는 JWT가 인증되었는지 확인합니다.

내 질문은 3 단계에 관한 것입니다. 내 서버 (또는 오히려 상태 비 저장, 자동 확장,로드 밸런싱 된 Docker 컨테이너)가 토큰이 인증되었는지 확인할 수 있습니까? "서버"는 JWT 자체를 발행하지 않았기 때문에 자체 보안 비밀을 사용할 수 없습니다 ( 여기 의 기본 JWT 예제에 설명 됨 ).

Cognito 문서를 읽고 많이 봤지만 서버 측에서 JWT로 무엇을해야하는지에 대한 좋은 지침을 찾을 수 없습니다.


2
Node / Express 앱을 사용하는 경우 원하는 작업 을 거의 수행하는 cognito -express 라는 npm 패키지를 만들었습니다. Cognito 사용자 풀에서 JWK를 다운로드하고 ID의 JWT 서명을 확인합니다. 토큰 또는 액세스 토큰.
ghdna

@ghdna 최근에 cognito-express를 다운로드하여 내 서버에 설치했지만 클라이언트 측의 Cognito에서는 accessKey, secretKey, sessionKey 및 만료 만 얻습니다. 어디서든 반환되는 ID 토큰 또는 액세스 토큰을 찾을 수 없습니다. 어딘가에 새로 고침 토큰도 있습니다. 그래서 현재 cogito-express에서 콘솔에 들어오는 것은 헤더에서 Access Token이 없거나 유효한 JWT가 아닙니다. 포인터가 있습니까?
elarcoiris

aws 빠른 시작 프로젝트에 따라 JWT가 디코딩 (base64 변환)되어 "kid"를 가져온 다음 URL에서 JWK를 가져오고 PEM으로 변환 한 다음 유효성을 검사하므로 JWT 유효성 검사를위한 명확한 코드 샘플을 제공 할 수 있었기를 바랍니다. PEM 변환에 갇혀 있습니다.
Abdeali Chandanwala

답변:


44

내가 문서를 제대로 읽지 않은 것으로 밝혀졌습니다. 여기에 설명되어 있습니다 ( "웹 API에서 ID 토큰 및 액세스 토큰 사용"으로 스크롤).

API 서비스는 Cognito의 암호를 다운로드하고이를 사용하여 수신 된 JWT를 확인할 수 있습니다. 완전한.

편집하다

Groady의 코멘트 @ 포인트에 :하지만 어떻게 당신은 토큰의 유효성을 검사합니까? 이를 위해 jose4j 또는 nimbus (둘 다 Java) 와 같은 전투 테스트를 거친 라이브러리를 사용하고 처음부터 확인을 구현하지 마십시오.

다음 은 최근에 java / dropwizard 서비스에서 구현해야 할 때 시작된 nimbus를 사용하는 Spring Boot의 구현 예입니다.


64
문서는 기껏해야 쓰레기입니다. 6 단계는 "디코딩 된 JWT 토큰의 서명을 확인하십시오" 라고 말합니다. ... 예 ... 어떻게!?!? 이 블로그 게시물 에 따르면 JWK를 PEM으로 변환해야합니다. 공식 문서에이 작업을 수행하는 방법을 넣을 수 없습니까?!

Groady에 대한 후속 조치입니다. 라이브러리에 따라 pem으로 변환 할 필요가 없습니다. 예를 들어, 저는 Elixir를 사용 중이고 Joken은 Amazon에서 제공 한 그대로 RSA 키 맵을 사용합니다. 열쇠가 끈이어야한다고 생각할 때 바퀴를 돌리는 데 많은 시간을 보냈습니다.
Law

예제 링크 주셔서 감사합니다! nimbus 라이브러리 사용 방법을 이해하는 데 많은 도움이되었습니다. 그러나 원격 JWK 세트를 외부 캐시로 추출 할 수 있다면 어떤 아이디어가 있습니까? 대신 Elasticache에 JWKSet을 넣고 싶습니다.
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를
redgeoff

1
재사용을 위해 JWK의 내용을 로컬 구성에 저장해야합니까? 이 콘텐츠가 만료되거나 향후 무효화됩니까?
NGHIA

@Nghia "Lambda 함수에서 직접 JWK 세트를 다운로드하는 대신 수동으로 한 번 다운로드하여 키를 PEM으로 변환 한 다음 Lambda 함수를 사용하여 업로드 할 수 있습니다." 에서 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. ID 토큰 디코딩
  2. 로컬 키 ID (아이)와 공개 키드 비교
  3. 공개 키를 사용하여 JWT 라이브러리를 사용하여 서명을 확인합니다.

AWS Cognito는 각 사용자 풀에 대해 두 쌍의 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, (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

위의 질문에 답하기 : 헤더에 비밀을 제공하는 경우 본문에는 필요하지 않습니다.
Zach Saucier

new Buffer(headerEncoded, 'base64')지금해야Buffer.from(headerEncoded, 'base64')
자크 Saucier에게

9

비슷한 문제가 있었지만 API 게이트웨이를 사용하지 않았습니다. 제 경우에는 AWS Cognito Developer Authenticated identity route를 통해 얻은 JWT 토큰의 서명을 확인하고 싶었습니다.

여러 사이트의 많은 포스터와 마찬가지로 외부에서 즉, 서버 측 또는 스크립트를 통해 AWS JWT 토큰의 서명을 확인하는 데 필요한 비트를 정확히 맞추는 데 문제가있었습니다.

AWS JWT 토큰 서명확인 하고 요점을 파악한 것 같습니다 . PyCrypto의 Crypto.Signature에서 pyjwt 또는 PKCS1_v1_5c를 사용하여 AWS JWT / JWS 토큰을 확인합니다.

네, 제 경우에는 파이썬 이었지만 노드에서도 쉽게 수행 할 수 있습니다 (npm install jsonwebtoken jwk-to-pem 요청).

나는 이것을 알아 내려고 할 때 나는 대부분 옳은 일을하고 있었지만 파이썬 딕셔너리 순서와 같은 뉘앙스가 있거나, 거기에 json 표현이 부족했기 때문에 주석에서 몇 가지 문제를 강조하려고했습니다.

바라건대 누군가 어딘가에 도움이 될 수 있습니다.


9

짧은 대답 :
다음 끝점에서 사용자 풀에 대한 공개 키를 얻을 수 있습니다.이 공개 키를
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
사용하여 토큰을 성공적으로 디코딩하면 토큰이 유효하지 않으면 위조됩니다.


긴 답변 :
코크 릿을 통해 성공적으로 인증하면 액세스 및 ID 토큰을받습니다. 이제이 토큰이 변조되었는지 여부를 확인하려고합니다. 전통적으로 우리는 토큰이 유효한지 확인하기 위해 이러한 토큰을 인증 서비스 (처음에이 토큰을 발행 한)로 다시 보냅니다. 이러한 시스템 은 비밀 키를 사용하여 페이로드를 암호화하는 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

cognito-jwt-verifier 는 최소한의 종속성으로 노드 / Lambda 백엔드의 AWS Cognito에서 얻은 JWT 토큰에 액세스하고 ID를 확인하는 작은 npm 패키지입니다.

면책 조항 : 나는 이것의 저자입니다. 나는 나를 위해 모든 상자를 체크하는 것을 찾을 수 없기 때문에 그것을 생각해 냈습니다.

  • 최소한의 의존성
  • 프레임 워크 불가지론
  • 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
}

2

Awslabs는 예제 구현이 Lambda 용이지만 좋은 리소스입니다. python-joseJWT를 디코딩하고 확인 하는 데 사용 합니다.
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

누군가는 또한 Amazon Cognito JWT를 디코딩하고 확인하기 위해 비동기 / 동기 모드에서 작동하는 cognitojwt 라는 Python 패키지를 작성했습니다 .


0

이것은 Derek 의 정교한 설명을 기반으로합니다 ( 답변 ). PHP 용 작업 샘플을 만들 수있었습니다.

pem 생성 및 코드 확인을 위해 https://github.com/firebase/php-jwt 를 사용했습니다 .

이 코드는 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");
}

?>
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.