ノベルティメディア

media

ヘッドレス開発にもフレンドリー、コードからフローを学べるJWT認証方式!

ヘッドレス開発にもフレンドリー、コードからフローを学べるJWT認証方式!
橋本 大地
ヘッドレス開発にもフレンドリー、コードからフローを学べるJWT認証方式!

みなさんこんにちは。ノベルティのエンジニア、橋本です!

今回はですね、「JWT認証」についての記事を書かせていただきました。

JWT認証について実装する機会がないまま今世を生きてきたのですが、この度学習する機会があったのでしっかりアウトプットしたいと思います。

本記事では、バックエンドの立場から解説していきます!

フロントエンドでJWT認証をしたことがあるけど中身はよくわからない!といった方にもおすすめです。

なるべくわかりやすく、JWT認証について理解できるように解説したいと思います!

この記事を見てわかること

  • JWT認証とは何か
  • JWT認証の基本的な処理フロー
  • JWTの取り扱いで注意すべきこと

JWT認証とは

まず、JWTとはなんでしょう。正式名称は「JSON Web Token」と言います。

私はしばらく読み方も気にせずにいましたが、世間的には「ジョット」や「ジェイダブリューティー」と呼ばれています。

こちらのJWTさんですが、何をしてくれるものかというと、主に「認証」を行なってくれます。

例えば「ログイン機能」というものは、身近にありますよね。

IDとパスワードを送って、保存されたデータと照合して合っていたらログインするというのが一般的な流れです。そして同時に、ログイン状態を維持してくれます。ECで別のページに移動しても再度ログインが求められないのは、この「状態維持」がされているためです。

とはいえ、その状態維持には何かしらの認証が必要になります。それを解決してくれる手段の一つが、「JWT認証」になります。

どんな場面でJWTを使うの?

認証と言ってもたくさんの認証方法があり、例えばサーバーサイドの「セッション」を用いる方法も一般的です。今回は一例としてセッションとJWTを比較してみましょう。

<認証方法比較>セッションの認証方法

手法としては、ログイン認証に成功すると「セッション」というデータをサーバー上で保存し、一意なIDを割り当てます。そしてこのIDをブラウザに返却し、保持しておきます。

このセッションというものは永続的ではないものの、ユーザーがページを離脱したとしても失われることはありません。

これでサーバー側とブラウザ側双方が鍵を持つことになり、通信のたびに照合することで認証ができる仕組みになっています。つまり割符のような感覚ですね。

ただし、これはあくまでも「ログインできたよ、OK!」という認証の話ですので、ユーザー情報の取得などはサーバーサイドから別途行う必要があります。データベースにユーザー名などが保存されていて、それを認証の鍵から引っ張ってくるというのが定石ですね。

<認証方法比較>JWTの認証方法

一方でJWTは、完全自己完結型の特徴を持ちます。

JWTの仕組みはログイン認証に成功した際サーバー側でJWT(トークン)を生成します。一般的にはそれをサーバーは保持しません。代わりにこのトークンと呼ばれる文字列をブラウザ側へ返却します。そしてトークンを再度サーバー側へ送信することで、認証を可能としている仕組みを持っています。

さらにJWTは、トークンそのものに任意の情報を持たせることが可能です。トークンをデコード(読み解く)することで、それらの情報を取得できます。

トークンというやや難解な表現をしていますが、これは要するに生成日時やユーザーIDなどの必要な情報の詰まった文字列です。
大切なのは、このトークンを特殊な形に変換し、秘密の鍵と混ぜ合わせる「署名」と呼ばれる行為です。この署名がJWTを安全に運用する肝になります。

次にこのトークンを利用して認証しようとするとき、サーバーサイドは同じトークンと秘密鍵を使って再度署名を生成し、それが受け取ったトークンに含まれる署名と一致するかを確認します。一致しなければ、そのJWTは正当なものとは認められません。

セッションは割符と表現しましたが、JWTはトークンという情報と署名による認証が分かれているイメージですね。なお、トークン自体は特に暗号化されていませんが、トークン全体が「署名」で保護されます。

JWTはヘッドレス開発にフレンドリーな認証方式

この仕組みを用いることで、バックエンドが複数に渡るシステムであったとしても、同じ鍵と署名の仕組みを用意しておけば、どのバックエンドでもユーザーIDなどを取得することが可能です。

なお、セッション認証を利用した場合は、同一ドメインなどによるセッションの利用制限を受けます。つまりドメインの異なるAのバックエンドとBのバックエンドがあったとして、セキュアにユーザーIDなどの情報を保持し双方のバックエンドで再利用することは困難です。

これを解消できる点がJWT認証の凄さで、ヘッドレスの開発にフレンドリーな認証だと言えます。

