株式会社メドレーDeveloper Portal

2019-04-26

ユーザー認証と OpenID Connect

こんにちは。開発本部のエンジニアの鶴です。 今回は先月に行った社内の勉強会 TechLunch の内容をご紹介させていただきます。

イントロ

Web サービスでは、ユーザーにアカウントを作ってもらい、ログインをしてサービスを利用してもらう、というユーザー認証を利用するサービスも多いかと思います。 Web サービスを開発する側としては、サービスごとに都度ユーザー認証の仕組みを構築する必要がありますが、セキュリティ対策の観点から考慮することが多く、地味に開発の工数がかかってしまいます。

また最近では、Amazon CognitoFirebase AuthenticationAuth0など、ユーザー認証サービスがいくつかリリースされ、ユーザー認証の機能をこれらの外部サービスに任せて開発の手間を省くという選択肢も取れるようになってきています。 自分自身、かつて担当したプロジェクトでユーザー認証の仕組みを Amazon Cognito にまかせてシステムを構築したことがありました。

しかし、当時は特にユーザープールの機能がリリースされて間もないこともあり、SDK の動作やサービスの仕様の理解にかなり手間取ったことを覚えています。

ユーザー認証サービスではOpenID Connectという仕様に準拠していることが多いのですが、おそらく自分にとってこの仕様の理解が疎かだったことが原因の一つだったと思います。

そこで今回は、ユーザー認証と OpenID Connect の仕組みについて改めて勉強し直したので、その内容を簡単に解説をさせていただこうと思います。

ユーザー認証とは

ユーザー認証の前に、そもそも認証とはどういう操作のことを指すのでしょうか。 みんな大好きWikipedia 先生によると、以下のような記載があります。

認証(にんしょう)とは、何かによって、対象の正当性を確認する行為を指す。 認証行為は認証対象よって分類され、認証対象が人間である場合には相手認証(本人認証)、メッセージである場合にはメッセージ認証、時刻の場合には時刻認証と呼ぶ。 単に認証と言った場合には相手認証を指す場合が多い。

ユーザー認証は Web サービスにとってリクエストを送信してきた相手の正当性を認証することなので、相手認証の 1 つですね。 さらに相手認証の認証方法として 2 通りの方法があります。

第 1 の方法は、被認証者が認証者に、秘密鍵をもっていることによって得られる何らかの能力の証明を行う方法である。第 2 の方法は、被認証者が認証者に、被認証者の公開鍵に対応する秘密鍵の知識の証明を行う方法である。

ユーザー認証の場合、多くはこの第 1 の方法での認証で、ログイン時にユーザー ID に加えて、この「秘密鍵」としてアカウント作成時に登録しておいたパスワードを入力することでユーザー認証を行っているかと思います。

Web サービスでのユーザー認証

Web サービスで扱う情報の秘匿性が高くなればなるほど、この「秘密鍵」が本当にそのユーザーにしか提供できない情報であることが求められます。

上述のようなパスワードによる認証の場合、パスワードが推測されるなどして悪意のある第三者にアカウントが乗っ取られてしまう事件はよく耳にします。

よりセキュリティを高めるため、パスワード以外の認証や多要素認証などを用いる事が増えてきました。

また、セキュリティの観点だけでなく利便性の観点からも、パスワード入力の代わりに指紋認証や顔認証によるログインや、あるいは各種 SNS アカウントによるログインも増えてきています。自社の複数のサービスを連携できるようユーザーに共通 ID を提供したい、といったケースもあるかもしれません。

