PHP로 AWS Cognito JWT 디코딩 및 검증

20803 단어
Skip to the working sample repository 또는 설명을 읽으십시오...


Unearthed에서는 AWS 서비스 Cognito를 사용하여 인증 중에 클라이언트에 JWT를 발급합니다. 거기에서 JWT는 사용자의 신원을 확인하기 위해 사용자가 상호 작용하는 모든 서비스와 교환됩니다. 이는 각 JWT가 사용자 서비스에 대한 직접적인 종속성 없이 인증된 사용자 세션을 설명할 수 있으므로 서비스 그래프를 구축할 때 유용합니다.

PHP에서 Cognito JWT를 디코딩하는 경우 시작하려면 몇 가지 주요 정보가 필요합니다.
  • Cognito 사용자 풀이 기반으로 하는 지역(이 경우 us-east-2).
  • 토큰이 발급된 사용자 풀 ID입니다.
  • 토큰을 발급하는 데 사용되는 클라이언트 ID입니다.

  • 토큰을 디코딩하기 위해 사용할 토큰web-token/jwt-framework과 함께 설치할 수 있는 몇 가지 다른 종속성:

    composer require web-token/jwt-checker web-token/jwt-signature-algorithm-rsa guzzlehttp/guzzle
    


    토큰을 디코딩하고 유효성을 검사하는 프로세스는 다음과 같습니다.
  • 토큰 서명에 사용되는 공개 키를 다운로드합니다.
  • 토큰을 디코딩하고 공개 키에 대해 유효성을 검사합니다.
  • 토큰의 클레임을 확인합니다.

  • 키 다운로드



    Cognito 구성은 간단한 값 개체로 나타낼 수 있습니다.

    <?php
    
    namespace Sam\JwtBlogPost;
    
    class CognitoConfiguration {
        public function __construct(
            public readonly string $region,
            public readonly string $poolId,
            public readonly string $clientId,
        ) {
        }
    
        public function getIssuer(): string {
            return sprintf('https://cognito-idp.%s.amazonaws.com/%s_%s', $this->region, $this->region, $this->poolId);
        }
    
        public function getPublicKeysUrl(): string {
            return sprintf('https://cognito-idp.%s.amazonaws.com/%s_%s/.well-known/jwks.json', $this->region, $this->region, $this->poolId);
        }
    }
    


    이 구성과 HTTP 클라이언트를 사용하여 키를 다운로드하는 키 관리자를 구현할 수 있습니다.

    <?php
    
    declare(strict_types=1);
    
    namespace Sam\JwtBlogPost;
    
    use GuzzleHttp\ClientInterface;
    use Jose\Component\Core\JWKSet;
    
    class CognitoKeyManager {
        public function __construct(private ClientInterface $client, private CognitoConfiguration $configuration) {
        }
    
        public function getKeySet(): JWKSet {
            return JWKSet::createFromJson($this->retrieveKeys());
        }
    
        private function retrieveKeys(): string {
            // @todo These keys can be cached.
            return (string) $this->client->request('GET', $this->configuration->getPublicKeysUrl())->getBody();
        }
    }
    


    클레임 디코딩 및 확인



    여기에서 토큰을 디코딩하고 공개 키에 대해 유효성을 검사해야 하며 토큰의 클레임이 유효성을 확인하고 특정 Cognito 사용자 풀에서 생성되었는지 확인해야 합니다.

    키 관리자와 구성을 사용하여 Cognito's documentation about how tokens should be decoded and verified 기반으로 작동하는 디코더가 있습니다.

    주목해야 할 주요 사항은 다음과 같습니다.
  • ID 토큰과 액세스 토큰은 확인 방법이 약간 다릅니다(각각 audclient_id 클레임을 확인해야 함).
  • token_use 클레임은 각각 id 또는 access로 확인되어야 합니다.
  • iat , nbf , expiss 와 같은 다른 표준 클레임은 둘 다에서 확인되어야 합니다. Cognito의 발급자 규칙은 CognitoConfiguration 개체에 캡슐화됩니다.

  • <?php
    
    namespace Sam\JwtBlogPost;
    
    use Jose\Component\Checker\AlgorithmChecker;
    use Jose\Component\Checker\AudienceChecker;
    use Jose\Component\Checker\ClaimCheckerManager;
    use Jose\Component\Checker\ExpirationTimeChecker;
    use Jose\Component\Checker\HeaderCheckerManager;
    use Jose\Component\Checker\IssuedAtChecker;
    use Jose\Component\Checker\IssuerChecker;
    use Jose\Component\Checker\NotBeforeChecker;
    use Jose\Component\Core\AlgorithmManager;
    use Jose\Component\Signature\Algorithm\RS256;
    use Jose\Component\Signature\JWS;
    use Jose\Component\Signature\JWSLoader;
    use Jose\Component\Signature\JWSTokenSupport;
    use Jose\Component\Signature\JWSVerifier;
    use Jose\Component\Signature\Serializer\CompactSerializer;
    use Jose\Component\Signature\Serializer\JWSSerializerManager;
    use Sam\JwtBlogPost\Checkers\ClientIdChecker;
    use Sam\JwtBlogPost\Checkers\TokenUseChecker;
    
    /**
     * Load and verify Cognito tokens.
     *
     * Rules for verifying tokens are:
     *  - Verify that the token is not expired.
     *  - The aud claim in an ID token and the client_id claim in an access token should match the app client ID that was created in the Amazon Cognito user pool.
     *  - The issuer (iss) claim should match your user pool. For example, a user pool created in the us-east-1 Region will have the following iss value: https://cognito-idp.us-east-1.amazonaws.com/<userpoolID>.
     *  - Check the token_use claim.
     *    - If you are only accepting the access token in your web API operations, its value must be access.
     *    - If you are only using the ID token, its value must be id.
     *    - If you are using both ID and access tokens, the token_use claim must be either id or access.
     *
     * @see https://web-token.spomky-labs.com/advanced-topics-1/security-recommendations#loading-process
     * @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
     */
    class CognitoJwtDecoder {
        public function __construct(private CognitoKeyManager $keyManager, private CognitoConfiguration $configuration) {
        }
    
        public function decodeIdToken(string $token): JWS {
            return $this->decodeAndValidate($token, [
                new AudienceChecker($this->configuration->clientId),
                new TokenUseChecker('id'),
            ], ['iss', 'aud', 'token_use']);
        }
    
        public function decodeAccessToken(string $token): JWS {
            return $this->decodeAndValidate($token, [
                new ClientIdChecker($this->configuration->clientId),
                new TokenUseChecker('access'),
            ], ['iss', 'client_id', 'token_use']);
        }
    
        /**
         * @throws \Jose\Component\Checker\InvalidClaimException
         * @throws \Jose\Component\Checker\MissingMandatoryClaimException
         * @throws \Exception
         */
        private function decodeAndValidate(string $token, array $claimChecks, array $mandatoryClaims): JWS {
            $headerChecker = new HeaderCheckerManager([new AlgorithmChecker(['RS256'])], [new JWSTokenSupport()]);
            $claimChecker = new ClaimCheckerManager(
                array_merge([
                    new IssuedAtChecker(),
                    new NotBeforeChecker(),
                    new ExpirationTimeChecker(),
                    new IssuerChecker([$this->configuration->getIssuer()]),
                ], $claimChecks)
            );
    
            $loader = new JWSLoader(new JWSSerializerManager([new CompactSerializer()]), new JWSVerifier(new AlgorithmManager([new RS256()])), $headerChecker);
            $jws = $loader->loadAndVerifyWithKeySet($token, $this->keyManager->getKeySet($token), $signature);
    
            $claims = json_decode($jws->getPayload(), true);
            $claimChecker->check($claims, $mandatoryClaims);
    
            return $jws;
        }
    
    }
    


    모두 함께 당기기



    이러한 구성 요소를 모두 갖추고 있으면 Cognito JWT를 검증하고 디코딩할 수 있는 개념 증명을 함께 가져올 수 있습니다.

    <?php
    
    require_once 'vendor/autoload.php';
    
    [, $region, $poolId, $clientId, $type, $token] = $argv;
    
    $config = new \Sam\JwtBlogPost\CognitoConfiguration($region, $poolId, $clientId);
    $keyManager = new \Sam\JwtBlogPost\CognitoKeyManager(
        new \GuzzleHttp\Client(),
        $config,
    );
    $decoder = new \Sam\JwtBlogPost\CognitoJwtDecoder($keyManager, $config);
    
    var_export($type === 'access' ? $decoder->decodeAccessToken($token) : $decoder->decodeIdToken($token));
    


    다음과 같이 호출할 수 있습니다.

    php run.php us-east-2 POOL_ID CLIENT_ID TOKEN_TYPE TOKEN
    


    디코딩된 토큰 생성:

    <?php
    Jose\Component\Signature\JWS::__set_state(array(
    'payload' => '{"sub":"420cd5cc-f537-4eab-a338-dc72f0b048e0","aud"...
    


    Symfony를 사용하는 경우 이 블로그 게시물에 사용된 공장 및 서비스 중 일부를 컨테이너에서 사용할 수 있도록 하는 Symfony Bundle이 있다는 점을 언급할 가치가 있습니다. 우리 애플리케이션에서는 이러한 종속성을 직접 인스턴스화하는 것이 바람직하다고 결정했습니다.

    좋은 웹페이지 즐겨찾기