さらに、JWT認証はその名の通りJSONと呼ばれるデータ形式を利用し、ブラウザとサーバーサイド間でやりとりを行います。このJSONは多くのプログラミング言語でサポートされているデータ形式なので、複数にまたがるバックエンドの言語が例え異なっていたとしても容易に認証を行うことができる、優れた点も持っています。

処理の流れについて詳しく

では、JWT認証の流れについて少し詳しく見ていきましょう。

まず、上記で説明した認証の流れを正常系フローにすると下記のようになります。

処理フロー

  1. ブラウザからID/パスワードなど、認証に必要なデータを送信
  2. サーバーサイドはそれを受け取り、ユーザー認証をする
  3. 正しければトークンを含めた情報をブラウザへ返却する
  4. 返却された情報をローカルストレージまたはクッキーで保持
  5. 認証が必要な場面で、サーバーサイドへトークンを送る
  6. 認証されればトークンをデコードし、トークン情報を利用した処理を行う

こちらがフローの簡略的な形です。

しかしこのままではふわっと概念的なものになってしまいますよね。

JWTはなぜセキュリティが担保されているか、どうしてトークンから特定の情報が取得できるのか、気になると思います。

そこで簡単なサンプルコードを用意しているので、詳しく見てみましょう。

コードで見るJWT認証

※今回実装しているコードについて、本例では簡易化していますが、本番環境ではより厳密な実装が必要です。

ブラウザからID/パスワードなど、認証に必要なデータを送信

サーバーサイドはそれを受け取り、ユーザー認証をする

クライアント側の実装はidとパスワードを送るログインフォームを想定し、サーバーサイドでそれらを照合するログイン機能を実装します。

この話はログイン機能のお話になってしまうので今回は割愛します。

次の項目から、コードで見てみましょう。

正しければトークンを含めた情報をブラウザへ返却する

1.2のフローが成功し、JWT認証用のデータを返却する部分に焦点を当てます。

まず、JWTの生成は、以下の3つの要素で成り立ちます。

  • ヘッダー(Header)
  • ペイロード(Payload)
  • 署名(Signature)

実際の各要素の内容は要件によって決まりますが、ヘッダーは暗号化の種別などを指定してJWTがどのように生成されるかを決め、ペイロードはデータやトークンのルールなどを管理します。最後に署名はヘッダとペイロードをもとに秘密鍵を使ってハッシュ化されたもので、「これは安全だよ」と言う証明書の役割を果たします。

これらを踏まえソースを見ていきます。

今回はサーバーサイドをphpで実装しました。

JWTのエンコード/デコードについては、comporserプラグインの「firebase/php-jwt」を利用させていただきました!

上記例のように、各言語にJWTを管理するプラグインが存在しているので、ぜひ利用しましょう。

それでは実際のソースが下記になります。JWT生成部分の全体像です。

$key = "example_key";
$payload = array(
    'iss' => 'https://example.com',
    'aud' => 'https://example.com',
    "iat" => time(),
    "nbf" => time(),
    "exp" => time() + (60*60),
    "user_id" => $user['id']
);

$jwt = JWT::encode($payload, $key, "HS256");
echo json_encode(array('jwt' => $jwt));

では、細分化して見てみましょう。

$key = "example_key";

まずこの$keyですが、これは秘密鍵を指します。

この秘密鍵はJWT生成時の大切なセキュリティ要素になり、上記の署名を発行するために利用されます。これはサーバー側で慎重に管理します。
※基本的にこのようなハードコーディングは非推奨です。
※実際の実装では、この$keyは環境変数やセキュアな設定ファイルから読み込むなど、取り扱いに注意する必要があります。

$payload = array(
    'iss' => 'https://example.com',
    'aud' => 'https://example.com',
    "iat" => time(),
    "nbf" => time(),
    "exp" => time() + (60*60),
    "user_id" => $user['id']
);

次に、このまとまりがペイロードです。それぞれ解説していきます。

今回は上記の通り、代表される幾つかのパラメータ(クレームと呼びます)を抽出しました。

まず、issとaudはトークンを発行/受け取る人やシステムの識別子です。サービスのURLなどが使われます。

次にiatはトークンが発行された時刻です。nbfも同様に時刻を指しますが、この時刻より前に発行されたトークンは無効であることを明示します。

続いて重要なのが「exp」です。

こちらはこのトークンがいつまで有効なのか、を明示します。これでトークンの有効時間切れを起こすことができます。

最後の「user_id」がシステム固有のパラメータになります。先ほど解説したトークン自身が情報を持っている、と言う部分ですね。
これは一つである必要はなく、たとえば「user_name」や「mail_address」などを持たせることもできます。

$jwt = JWT::encode($payload, $key, "HS256");

最後にペイロードの情報と秘密鍵を使ってJWTを生成させているコードがこちらです。