最近ではパスワードレス認証やWebAuthnも注目されていますね。今回は紹介は割愛しますが、パスワードレス認証の一つであるFIDO 認証は、前述の「被認証者が認証者に、被認証者の公開鍵に対応する秘密鍵の知識の証明を行う方法」を利用した認証方式のようです。(ref1, ref2

このように、セキュリティの観点やユーザー利便性の観点などにより、Web サービスにおけるユーザー認証機能は 1 回作ったら終わりではなく、時流に応じて適宜改修する必要が出てくることもあるかと思います。

しかし、特にユーザー認証がメインのサービスと密結合している場合などでは、認証の前後など認証処理そのものだけでなくその周辺の処理への影響範囲も無視できない場合もあり、ユーザー認証の改修に工数が思ったよりかかってしまったり、対応が滞ってしまうこともあるかもしれません。

そんなとき、認証サービスをメインのサービスと切り離すことでより柔軟なユーザー認証手段を提供できるよう、OpenID Connect の導入を検討してみても良いかもしれません。

OpenID Connect とは

OpenID Connect(以降、OIDC)について、本家サイトでは以下のように説明されています。

OpenID Connect 1.0 は, OAuth 2.0 プロトコルの上にシンプルなアイデンティティレイヤーを付与したものである. このプロトコルは Client が Authorization Server の認証結果に基づいて End-User のアイデンティティを検証可能にする. また同時に End-User の必要最低限のプロフィール情報を, 相互運用可能かつ RESTful な形で取得することも可能にする.

この仕様は, OpenID Connect の主要な機能である OAuth 2.0 上で End-User の情報伝達のためにクレームを用いる認証機能を定義する. この仕様はまた, OpenID Connect を利用するための Security, Privacy Considerations を説明する.

日本語, 英語

個人的には、メインのサービスと認証サービスを切り離して運用することを想定して仕様が規定されている点が重要と考えます。 OIDC を利用することで、ユーザー認証をより柔軟に改修したり新しい認証方法に対応したりすることがしやすくなることが期待されるからです。

なお、OIDC の仕様には認証手段自体(パスワード認証や多要素認証など)に関しては規定されておらず、あくまで認証サービスによる認証結果の取得方法や扱い方についてが規定されています。

また、様々なユースケースに対応できるよういくつかの処理フローやオプショナルな設定が提供されていますが、その反面セキュリティの確保は実装者に委ねられており、ユースケースに応じて適切な実装を行う必要があります。

前述したユーザー認証サービスである Amazon Cognito や FirebaseAuthentication などは、認証手段が標準でいくつか提供されており、加えてバックエンドと SDK に OIDC 固有のセキュアな実装が施されてあるため、開発者は最小限の設定だけでユーザー認証機能が利用できるようになります。便利ですね。

処理フローの解説

さて、OIDC の具体的な処理について解説していこうと思います。

まず登場人物です。

  • OpenID Provider(OP):認証認可を行うサービス。ユーザー認証情報(識別子やパスワードなど)を管理したり、認証に関するユーザー属性情報(氏名やユーザー名など)を保持する。
  • RelyingParty(RP): アクセス元のユーザーの認証とユーザー属性情報を要求するサービス。ユーザーからのリクエストに対し OP による認証結果を信頼(rely)してリソースへのアクセスを許可する(例えばマイページを表示するなど)。
  • EndUser:ログインをしてサービスを利用しようとしているユーザー。

基本的な用語も先に簡単に紹介しておきます。

  • クライアント ID:OpenID Provider で管理する、RelyingParty の識別情報。
  • クライアントシークレット:OpenID Provider が RelyingParty ごとに発行する秘密鍵。
  • 認証コード:後述する AuthorizationCodeFlow で OpenID Provider が発行する短命のパスワードのようなもの。
  • ID トークン:OpenID Provider から発行される、ユーザーによる認証を行った証明情報。JSON Web Token(JWT)で表現され、検証により改ざん検知することができる。認証の内容(OpenID Provider、ユーザー識別子、RelyingParty のクライアント ID、有効期限など)やユーザー属性情報が格納される。
  • アクセストークン:OpenID Provider が保持するユーザー属性情報に対しアクセスするための OAuth2 の認可トークン。

OIDC の処理フローは大きく分けて 3 種類が規定されています。

  • AuthorizationCodeFlow:認証成功時に OpenID Provider が RelyingParty に対し認証コードを発行し、RelyingParty はこれを用いて OpenID Provider から ID トークン等を取得する。RelyingParty がサーバーサイドアプリケーションで、OpenID Provider から発行されるクライアントシークレットを安全に管理することができる場合などに用いられる。
  • ImplicitFlow:認証コードを使わず認証結果のレスポンスで ID トークン等を取得する。RelyingParty がクライアントアプリケーションの場合など、クライアントシークレットが安全に管理できない場合などに用いられる。
  • HybridFlow:AuthorizationCodeFlow と ImplicitFlow の組み合わせ。

これらのフローの違いは以下の表のとおりです。

f:id:medley_inc:20190426141901p:plain

公式より引用

今回は、公式こちらの解説記事などを参照しながら、基本の処理フローである AuthorizationCodeFlow について解説します。

簡略化のため、イメージ重視で登場人物は「ユーザー」「(ユーザーにサービスを提供する)Web サービス」「認証サービス」と表現することにします。

f:id:medley_inc:20190426141939p:plain

大まかには以下のステップで処理が行われます。

  1. ユーザーからのアクセスに対し、Web サービス認証サービスにユーザー認証を要求する
  2. 認証サービスはユーザー認証を行い、認証コードを発行して、ユーザーWeb サービスにリダイレクトさせる
  3. Web サービスは 2 で取得した認証コードを用いて認証サービスに ID トークン等をリクエストする
  4. Web サービスは 3 で取得した ID トークンを検証し、ユーザーの識別子を取得する

Step.0 : 事前準備

あらかじめWeb サービス認証サービスからクライアント ID とクライアントシークレットを取得し保持しておきます。

f:id:medley_inc:20190426142007p:plain

Step.1: ユーザー認証の要求

ユーザーWeb サービスに対し一般的なログインの流れでログインを要求すると、Web サービス認証サービスにリクエストをリダイレクトします。

f:id:medley_inc:20190426142502p:plain

Web サービスから認証サービスへのリダイレクトの URL は以下のような感じです。

HTTP/1.1 302 Found
Location: https://server.example.com/authorize?
   response_type=code
   &client_id=s6BhdRkqt3
   &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
   &scope=openid%20profile
   &state=af0ifjsldkj

response_type で OIDC のどの認証フローを使うかを指定します。

redirect_uri は、認証サービスでの認証が成功したときのWeb サービスにコールバックする URL です。これは事前に認証サービスに登録しておく必要があります。

scope には認証の内容を設定します。openid は必須で、他には OAuth2 のアクセストークンを使って取得できるユーザー属性情報を指定します。

scope で指定できるユーザー属性情報は以下のとおりです。

f:id:medley_inc:20190426142232p:plain

公式より引用

ユーザーの認証でよく使われそうな「氏名」や「メールアドレス」など基本的な属性情報が定義されています。

state は CSRF 対策などのためのランダム値です。認証フローを開始するたびにWeb サービスが発行し、リクエストとコールバックの間で値が維持されます。

他にもいくつかのパラメータ(nonce など)が定義されており、必要に応じて利用します。

Step.2: ユーザー認証と認証コードの発行

認証サービスでは認証手段に応じてログイン ID ・パスワードの入力フォームなどを表示し、ユーザーから認証情報を取得して認証処理を行います。

f:id:medley_inc:20190426142212p:plain

認証サービスはユーザーの認証に成功すると、認証コードを発行し、ユーザーWeb サービスにリダイレクトさせます。

HTTP/1.1 302 Found
Location: https://client.example.org/cb?
   code=SplxlOBeZQQYbYS6WxSbIA
   &state=af0ifjsldkj

リダイレクト先について、認証サービスは Step.1 で受け取った redirecturl を認証サービスに予め登録されている URL と合致することを検証する必要があります。_Web サービスのなりすましを防ぐためです。

またWeb サービス側で認証サービスからのレスポンスであることを確認できるよう、state もパラメータに含めます。

なお、認証に失敗した場合は下記のように認証エラーした内容をパラメーターに加えて Web サービスにリダイレクトさせます。

HTTP/1.1 302 Found
Location: https://client.example.org/cb?
   error=invalid_request
   &error_description=
     Unsupported%20response_type%20value
   &state=af0ifjsldkj

Step.3: 認証結果の取得

Step.2 でWeb サービス認証サービスからのリダイレクトを受け、認証コードを取得すると、この認証コードを利用して認証サービスに対して認証結果情報(ID トークンなど)を取得します。

f:id:medley_inc:20190426142612p:plain

Web サービスから認証サービスへの認証結果取得リクエストは以下のような形式になります。

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
  &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

認証コードを送信する必要があるため、POST メソッドでリクエストします。またクライアント ID とクライアントシークレットによる BASIC 認証を行います。

このリクエストでクライアントシークレットが必要になるのですが、これは認証サービスにとってWeb サービスの正当性を検証するための重要なパラメータであり、安全に管理される必要があります。

SinglePageApplication のようにユーザー側にあるアプリケーションで OIDC を処理する場合には、このクライアントシークレットが安全に管理される保証がないため、AuthenticationCodeFlow ではなくImplicitFlowなどを利用する必要があります。

Web サービスからのリクエストを受け取った認証サービスは grant_type に Step.1 で指定した処理フローに該当する情報を渡し、認証コード( code )と合わせて認証サービスにリクエストの検証をさせます。

認証サービスはリクエストの検証に成功すると、Web サービスに対し認証結果として ID トークン等を返却します。

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-cache, no-store
Pragma: no-cache
 {
  "access_token":"SlAV32hkKG",
  "token_type":"Bearer",
  "expires_in":3600,
  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
  "id_token":"eyJ0 ... NiJ9.eyJ1c ... I6IjIifX0.DeWt4Qu ... ZXso"
 }

access_token は認証サービスで管理しているユーザー属性情報を取得するための OAuth2 トークンです。

refresh_token は認証サービスから access_token を再発行する際に利用します。

Step.4: 認証結果の検証

Web サービス認証サービスから取得した ID トークンを検証します。ID トークンは前述の通り JWT で表現されており、認証サービス_の公開鍵を用いて検証することができます。

手順は こちら をご確認ください。他にも参考リンクを紹介しておきます。

下記は ID トークンに含まれる認証情報の例です。

{
  "iss": "https://server.example.com",
  "sub": "24400320",
  "aud": "s6BhdRkqt3",
  "exp": 1311281970,
  "iat": 1311280970
}

このうち sub が認証サービスで管理されているユーザーの識別子です。

iss は認証サービス、aud はWeb サービスのクライアント ID になります。

exp、iat はそれぞれ認証の有効期限と認証したタイムスタンプです。

Web サービスは ID トークンが正しい内容であることが確認できれば、これをログインセッションと紐づけて保管します。

以上で認証処理は完了です。

ユーザー属性情報の取得

ユーザー認証後、Web サービスがユーザー名などのユーザー属性情報が必要になった場合、Step.3 で取得した access_token を利用し認証サービスに対してユーザー属性情報をリクエストします。

GET /userinfo HTTP/1.1
Host: server.example.com
Authorization: Bearer SlAV32hkKG

このリクエストにより、Step.1 の scope で指定したユーザー属性情報が取得できます。

まとめ

以上少し長くなりましたが、ユーザー認証と OpenID Connect、特に基本の AuthenticationCodeFlow について解説しました。限られた発表時間の中での解説のため厳密さより雰囲気を重視した内容となりましたが、お気づきの点などあればお知らせいただければと思います。

サービスの要件やフェーズによって OIDC を取り入れるかどうかは様々ですが、ユーザー認証の実装を自前で実装、メンテナンスしていくだけでなく、Amazon Cognito などの便利な認証サービスを利用していくことも選択肢の一つとして検討してみても良いかもしれません。

そしてそれら便利な認証サービスをうまく使いこなすためにも、その背景にある OIDC の仕様や思想、そもそも認証の仕組みについて立ち返ってみると、理解が一段と深まるかとおもいます。

www.medley.jpwww.medley.jp

株式会社メドレーDeveloper Portal

© 2016 MEDLEY, INC.