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