モチベーション
- RailsでIDaaSを使った認可認証を一通り自分で作りたい
- それによってOIDC Implicit Flowの実現において、必要な処理に関する理解を深めたい
- OIDC Implicit Flowを使ったのは、既存のSPAサービスに組み込むのであれば、これが一番楽そうだったので
- 他にもSAML認証などがある。おいおい試していきたい
自分で用意したサンプル実装はこちら
今回参考にした資料
- https://github.com/nov/openid_connect
- openid foundation japan が公開しているサンプルは最新のopenid_connectでは動かない
- このコミット で動かなくなっている
前準備
上記URLの「AD テナントへのアプリケーションの登録」をそのままなぞれば大丈夫。1点だけ気をつけるべきポイントは、Reply URLsにはフルパスを指定する必要があること。
今回の処理の概要
今回は実装にあたって、openid-foundation-japanの提供するサンプルを参考にした。サンプル実装を取り入れるにあたって、あまりフローの整理などはしていない。もう少し整理ができるはずなので、ちょいちょいコードを整えていきたい。
- Client(JS Frontend)からServerに、IdP(Azure AD)への認証URLを問い合せる
- Rails Serverで、Clientを認証用 URLにリダイレクトさせる
- IdPでユーザーの認証を行う
- IdPでユーザーの認証ができたら、Rails Serverの任意のURLにリダイレクトをさせる
- Rails Serverで、IdPから受け取ったID Tokenをdecodeする
- 問題なければLogin後画面に遷移させる
実装説明
IdPの認証URL問い合わせ
# app/controllers/authorizations_controller.rb class AuthorizationsController < ApplicationController def new redirect_to authz.authorization_uri(new_state, new_nonce) end private def authz @authz ||= Authorization.new end def new_state session[:state] = SecureRandom.hex(8) end def new_nonce session[:nonce] = SecureRandom.hex(16) end end
# app/models/authorization.rb 一部抜粋 class Authorization include ActiveModel::Model def initialize @issuer = Rails.application.credentials.oidc[:issuer] @identifier = Rails.application.credentials.oidc[:identifier] @redirect_uri = Rails.application.credentials.oidc[:redirect_uri] end def authorization_uri(state, nonce) client.redirect_uri ||= redirect_uri response_type = %i[id_token token] client.authorization_uri( response_type: response_type.collect(&:to_s), state: state, nonce: nonce, scope: %i[openid email profile].collect(&:to_s) ) end private def client @client ||= OpenIDConnect::Client.new( issuer: issuer, identifier: identifier, authorization_endpoint: config.authorization_endpoint ) end def config @config ||= OpenIDConnect::Discovery::Provider::Config.discover! issuer end end
authorization_uri methodで生成される文字列は下記のようになる。
https://login.microsoftonline.com/xxx/oauth2/v2.0/authorize? client_id=xxx& nonce=33cf756a5fc3c69c05f40ae6baa17dac& redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback& response_type=id_token+token& scope=openid+email+profile& state=8fd4930f285a4e9e
ID TokenのDecode
authorizations/callback
受け取るレスポンス
http://localhost:3000/authorization #access_token=xxx &token_type=Bearer &expires_in=3599 &scope=profile+openid+email+00000003-0000-0000-c000-000000000000%2fUser.Read &id_token=xxxx &state=d612642b27161676 &session_state=e9ed8a15-9337-4cb1-bb8b-823dfef7a15d
# app/models/authorizations_controller.rb 一部抜粋 class AuthorizationsController < ApplicationController def callback end end
# app/views/authorizations/callback.html.erb <h1>CallBack!!</h1> <%= javascript_pack_tag "openid_connect" %>
// app/packs/openid_connect.js 一部抜粋 let params = {} const postBody = location.hash.substring(1); const regex = /([^&=]+)=([^&]*)/g; let m; while (m = regex.exec(postBody)) { params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); } var req = new XMLHttpRequest(); req.open('POST', '//' + window.location.host + '/authorization', true); req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); var token = document.querySelector("meta[name='csrf-token']").attributes['content'].value; req.setRequestHeader('X-CSRF-Token', token); req.onreadystatechange = function (e) { if (req.readyState == 4) { if (req.status == 200) { console.log("success!!"); } else if (req.status == 400) { alert('There was an error processing the token') } else { alert('Something other than 200 was returned') } } }; req.send(postBody);
authorization/create
# app/controllers/authorizations_controller.rb 一部抜粋 class AuthorizationsController < ApplicationController def create # TODO: strong parameter使う id_token = authz.decode_id_token(params) id_token.verify!(issuer: authz.issuer, client_id: authz.identifier, nonce: stored_nonce) session[:identifier] = id_token.subject render json: id_token.raw_attributes, status: :success end end
細かい説明については、OIOI載せていく