JWTとJWKSを用いてCognito認証を行う

この記事の目次
chevron_right JWTとは?
chevron_right 参考
chevron_right JWKとは?
chevron_right 参考
chevron_right 実践
chevron_right コード
chevron_right バックエンド
chevron_right コード

JWTとは?

いろんな情報を3つの要素に分けて、BASE64エンコードして、ドット(.)でつないだ文字列。

参考

JWKとは?

JWTを作るときに暗号化をした場合、その暗号化に用いられたアルゴリズムや公開鍵情報が書かれたJSONファイル

参考

JWKとJWTを使った認証の流れ

AWSのCognitoを使った場合の流れです。
CognitoはJWTを「idToken」という名前で表しています。

  1. JavaScriptを用いてCognitoの認証ページを叩く
  2. Cognitoが認証を行う
  3. idTokenをCognitoが発行し、リダイレクト先のページにリダイレクト。このときidTokenがブラウザのlocal storageに書き込まれる
  4. idTokenをPHPに送信
  5. PHPがidTokenを受け取り、JWKを使ってidTokenが正しいかをチェック

という流れになります。

実践

フロントエンド

あんまりフロントは得意じゃないので、間違っていたらすいません。
(一応動作は確認済み)

必要モジュールのインストール

$ npm install aws-amplify

コード

import { Auth } from 'aws-amplify'

// Congnitoにアクセスするための情報
const conf = {
    userPoolId: 'AWSが発行するuserPoolId',
    userPoolWebClientId: 'AWSが発行するuserPoolWebClientId',
    region: 'AWSのリージョン',
    oauth: {
        domain: 'AWS Cognitoのドメイン',
        scope: ['openid'],
        redirectSignIn: 'CognitoにアクセスするページのURL(http://localhost:3000など)',
        redirectSignOut: 'サインアウト時にリダイレクトされるページのURL',
        responseType: 'code',
    }
};

// 認証を行う場合
const checkFlg = window.confirm('認証しますか?');
if (checkFlg) {
    // 上で定義したコンフィグを設定
    Auth.configure(conf);
    // SAML認証を使ってSingIN(これが実行されると、Cognitoが発行する認証ページにリダイレクトされる)
    Auth.federatedSignIn();
}

// Cognito認証後にリダイレクトされた場合(window.confirm('認証しますか?');でNOを選択した場合)、
// これを再度実行することにより、localstorageにidToken情報が書き込まれる
Auth.configure(conf);

バックエンド

必要ライブラリのインストール

$ composer require firebase/php-jwt
$ composer require phpseclib/phpseclib

コード

$jwks = json_decode(file_get_contents('JWKSのURL'), true);

// JSON Web Tokenをデコードしてヘッダーのkidを見つけます
$tks = explode('.', self::ID_TOKEN);
if (count($tks) !== 3) {
    throw new Exception('JWTのフォーマットがおかしいです');
}
$jwtHeader = $tks[0];

$jwt_header = json_decode(JWT::urlsafeB64Decode($jwtHeader), true);
if (empty($jwt_header["kid"])) {
    throw new Exception("JSON Web Tokenにkidがありません");
}

// JSON Web Keysをデコードしてjwtのkidと合致するjwkから公開鍵を取得します
$publicKey = "";
foreach ($jwks["keys"] as $jwk) {
    if ($jwk["kid"] == $jwt_header["kid"]) {
        // 公開鍵取得
        $publicKey = $this->createPubKey($jwk);
        break;
    }
}
if ( !$publicKey ) {
    throw new Exception("公開鍵が取得出来ません");
}

// cognitoが発行したidTokenが正しいものかを公開鍵を使って検証する
$key = new Key($publicKey, 'RS256');
$decoded = JWT::decode(self::ID_TOKEN, $key);

// ここでエクセプションが出なければ使っていいidToken

// ログイン情報などはpayloadの中にいるので、そこから取り出す
$payload = json_decode(JWT::urlsafeB64Decode($tks[1]), true);
var_dump($payload);
die;


/**
  * 公開鍵を作成する関数
	*/
private function createPubKey(array $jwk): string
{
    return PublicKeyLoader::load([
        'e' => new BigInteger(base64_decode($jwk['e']), 256),
        'n' => new BigInteger(base64_decode(strtr($jwk['n'], '-_', '+/'), true), 256)
    ])->toString('PKCS8');
}
この記事を書いた人
Nな人(えぬなひと)。
Nは本名から取っています。
Laravelが大好きなPHPerで、WEBを作るときはLaravelを技術スタックに絶対推すマン。
PHP、Pythonと、昔はperlを書いていたP言語エンジニア。
最近はNimを書いたりしています。