OneLoginが提供しているruby-samlを使ったSAML認証について調査する機会があったので、備忘も兼ねて動きをまとめようと思います。
目的
- SAML 認証時の処理シーケンスの流れを、ruby-samlのサンプルアプリを動かしながら追いつつ理解すること
想定読者
- SAML認証を触り始めて間もなく、概念をざっと確認した程度の人
- 基礎的なRailsアプリを開発したことがある人
前提
今回はOneLoginを使ったSAML認証を確認します。
またOneLoginが提供しているruby-samlを使って、開発しているアプリにSAML認証の処理の流れを確認します。
今回は、こちらもOneLoginが提供しているruby-saml-exampleというSAML認証確認用のサンプルアプリを使って検証していきます。
SAML認証に関する用語のおさらい
簡単に用語を最低限確認しておきます。
用語 | 意味 |
---|---|
Security Assertion Markup Language(SAML) | 異なるドメイン間で認証・認可データをやり取りするための標準策定されたXMLベースのマークアップ言語 |
Identity Provider(IdP) | 認証を行い、認証情報を提供する |
Service Provider (SP) | IdPに認証を委託し、IdPの認証情報を信頼してサービス提供する |
Assertion | IdPから発行される認証情報。SAML形式で発行される |
Assertion Consumer Service (ACS) | SPでIdPから発行されたAssertionを解釈し、認可を行う |
SAML認証の処理シーケンスの概要図
こちらのSAML認証の処理シーケンスの概要図を前提に、 ruby-saml-example の動きを追っていきます。
図のクラウドサービスは、 ruby-saml-example と読み替えてください。
画像引用元:SAMLとは |クラウド型シングルサインオン・アクセスコントロール(IDaaS) OneLogin - サイバネット
今回の検証では、それぞれ下記が対応します。
- SP(Service Provider): ruby-saml-example
- ユーザー: 自分自身
- IdP(ID Provider): OneLogin SAML Test Connector
検証の準備
下記の記事がわかりやすいので、こちらを参考に検証の準備をします。ここでは詳細は割愛しますが、準備のアウトラインだけ書いておきます。
SSO を実現するための SAML2.0 の実装。まずはサンプルを動かす
IdPの準備
- トライアル用の OneLogin アカウントを取得する
- IdPとしてOneLoginにOneLogin SAML Test Connectorを作成する
- OneLogin SAML Test ConnectorのConfigulationで下記の通り設定する
変更箇所 | 値の取得場所 |
---|---|
Audience | http://localhost:3000/saml/metadata |
Recipient | http://localhost:3000/saml/acs |
ACS URL Validator | ^http:¥/¥/localhost:3000¥/saml¥/acs$ |
ACS URL | http://localhost:3000/saml/acs |
SPの準備
-
ruby-saml-exampleをクローンし、
http://localhost:3000
でアプリが起動できるようにしておく - OneLogin SAML Test Connector のApp IdとIdPの証明書をアプリに設定する
処理の流れ
それではシーケンスに沿って ruby-saml-example アプリ内でどんな処理をしているか追っていきます。
① ユーザーがSPにアクセス
Webブラウザからhttp://localhost:3000
とアクセスすると、「Login」 とシンプルに表示されたページが表示されます。
② SPはユーザーのリクエストを確認し、SAML認証要求を作成 / ③ SAML認証のリクエストとともにIdPにリダイレクト
「Login」をクリックすると、設定した OneLoginの認証画面にリダイレクトされます。
図の②、③を担っているのは、SamlController#ssoアクションです。ざっくり何をやっているか確認するとこんな感じです。
-
settings = Account.get_saml_settings(get_url_base)
でOneLoginとの連携に関する各種設定を行う -
request = OneLogin::RubySaml::Authrequest.new
でOneLoginへの認証リクエストを作成する -
redirect_to(request.create(settings))
で、settings
の情報を基にOneLoginの認証画面へリダイレクトする
④ OneLoginでSAML認証リクエストを解析・認証 / ⑤ OneLoginの認証画面でユーザー認証
SPからのSAML認証リクエストに応答する形で、OneLogin の認証画面が表示されます。
OneLogin の認証画面で ID/PASS を入力し、ログインします。ログイン処理は OneLogin 側で行います。
⑥ OneLoginがSAML認証レスポンスを作成 / ⑦ SAML認証レスポンスとともにSPにリダイレクト
OneLoginでのログインが成功すると、OneLoginからSPのACS
のURLにリダイレクトされます。
- SAML認証のレスポンスは
Parameters: {"SAMLResponse"=>"XXXXXXXXXXXXXXXXXXXX..."}
のように暗号化された状態でリダイレクト先に送信されます。 - リダイレクト先は、OneLogin SAML Test Connector で指定したACS URLです。今回の場合は
http://localhost:3000/saml/acs
です。
⑧ SPがSAML認証レスポンスを検証し、ユーザーログインを許可
ACSでSAML認証レスポンスを解析・認可の判断をします。
図の⑧を担っているのは、SamlController#acsアクションです。コードを抜粋します。
def acs
settings = Account.get_saml_settings(get_url_base)
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :settings => settings)
if response.is_valid?
session[:nameid] = response.nameid
session[:attributes] = response.attributes
@attrs = session[:attributes]
logger.info "Sucessfully logged"
logger.info "NAMEID: #{response.nameid}"
render :action => :index
else
logger.info "Response Invalid. Errors: #{response.errors}"
@errors = response.errors
render :action => :fail
end
end
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :settings => settings)
でSAML認証のレスポンスを解析してます。
解析されたレスポンスは、この行の下に```logger.debug response.document
参考:<a href="https://www.samltool.com/generic_sso_res.php" target="_blank" rel="nofollow noopener">SAML Response Examples - SAML Assertion Example | SAMLTool.com</a>
saml:Issuerhttps://app.onelogin.com/saml/metadata/xxxxxx
samlp:Status
saml:Issuerhttps://app.onelogin.com/saml/metadata/xxxxxx
ds:SignedInfo
ds:Transforms
ds:DigestValueXXXXXXXXXXXXXXXXXXXXXXXXXXX
ds:SignatureValueXXX
ds:KeyInfo
ds:X509Data
ds:X509CertificateXXX
saml:Subject
example@example.com
saml:AudienceRestriction
saml:Audiencehttp://localhost:3000/saml/metadata
saml:AuthnContext
saml:AuthnContextClassRefurn:oasis:namesSAML:2.0classes:PasswordProtectedTransport
この認証情報を```response.is_valid?``` でバリデーションチェックし、問題なければユーザーにログインを許可します。
### ⑨ ユーザーがSPにログインできる
ACSのチェックでログインが許可されれば、ログイン完了後の画面に無事遷移します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/222030/9f6f014e-7bde-6c05-230c-90aecc7737d1.png)
## 検証でハマった点
これでログインするまでの一通りの流れは追えたのですが、検証中ハマった点をまとめておきます。
### ```response.is_valid?``` のバリデーションエラーでハマった
検証の最初の方で、OneLogin SAML Test Connectorの ```Audience``` と ```Recipient``` を空欄のままにしていたのですが、そうすると```http://localhost:3000/saml/acs``` でやっている```response.is_valid?``` でバリデーションエラーが出ました。
#### ```Audience```を空にした場合のバリデーションエラー
![image.png](https://qiita-image-store.s3.amazonaws.com/0/222030/8457907e-fb8e-74ef-1385-eaf8fd8423fb.png)
http://localhost:3000/saml/metadata is not a valid audience for this Response - Valid audiences: {audience}
#### ```Recipient``` を空にした場合のバリデーションエラー
![image.png](https://qiita-image-store.s3.amazonaws.com/0/222030/c7f850e9-3830-780f-f249-26858ab656cb.png)
The response was received at {recipient} instead of http://localhost:3000/saml/acs
#### バリデーションエラーの原因
ruby-saml の ```is_valid?``` で実施しているバリデーションでコケてしまっているのが原因でした。
https://github.com/onelogin/ruby-saml/blob/master/lib/onelogin/ruby-saml/response.rb#L346-L383
##### ```Audience```が空の場合
バリデーション```validate_audience```で、```Audience```が ```settings.issuer``` と合ってないぞ!と怒られている
https://github.com/onelogin/ruby-saml/blob/master/lib/onelogin/ruby-saml/response.rb#L586-L601
空の場合はSAML認証レスポンスの ```Audience``` に ```{audience}```という値が入ってくる
設定した場合
saml:Audiencehttp://localhost:3000/saml/metadata
空の場合
saml:Audience{audience}
##### ```Recipient```が空の場合
バリデーション```validate_destination```で、```Destination```が ```settings.assertion_consumer_service_url```と合ってないぞ!と怒られている
https://github.com/onelogin/ruby-saml/blob/master/lib/onelogin/ruby-saml/response.rb#L603-L625
空の場合はSAML認証レスポンスの ```Destination``` に ```{recipient}```という値が入ってくる
設定した場合
空の場合
#### 対応方法
上記バリデーションをパスするには、下記のような設定をする必要がありました。
* ```Audience``` の設定値は、 ```saml_settings``` の ```settings.issuer``` と一致させる(今回は```http://localhost:3000/saml/metadata```)
* ```Recipient``` の設定値は、 SP の ACS URL と一致させる(今回は```http://localhost:3000/saml/acs```)
#### なぜこのバリデーションが必要なのか考察
<a href="https://support.onelogin.com/hc/en-us/articles/202673944-Use-the-OneLogin-SAML-Test-Connector" target="_blank" rel="nofollow noopener">OneLogin SAML Test Connectorのドキュメント</a>だと、```Audience``` と ```Recipient```は Not Requiredとなっています。なのにそれらを空欄にすることで、ruby-saml でバリデーションが失敗するのには疑問がありました。
何故なのかあれこれ理由を考えてみたのですが、ruby-samlの```is_valid?``` では、 **レスポンスの返答先の信頼性を必ず確認したい** からなのかな、と個人的には考えています。
そもそも OneLogin における ```Audience``` と ```Recipient``` は何なのでしょうか。<a href="https://support.onelogin.com/hc/en-us/articles/202673944-Use-the-OneLogin-SAML-Test-Connector" target="_blank" rel="nofollow noopener">OneLogin SAML Test Connectorのドキュメント</a>から引用して確認してみます。
> The Recipient will tell you exactly who the SAML response is for, but the Audience will tell you, at a broader level, where the response should go. So for example, the Recipient could be Yankee Stadium, while the Audience could be New York City.
>
> (意訳)Recipient は SAMLレスポンスの返答先を正確に示しますが、 Audience は広範なレベルで、レスポンスがどこに向かうべきかを示します。 例えば、Recipientはヤンキースタジアム、Audience はニューヨーク市と捉えることが出来ます。
つまり、```Audience``` と ```Recipient```はSAMLレスポンスの返答先を定めるような役割があるということがわかります。
こちらを踏まえ、```SamlController#acs```のアクションで ```response.is_valid?``` をつけない場合を考えてみます。この場合、OneLogin SAML Test Connector の Configuration で、```ACS URL```のみ指定してさえすれば、OneLoginのログイン成功後、 ```ACS URL```にリダイレクトされ、そのままログインが出来てしまいます。
しかしその場合、アプリのACS URLに飛んでくるOneLoginからのSAML認証レスポンスが、どこをレスポンス返答先として送られたものなのか、という検証がアプリ側でできなくなります。もしかしたら別のアプリに向けたレスポンスが飛んできたとしても、アプリ側でそれを検知する術がなくなってしまうということになります。
ですので、 ```Audience``` と ```Recipient``` を指定することで、正しいSPへのレスポンスであるかどうかも検証することで、セキュリティレベルを担保しているのかなと思いました。
ちなみに```Audience``` には SPエンティティIDとも呼ばれる、SPのmetadata を返すURLを指定するようです。
今回の場合```http://localhost:3000/saml/metadata``` ですが、こちらにアクセスすると下記のような XML ファイルが表示されます。
```entityID``` や、```Location``` などが明示されていることがわかります。
md:NameIDFormaturn:oasis:namesSAML:1.1:nameid-format:emailAddress
参考:https://github.com/onelogin/ruby-saml/issues/325
## 終わりに
長くなりましたが以上です。OneLogin の SAML 認証は奥が深いですね。。
## 参考
* <a href="https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language" target="_blank" rel="nofollow noopener">Security Assertion Markup Language - Wikipedia</a>
* <a href="https://en.wikipedia.org/wiki/Identity_provider" target="_blank" rel="nofollow noopener">Identity provider - Wikipedia</a>
* <a href="https://qiita.com/katsuhiko/items/1960f96661cdf6daf63b" target="_blank" rel="nofollow noopener">SSO を実現するための SAML2.0 の実装。まずはサンプルを動かす</a>