【第14回】Go言語でJWT認証を実装してみよう

前回学習したGo言語を使ってJWT(JSON Web Token)認証を実装してみます。

Homeブログ一覧【第14回】Go言語でJWT認証を実装してみよう

認証について

認証(Authentication)とは、何らかのシステムにアクセスする際に、そのユーザーが自分自身を証明するためのプロセスのことであり、簡単に言えばユーザーが誰であるかを確認することです。

Webに限った話ではなく、広義な意味での認証は大きく下の3つに分類されます。

  • 知識情報 (例: パスワード認証)
  • 所有情報 (例: SMS認証)
  • 生体情報 (例: 指紋認証, 顔認証)

このように、何らかの情報を用いてユーザーを特定することを「認証」と呼びます。ユーザーを特定することを目的としており、それ以外の目的は認証に含まれません。

認可について

認証と似た単語に「認可(Authorization)」というものがあります。

認可とは、ユーザーが特定のリソースにアクセスする権限を持っているかどうかを確認することです。例えば、映画館のチケットを持っていると「対象の上映作品、特定の座席」というリソース(対象物)に対して、利用することができるという権限(認可)を与えられたということになります。

認証方法まとめ

Basic認証

Basic(ベーシック)認証とは、IDとパスワードをBase64エンコードしてリクエストヘッダーに含めることで認証を行います。実装が容易で、ほぼ全てのWebサーバおよびブラウザで対応している事もあり、簡易的な認証として広く使われています。

しかし、IDとパスワードが平文で送信されるため、盗聴されると簡単に解析されてしまいます。

サーバークライアントサーバークライアントalt[認証成功][認証失敗]保護リソース要求401 Unauthorized (WWW-Authenticate: Basic)再リクエスト (Authorizationヘッダー: Base64エンコードされたIDとパスワード)IDとパスワードを検証保護リソースを返す401 Unauthorized

Digest認証

Digest(ダイジェスト)認証とは、先ほどのBasic認証の改良版のようなもので、サーバーが生成したランダムな文字列をパスワードに付与し、ハッシュ化して送信します。

そのため、盗聴されてもパスワードの解析が困難になります。

サーバークライアントサーバークライアントalt[認証成功][認証失敗]保護リソース要求401 Unauthorized (WWW-Authenticate: Digest, nonce="サーバーが生成した値")再リクエスト (Authorizationヘッダー: ハッシュ化された認証情報)受信したハッシュを検証保護リソースを返す401 Unauthorized

セッションベース認証

通信の度にIDとパスワードを送信するのはセキュリティ上問題があり、認証のオーバーヘッドも大きくなってしまいます。

セッションベース認証は、認証後にサーバー側でセッションIDとユーザー情報を対応づけておき、セッションIDを発行します。クライアントは受け取ったセッションIDをCookieなどで保持します。

クライアントがリソースにアクセスする際には保持しているセッションIDを含めてサーバーにリクエストを送信します。サーバーは受け取ったセッションIDを検証し、セッションIDに対応するユーザー情報を取得(ステートフルな通信)します。最後にサーバーがアクセス可能なリソースを返却します。

セッションベース認証はサーバー側でログイン状態を管理できるので、例えばある端末でパスワードが変更されたときにログインしている全ての端末をログアウトさせたり、同時多重ログインを検知して防止したりすることができます。

トークンベース認証

トークンベース認証は、ユーザー情報を含む(パスワードは含まない)アクセストークンをクライアントに発行します。セッションベース認証と違って、サーバー側が状態を持つ必要がない(ステートレスな通信)ためサーバーへの負荷が軽減されます。

後ほど詳しく説明します。

OAuth/OAuth2

OAuthとは、GoogleやFacebook, X(Twitter)などの外部サービスの認証を利用する(認可)ためのプロトコルであり、ユーザー名やパスワードの共有をせずに別のサービスにアクセス許可を与えることができます。

例えばMaximum IdPでは、ユーザー名・パスワードを打ち込むことなくGitHubアカウントを使ってログインすることができます。

IdPでは現在はユーザーデータの読み取り権限しか要求していませんが、権限要求の仕方によってはIdPからGitHubに対して新規リポジトリ作成などの要求をするなど、連携先に対してのさまざまな命令をすることができます。

JWT(JSON Web Token)とは

JWTはトークンベースの認証であり、リクエストを送信する際にヘッダーにトークンを含めることで認証を行います。

例えば、ユーザーがあるサービスにログインをした時にそのログインしたユーザーしかアクセスできないページを作りたいとしましょう。

ログインしたユーザーのみがアクセスできる、/mypageというページにアクセスするためには、トークンをリクエスト毎に送信し、そのトークンが有効かどうかを検証することでアクセスを許可するかどうかを判断します。

JWTの仕組み

JWTの仕組みについて詳しく説明をします。JWTはその名の通りJSON形式でデータを扱います。JWTの使う利点には次のようなメリットがあります。

  • ブラウザ側でトークンを保持するため実装が容易
  • トークンの有効期限を設定できる
  • トークンの署名を用いて改ざんを防ぐことができる
  • シングルサインオン(SSO)に対応できる