第三引数の“HS256”は署名アルゴリズム…つまりどんな感じで暗号化するかを指定しています。こちらを用いてプラグイン側がヘッダーを生成しています。

ここまでの情報で、JWTはヘッダーとペイロード、そして秘密鍵を混ぜて作られていることがわかると思います。

一見ヘッダーやペイロード、秘密鍵という複数の暗号化された仕組みが絡んでセキュリティを担保しているようにも見えますが、しかしながら発行されたトークンのペイロードは簡単に複合化することができます。

重要なのは秘密鍵で、これが署名改ざんを保護します。

そのような背景があるため、秘密鍵が盗まれた瞬間に他者が正当性のあるトークンを偽装して作成し、利用することもできます。
そのため、秘密鍵の扱いは特に慎重になる必要があります。

返却された情報をローカルストレージまたはクッキーで保持

$jwtが返却されるので、それを保持しましょう。

クッキーで保持する際には、HTTPOnly/Secure属性を持つクッキーに保存できるとJavaScriptからアクセスできないため、クロスサイトスクリプティング(XSS)の対策になります。

認証が必要な場面で、サーバーサイドへトークンを送る

続いてはこちらです。

こちらの項目の理解として重要な点は、情報をヘッダーの「Authorization」として送信することです。

この内容で特に違和感がない方は、次の項目まで読み飛ばしていただいてOKです。

そうでない方は、おそらく認証周りをGETやPOSTに含めて送信してしまうと思います。もちろんそのようなケースもありますが、下記のようにすることがベターです。

fetch('https://example.com/getUserId', {
  method: 'GET',
  headers: {
    'Accept': '*/*',
    'Authorization': '{返却されたjwt}'
  }
})
//以下略

こちらはjavascriptでJWTトークンを送信している部分ですが、ヘッダーの「Authorization」を用いて送信しています。

Authorizationは認証用の値を送信するためのものであり、Basic認証などにもこちらが利用されます。GETやPOSTのパラメーターと切り離されるため、より分離することでそれぞれの実装に集中できます。

トークンをデコードし、トークン情報を利用した処理を行う

最後にトークン情報の「認証」です。

認証されればセキュアな処理が可能となり、トークンから情報(名前など)を取得して処理に利用することも可能です。

こちらは先ほど説明した3つの要素(ヘッダー、ペイロード、署名)に当てはめていくと、比較的分かりやすいかもしれません。

コードはこちらです。

$key = "example_key";
$decoded = JWT::decode($jwt, new Key($key,"HS256"));
$userId = $decoded->user_id;
http_response_code(200);
echo $userId;

まず、暗号化した際に必要な要素は、次の要素が必要でした。

  • 暗号化の種別などを指定した「ヘッダー」
  • データを指定した「ペイロード」
  • 秘密鍵

なお$jwtのトークンには、ヘッダーとペイロードが含まれています。そこに秘密鍵と認証方式を渡すことで、複合することが実現できます。

$decoded = JWT::decode($jwt, new Key($key,"HS256"));

実際の複合化コードはこちらですね。その後、「$decoded->user_id」と言う記述でuser_idを取得してクライアントサイドに返しています。

これで複合化の際にサーバーサイドが保持しているデータを取得しなくても、さまざまな情報をトークンから取得できます!

結びに

これでJWT認証についての解説は終了です!基本的な流れの理解につながれば幸いです。

JWTはここに書いた情報のほか、さまざまな機能があります。また、XSSやCSRF対策などセキュリティ上の気をつけたい部分も多々あります。

これらを学習して、よりセキュアな認証方法を身につけていきましょう!

ノベルティではヘッドレス開発にも注力しています。優れたUIとUXが実現できるシステム開発もお任せください!

また、モダンなフロントエンドの開発/制作を一緒にできる仲間を募集中です。

まずはお気軽にお問い合わせくださいませ。

それではまた!

この記事をシェアする
橋本 大地

橋本 大地

Engineer

バックエンドを経てフロントエンドの世界へ 持ち前のポジティブさと細やかさでノベルティを救う☆ 元気の源は愛妻弁当! 乾電池を通勤カバンに常備しているのできっと電池で動いています。

Webプロモーション・業務改善は
ノベルティひとつで完結

はじめての依頼にも
全力でサポートさせていただきます

メールでのお問い合わせ
各種サービス案内などをダウンロード

おすすめ記事/ PICKUP

    記事カテゴリー/ CATEGORY

      Webプロモーションや業務改善・DX化

      企業の課題はノベルティひとつで完結

      ホームページ制作などのWeb制作をはじめ、
      システム開発やマーケティング支援などワンストップで対応
      まずはお気軽にお問い合わせください

      お問い合わせ

      お電話またはメールでお気軽にお問い合わせください。

      資料ダウンロード

      各種サービスの資料をご用意しています