selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

RailsでOpenID Connect Implicit Flowを試す

モチベーション

  • RailsでIDaaSを使った認可認証を一通り自分で作りたい
  • それによってOIDC Implicit Flowの実現において、必要な処理に関する理解を深めたい
  • OIDC Implicit Flowを使ったのは、既存のSPAサービスに組み込むのであれば、これが一番楽そうだったので
    • 他にもSAML認証などがある。おいおい試していきたい

自分で用意したサンプル実装はこちら

GitHub - selmertsx/pomodoro

今回参考にした資料

前準備

https://docs.microsoft.com/ja-jp/azure/active-directory/develop/v1-protocols-oauth-code#register-your-application-with-your-ad-tenant

上記URLの「AD テナントへのアプリケーションの登録」をそのままなぞれば大丈夫。1点だけ気をつけるべきポイントは、Reply URLsにはフルパスを指定する必要があること。

f:id:selmertsx:20180813172651p:plain

今回の処理の概要

今回は実装にあたって、openid-foundation-japanの提供するサンプルを参考にした。サンプル実装を取り入れるにあたって、あまりフローの整理などはしていない。もう少し整理ができるはずなので、ちょいちょいコードを整えていきたい。

  1. Client(JS Frontend)からServerに、IdP(Azure AD)への認証URLを問い合せる
  2. Rails Serverで、Clientを認証用 URLにリダイレクトさせる
  3. IdPでユーザーの認証を行う
  4. IdPでユーザーの認証ができたら、Rails Serverの任意のURLにリダイレクトをさせる
  5. Rails Serverで、IdPから受け取ったID Tokenをdecodeする
  6. 問題なければ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載せていく