トークンとは

トークンは暗号化された文字列であり、許可証のような役割を持ちます。JWTは、ヘッダー、ペイロード、シグネチャの3つの部分から構成されています。JWTを検証できるサイトがあるので試してみましょう。

tokenの構造

ヘッダー部分には使用する署名方式が記述され、HS256がそれにあたります。ペイロード部分には、ユーザー情報やトークンの有効期限などの情報が記述されます。これらはそれぞれbase64UrlEncodeという形で変換されています。シグネチャ(署名)部分には、サーバー側に保持されてる秘密鍵を使って署名したものが記述されています。

tokenの変換

トークンの生成と検証

トークンはユーザー新規登録時やログイン時に生成されます。生成されたトークンには、ユーザー情報やトークンの有効期限などの情報が含まれ、ブラウザのローカルストレージやcookieに保存されます。

データベースサーバークライアントデータベースサーバークライアントalt[認証成功][認証失敗]ログインリクエスト (ID, パスワードなど)ユーザー情報を確認結果を返却 (成功/失敗)JWTを生成JWTを返す認証エラー

保護されたリソース(先ほどの/mypageや管理者画面など)にアクセスする際は保持しているトークンをリクエストヘッダーに含めて送信します。サーバー側では、受け取ったトークンを検証し、有効であればリソースにアクセスを許可します。

データベースサーバークライアントデータベースサーバークライアントalt[JWT有効][JWT無効/期限切れ]保護リソース要求 (JWTを送信)JWTを検証必要なデータを取得データを返すデータを返す401 Unauthorized

JWTの実装方法

今回は、Sor4chiさんが作成したGo言語でJWT認証を実装するためのテンプレートを基にJWT認証を実装方法を説明します。

ライブラリの説明

主にJWTを実装する際に使用するライブラリは以下の通りです。

  • github.com/dgrijalva/jwt-go : JWTを扱うためのライブラリ
  • golang.org/x/crypto/bcrypt : パスワードのハッシュ化を行うためのライブラリ

トークンの作成と検証

loginハンドラーをもとに説明します。クライアントから送られてきたリクエストボディからユーザーとパスワードを取得します。データベースに接続しユーザー情報からパスワードを取得します。

ユーザー作成時にパスワードはハッシュ化して格納されているため、リクエストから送られてきた平文のパスワードをハッシュ化し、データベースに格納されているハッシュ化されたパスワードと照合します。

// リクエストボディの読み込み
var user User
if err := decodeBody(r, &user); err != nil {
    respondJSON(w, http.StatusBadRequest, map[string]string{"message": "不正なリクエストです"})
    return
}

// ユーザーの取得
row := db.QueryRow(selectUserByEmail, user.Email)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Email, &u.Password, &u.CreatedAt)
if err != nil {
    respondJSON(w, http.StatusBadRequest, map[string]string{"message": "メールアドレスまたはパスワードが間違っています"})
    return
}

// パスワードの照合
if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(user.Password)); err != nil {
    respondJSON(w, http.StatusBadRequest, map[string]string{"message": "メールアドレスまたはパスワードが間違っています"})
    return
}

ユーザー情報が正しい場合、JWTを生成します。JWTのペイロード部分にユーザーIDとトークンの有効期限を設定します。

// JWTの作成
claims := jwt.MapClaims{
    "user_id": u.ID,
    "exp":     time.Now().Add(time.Hour * 72).Unix(), // 72時間が有効期限
}

JWTを署名するために、サーバー側で設定している秘密鍵を用いて署名を行います。

// 署名を設定する
tokenString, err := token.SignedString(secret)

クライアントにJWTを返却します。

// トークンをレスポンスする
respondJSON(w, http.StatusOK, map[string]interface{}{
    "token": tokenString,
    "user":  u,
})

F12キーを押して検証ツールを開き、Applicationタブを選択し、Local Storageを選択すると、トークンが保存されていることが確認できます。

トークンの確認

このトークンを先ほどのサイトでデコードして中身を確認してみましょう。

トークンのデコード

user_idと有効期限expが得られ、トークンが正常に生成されていることが確認できます。

ログアウト処理

ログアウト処理は、クライアント側で保持しているトークンを削除するだけで行うことができます。

// トークンを削除
localStorage.removeItem("token");

まとめ

これにて今年度のWeb研究会の活動は終了となります。最後に、本サークルのGitHubリポジトリにSor4chiさんが作成したGoとReactを使ったサンプルWebアプリを紹介したいと思います。これまでの学習内容を活かしてサンプルアプリを自由にアレンジしてみてください。

最もシンプルな例です、あくまでも勉強用のもので公開・本番には向かないと思います。Dockerなしです。

↑のテンプレートに認証を足したもの。上と下のリポジトリのコミット履歴を比較することでどうやって認証を実装したかをみることができます。

実用に耐えうるガチ構成ガン積みテンプレートです。本格的にアプリ作って公開したいならこれ。Dockerの勉強にもなると思います。