Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 8, 2022 03:13 am GMT

Decoding and validating AWS Cognito JWTs with PHP

Skip to the working sample repository or read on for explanation...

At Unearthed, we use the AWS service Cognito to issue JWTs to clients during authentication. From there, the JWT is exchanged with whichever services the user is interacting with in order to validate their identity. This is helpful when building out a service graph, since each JWT can describe an authenticated users session without any direct dependency on a user service.

If you are decoding Cognito JWTs from PHP, you'll need a few key pieces of information to start:

  • The region your Cognito user pool is based in (us-east-2 in our case).
  • The User Pool ID the token was issued from.
  • The Client ID used to issue the token.

To decode the tokens we'll be using web-token/jwt-framework and a few other dependencies that can be installed with:

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

The process of decoding and validating a token is:

  1. Download the public keys used to sign the token.
  2. Decode the token and validate it against the public keys.
  3. Verify the claims in the token.

Downloading the keys

Our Cognito configuration can be represented as a simple value object:

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

Using this configuration and a HTTP client we can implement a key manager to download the keys:

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

Decoding and Verifying Claims

From there, the token needs to be decoded, validated against the public keys and the claims in the token need to be validated, to ensure they are valid and originated from your specific Cognito user pool.

Using the key manager and our configuration, here is a working decoder based on Cognito's documentation about how tokens should be decoded and verified.

Key things to note are:

  • ID tokens and access tokens have slightly different means of validation (the aud and client_id claims need to be validated in each, respectively).
  • The token_use claim should be validated as either id or access respectively.
  • Other standard claims like iat, nbf, exp and iss should be validated in both. Cognito's Issuer convention is encapsulated in the CognitoConfiguration object.
<?phpnamespace 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;    }}

Pulling it all together

With all of these components in place, it's possible to pull together a proof of concept that can validate and decode Cognito JWTs:

<?phprequire_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));

Which can be invoked with:

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

Yielding a decoded token:

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

It's worth mentioning, if you are using Symfony, there is a Symfony Bundle which will make some of the factories and services used in this blog post available from the container. In our application we decided instantiating these dependencies directly was preferable.


Original Link: https://dev.to/unearthed/decoding-and-validating-aws-cognito-jwts-with-php-3825

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To