Medley Developer Blog

株式会社メドレーのエンジニア・デザイナーによるブログです

ユーザー認証と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で受け取った redirect_url を認証サービスに予め登録されている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.jp

www.medley.jp