ユーザー認証を実装することになったとき、どう設計すればいいのかが分からないかもしれません。あるいは、認証にはいろんなやり方がありますが、採用しようとしている方法で問題ないかが不安かもしれません。
この記事では、ウェブアプリケーションやネイティブアプリでのユーザー認証の仕組みや方法を説明した上で、どの方法がいいのかをケースごとに見ていきます。
私はBtoB、BtoCのウェブアプリケーションやモバイルアプリをいくつか作ってきました。その度に、ソフトウェアの種類に応じたユーザー認証について考え、設計・実装してきました。その経験をもとにこの記事を書いています。
この記事はプログラミングを学習中の方、エンジニアとしての実務経験が1〜3年目の方を対象として書いています。わかりやすさを優先して、用語の使い方が厳密でないところがあります。その際は注釈として補足しています。
テクニカルライター。元エンジニア。共著で「現場で使えるRuby on Rails 5」を書きました。プログラミング教室を作るのが目標です。
ユーザー認証とは
この記事でいうユーザー認証とは、ウェブアプリケーションやネイティブアプリにおいて、リクエストした相手がユーザーであることを確認することをいいます。たとえば現実世界でいう運転免許証での本人確認です。
確認されたユーザーにだけなんらかの機能を提供したいなどときに、ユーザー認証を行います。
ユーザー認証のフロー
ユーザー認証は、基本的に次のようなフローで行われます。
- クライアントからサーバーに認証情報を送る
- サーバー上で認証情報を検証する
- 認証情報が正しければ、クライアントにアクセストークンを送る(+必要に応じてセッションに記録する)
たとえばフォームにメールアドレスとパスワードを入力・送信し、入力内容が正しければサーバーからアクセストークンが渡される、というイメージです。
ユーザー認証を設計する上で決めるべきこと
このユーザー認証ですが、設計する上で次の4つについて決める必要があります。
順番 | 項目 | 選択肢 |
---|---|---|
1 | 認証方法をどうするか | パスワード認証、OpenID Connect |
2 | アクセストークンをどう管理するか | JWT |
3 | アクセストークンをどう引き回すか | Authorizationヘッダー、Cookieヘッダー |
4 | アクセストークンをどう保持するか | OS標準のストア、メモリ、Cookie、localStorage |
今はまだそれぞれがなにかを理解する必要はありません。これからひとつずつ見ていきましょう。
1. 認証方法をどうするか
まず、ユーザー認証の方法には、大きく次の二つがあります。
番号 | 認証方法 | 概要 |
---|---|---|
1 | パスワード認証 | IDとパスワードをサーバーに送る |
2 | OpenID Connect | 外部のプロバイダ上で認証し、アクセストークンをサーバーに送る |
OpenID Connectは、OAuth認証と聞くとなじみがあると思います。たとえばGoogleやTwitterなどのアカウントによる認証です。ただ、「OAuth認証」という言葉には語弊があります。これは後述します。この二つの認証方法についてひとつずつ見ていきます。
認証方法(1): パスワード認証
パスワード認証にも、大きく二つの認証方法があります。
- Basic認証やDigest認証など
- 独自の認証機構
独自の認証機構とは、たとえばフォームからIDとパスワードをサーバーに送って、ユーザー基盤をもとに認証を行う一般的なやり方です。広く使われているフルスタックのウェブフレームワークならだいたい認証機構をもっていると思います。
この認証方法はパスワードを平文で送ることになります。仮にSSLで通信を暗号化していたとしても、ログに書かれて流出につながったりします。
認証方法(2): OpenID Connect
次にOpenID Connectについて説明します。これはGoogleやTwitterなどのアカウントで認証する方法です。まず、前提知識として、OAuthという「アクセストークンを発行する仕組み」があります。アクセストークンは、アプリケーションをAPI経由で操作するときなんかに使われます。たとえばTwitterクライアントを自作するときにアクセストークンを使ったりします。
このOAuthは前述のとおりアクセストークンを発行する仕組みであって、認証については定められていません。認証に必要な、ユーザー情報などの取得については決まっていません。
このOAuthを拡張し、ユーザー情報の取得についてなどを標準化したのがOpenID Connectというわけです。これについては「OpenID Connectユースケース、OAuth 2.0の違い・共通点まとめ」の説明がわかりやすいです。
OpenID Connectは、「OAuth 2.0を使ってID連携をする際に、OAuth 2.0では標準化されていない機能で、かつID連携には共通して必要となる機能を標準化した」OAuth 2.0の拡張仕様の一つである。
OpenID Connectによる認証のフロー
このOpenID Connectは広く使われている認証方法なので、フローを押さえておきます。まず、IDトークンという「ユーザー情報が含まれたアクセストークン」があり、これを次のフローでやりとりします。プロバイダというのはGoogleなどの認証を行うサービスを想定しています。
順番 | リクエスト元 | 相手 | 内容 |
---|---|---|---|
1 | クライアント | プロバイダ | IDトークンを要求する |
2 | プロバイダ | ユーザー | IDトークンの発行可否を聞く。あわせて認証を行う |
3 | ユーザー | プロバイダ | 発行可否と、発行する場合に認証情報を送る |
4 | プロバイダ | クライアント | IDトークンを発行する |
この1と4のやりとりを標準化したのがOpenID Connectです。OAuthが「アクセストークンを発行する仕組み」なのに対して、OpenID Connectはこれを拡張して「IDトークンを発行する仕組み」と考えるとわかりやすいかもしれません。
詳しいことはさておき、「Googleなどのアカウント認証を使う」=「OpenID Connectを使う」という認識がもてればいいのかな、と思います。
OpenID Connectについて書きましたが、実際のところGoogleやTwitterの認証はOpenID Connectではありません。GoogleはOpenID Connectの仕様に準拠しつつ、OAuth 2.0を採用しています。TwitterはOAuth 1.0aという仕様で、OpenID Connectには準拠していません。
いずれにしてもOAuthをベースにした認証を行なっていますが、ここではわかりやすさのためにOpenID Connectという用語に統一して説明しています。
2. アクセストークンをどう管理するか
OpenID ConnectはIDトークンをやり取りする、と書きました。これはJWTというトークンの形式になっています。JWTはユーザー認証において大切な概念なので、簡単に説明しておきます。
JWTとは
JWTは「二者間で情報のやりとりを目的とした、JSONベースの形式について規定した標準仕様」です。「ジョット」と読みます。たとえばJWTは次のような値になります。
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
わかりづらいですが、三つの文字列がピリオドで連結されています。わかりやすく書くと次のようになります。
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9 . eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ . dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
それぞれBase64でエンコードされた文字列となっています。上から順番に、次のような名前と役割があります。
順番 | 名前 | 役割 |
---|---|---|
1 | ヘッダー | 署名の検証に必要な情報 |
2 | ペイロード | やり取りに必要な情報。ユーザー情報など |
3 | 署名 | 検証する内容 |
署名には秘密鍵を使うため、これを用いて検証を行うことができます。署名はヘッダーやペイロードをもとに行うので、内容の改ざんができない、という仕組みです。
クライアントからトークンを受け取ると、サーバー側でトークンが正しいかどうかをその場で検証できます。JWTを用いることで、パスワードなどの認証情報をデータベースに保存する必要がない、というメリットがあります。
上の説明ではJWTという用語を出しました。これは広く使われているのでこうしましたが、正しくはJWSという仕様です。JWSはJSON Web Signatureで、署名つきのJWTの場合にとりわけてこう呼びます。
JWTは使うな?
JWTは危険だという議論があります。確かにJWTの署名を検証する方法によっては脆弱性を作り出してしまうという問題があります。たとえばヘッダーの情報を改ざんするやり方です。これについては「JSON Web Token(JWT)の紹介とYahoo! JAPANにおけるJWTの活用」に詳しいです。
ただ、これは実装によって対応できる問題であり、ほとんどのライブラリ側で対応されています。JWTは使うメリットが大きいので、脆弱性への対応がされていることを確認した上で、積極的に使うべきだと思っています。
3. アクセストークンをどう引き回すか
上で、ユーザー認証の方法としてパスワードとOpenID Connectについて書きました。この認証結果として、アクセストークンが返されることになります。以降は、このアクセストークンを用いて認証することになります。
では、このアクセストークンはどう引き回せばいいでしょうか。つまり、どうやってクライアントからサーバーにリクエストを送ればいいのでしょうか。これには次の二つがありますが、結論からいうと「特定の条件を除いてAuthorizationヘッダーで行えばよい」と思います。
方法1: Authorizationヘッダー
このやり方は、認証を行うために定義されているAuthorizationヘッダーにアクセストークンを入れるやり方です。たとえば次のような形式になります。
Authorization: Bearer <アクセストークン>
このAuthorizationヘッダーは、Basic認証やDigest認証で使われていました。その後RFC6750でBearerというスキームが策定されました。これは単一の文字列を認証情報として送信するのに適しています。
特徴をまとめると、次のようになります。Authorizationヘッダーを用いるやり方は、OpenID Connect、パスワードのどちらの認証方法にも適しています。
- ステートレス。サーバー側にセッションストアがいらない
- ネイティブプラットフォームで扱いやすい
- Cookieが使用できない環境でも問題ない
方法2: Cookieヘッダー
このやり方は、CookieヘッダーにセッションIDを入れつつ、サーバー上でも保存しておくやり方です。ユーザー認証を一度行ったら、以降はCookieヘッダーに含まれるセッションIDとサーバー側のセッションIDを照合してユーザーを識別します。
次のような形で、サーバー側からクライアントにCookieのセットをリクエストします。
Set-Cookie: SID=<セッションID>
そして、次のような形でクライアントからサーバーにリクエストを行います。
Cookie: SID=<セッションID>
特徴をまとめると、次のようになります。APIは一般的にステートレスで行うため、Cookieヘッダーによる引き回しは実用的ではないといえます。このやり方はパスワード認証かつ、ネイティブアプリやSPAでない従来のウェブアプリケーションに適していると思います。
- ステートフル。データベースなど外部のストレージからセッションを取得する必要がある
- サーバーにリクエストするたびに自動でCookieが送信される
- CSRF脆弱性への対策をする必要がある
- 異なるドメインに対して制約がつく(=CORS)
4. アクセストークンをどう保持するか
長くなりましたが、最後のテーマです。前述のとおり、パスワードやOpenID Connectでユーザー認証を行うと、アクセストークンが発行されます。これをヘッダーに乗せて認証します。つまり、クライアント側でアクセストークンを保持しておかなければなりません。
アクセストークンはどう保存すればいいのでしょうか。大きく次の4つがありますが、結論からいうと可能な限り「OS標準のストレージ」か「メモリ」に保持します。
番号 | 場所 | 概要 |
---|---|---|
4.1 | OS標準のストレージ | iOSのKeyChain、AndroidのKeyStore |
4.2 | メモリ | JavaScriptの変数など |
4.3 | Cookie | ウェブブラウザのCookie |
4.4 | localStorage | Web Storage APIのlocalStorage |
ひとつずつ見ていきます。
4.1 OS標準のストレージ
iOSのKeyChainやAndroidのKeyStoreなど、OSが標準で提供しているストレージを利用するやり方です。Auth0によるアクセストークンの保持についての記事でも、次のメモリとあわせて推奨されているやり方です。
4.2 メモリ
JavaScriptの変数などに格納し、CookieやlocalStorageには保存しないやり方です。スコープに気をつける必要はありますが、永続化しないため安全といえます。ただ、ページから離脱するとアクセストークンが消えてしまうので、ソフトウェアが要件を満たせる場合のみ採用できるやり方になります。
4.3 Cookie
これはウェブブラウザのCookieを使うやり方です。Cookieを使うやり方にはいくつか問題があります。たとえばXSS脆弱性やCSRF脆弱性などです。
CookieにSecure属性やHttpOnly属性をつければ安全性はいくらか高まります。ただ、Authorizationヘッダーを用いる場合JavaScriptを用いることになるのでHttpOnly属性をつけられません。つまりXSS脆弱性が残ってしまいます。
4.4 localStorage
Web Storage APIのlocalStorageを使うやり方です。これもCookieと同じくJavaScriptから操作可能なので、XSS脆弱性が残ります。また、localStorageには「HTML5のLocal Storageを使ってはいけない」で書かれているようないくつかの問題点もあります。
以上、いずれの場合もクライアントとサーバーのやり取りにHTTPSで通信するのは必須ですね。HTTPだと通信が見えてしまうので、アクセストークンが盗まれる可能性があります。また、CookieやlocalStorageを使う場合は有効期限を短くしてリスクを下げるなどの対策が必要だと思います。
ユーザー認証の設計例
記事のはじめにも書きましたが、ユーザー認証を設計する上で決めるべきこととして、次の4つがあります。
順番 | 項目 | 選択肢 |
---|---|---|
1 | 認証方法をどうするか | パスワード認証、OpenID Connect |
2 | アクセストークンをどう管理するか | JWT |
3 | アクセストークンをどう引き回すか | Authorizationヘッダー、Cookieヘッダー |
4 | アクセストークンをどう保持するか | OS標準のストア、メモリ、Cookie、localStorage |
この4つについて、どう選択すればいいのでしょうか。これはソフトウェアの種類や事業のステージなどによって異なりますが、いくつかの例をとおして見てみます。
ケースに応じたユーザー認証の設計例
たとえばフルスタックなウェブフレームワークを用いて、APIを使わないウェブアプリケーションを構築するケース。この場合はパスワードで認証し、Cookieヘッダーでリクエストします。JWTは使わず、アクセストークンはクライアントのCookieに保持します。
SPAでサーバーはAPIとしてのみ利用する場合は、OpenID Connectで認証し、Authorizationヘッダーでやり取りします。アクセストークンはメモリ上に保持します。
ネイティブアプリかつユーザー基盤を自前で構築する場合は、パスワード認証をしつつJWTでやり取りします。リクエストはAuthorizationヘッダーを用い、KeyChainやKeyStoreといったOS標準のストレージを利用します。
まとめ
長くなってしまいましたが、ユーザー認証を設計するための基本的な知識はだいたい書けたかと思います。OpenID ConnectやJWTなど、実際に開発する上でもっと深く掘り下げなければならない知識はあると思いますが、この記事がユーザー認証を実装するときの参考になればうれしいです。