Cloud Foundryの認証をシングルサインオンにする

この記事は[Cloud Foundry Advent Calendar](https://qiita.com/advent-calendar/2017/cloudfoundry) 3日目の記事です。

# この話は何か

　Cloud FoundryにはUAAと呼ばれる認証基盤が組み込まれており、UAAを介して認証を行います。しかし会社の事情によっては、社内のLDAPと連携させたい、という要望があったり、Gmailのアカウントと連携させたい、という要望があったり、既に認証基盤があるので、そこに認証統合したい、という要望もあるのではないでしょうか。そういった要望をSAML認証という手法で解決するのが、本記事の趣旨です。

# SAMLとは？

SAMLとは、Security Assertion Markup Languageの略称であり、OASISによって策定された異なるインターネットドメイン間でユーザー認証を行うための XML をベースにした標準規格です。2002年に策定され、2005年にはバージョン2.0となっています。[Oneloginのサイトより引用](http://www.cybernet.co.jp/onelogin/function/saml.html) 

# なぜOAuth2ではないのか？

OAuth2は認可のプロトコルなので…という話をすると、論争が起きてしまうので、その話はひとまず横に置いて、別の話をすると、OAuth2はプロトコルの仕組み上、OAuthトークン取得時にSP（Service Provider）からIDP（Identity Provider）に直接通信できる状態を作る必要があり、SPからIDPの間の通信が許可できないようなファイアウォールを介することになると、OAuth2では対応できないケースが出てきます。

SAMLは通信のデータをブラウザを介してクロスサイトで通信するため、クライアントがIDP・SPの両方に通信できる状態にあれば、認証連携することができます。この仕組みを使うことで、Cloud Foundryはクラウド上にあるが、社内のLDAPサーバはクラウド上には上げたくないし、公開したくない、というようなケースが解決できます。

# 今回する話

* Cloud FoundryでSAMLプロトコルでの認証連携をする方法
* 認証連携を使用してcf loginする方法

# 今回しない話

* 認証IDPの構築方法
* SAMLのプロトコルの詳細解説

　これらを書いてしまうと、Cloud Foundryの話がどうでもよくなってしまうぐらい膨大な量になるので、この中では語りません。今回私の環境ではOpenAMのサーバを構築しましたが、IDPの構築は企業ごとにセキュリティポリシーも違うと思うので、細かい話は省略します。また、IDPの構築が面倒であれば、SAML認証に対応したIDaaSのサービスを利用するというのも一つの手だと思います。今回OpenAMという認証サーバを用意していますが、既にQiitaにもいくつか構築記事はあるようなので、そちらを参照してください。

* [OpenAMによる認証環境の構築](https://qiita.com/tkhm/items/260493729d07b012e0e2)

# 検証環境

今回私のローカルに用意した環境は以下です。これらをすべてVirtualBox上で動作させています。

* [pcfdev v0.26.0](https://pivotal.io/pcf-dev) Pivotal社提供のローカルのVirtualBox上でPCFを動かす仕組みです。とても重宝してます。pcfdevではデフォルトで192.168.11.11をapi.local.pcfdev.ioというドメインでアクセスできるように設定してくれますが、私の家庭内LANのサブネットと被ったため、今回は192.168.101.11を割り当てています。このIPアドレスの場合はapi.192.168.101.11.xip.ioというドメインでアクセスすることができます。
* [OpenAM Community Edition 11.0.3](https://github.com/ForgeRock/openam-community-edition/releases) 元々はSunのOpenSSOというソフトウェアがベースで、現在はForgeRock社が中心に開発をしています。かなり多くの認証方式に対応していて、使いこなすのは大変ですが、非常に便利なツールです。今回は192.168.11.15というIPアドレスを割り当て、openam.example.comというドメインを暫定的に割り当てました。

　ネットワーク的にはこのような状態になります。

![Network.png](https://qiita-image-store.s3.amazonaws.com/0/213990/c6855de1-e027-823a-47c8-705768180274.png)

　あと、本来であればSAMLプロトコルのやり取りは、安全性を上げるためHTTPS通信下で行わないといけないのですが、ローカルでこの記事を書くためだけに雑に構築した環境のため、クライアントとOpenAMの間はhttpで行っています。きちんとした環境はCloud Foundry、OpenAM共にHTTPSで作る必要がありますので、そこは気を付けてください。

# 全体の作業の流れ

　SAMLの詳細な解説は避けますが、IDPとSP間でSAML認証をするためには、IDPとSPはそれぞれ事前に信頼されたものであることが前提になっています。IDP・SPの情報はそれぞれメタデータという形で提供しており、IDP側ではSPのメタデータを登録して、トラストサークルと呼ばれるグループにSPを追加する、SPではIDPのメタデータを登録する、という作業が必要になります。

## トラストサークルの構築（OpenAM側作業）

　まず、OpenAM側にトラストサークルを作成します。トラストサークルはSAMLの一般的な用語なので、他の製品やIDaaSを使う上でも同じはずですが、IDaaSの場合は既に提供者がトラストサークルを作ってくれているので、そこに信頼済みにするアプリケーション（SP）を追加する、というイメージになります。

　OpenAMであれば、以下の画面でトラストサークルを作成します。

![OpenAM_Create_COT.png](https://qiita-image-store.s3.amazonaws.com/0/213990/dd410135-c03c-e0b1-f103-e88a1709c90b.png)

「新規」作成ボタンを押して、今回は`cfcot`という名前で、トラストサークルを作成します。

## メタデータの取得（Cloud Foundry側作業）

　次にトラストサークルに、Cloud FoundryのUAAのメタデータを取得して、追加します。
　Cloud FoundryのUAAでは、SAMLのメタデータを`/saml/metadata`で提供しています。
　今回は`https://login.192.168.101.11.xip.io/saml/metadata`からダウンロードできます。

## エンティティープロバイダの追加（OpenAM側作業）

　先ほど取得したSAMLのメタデータを、OpenAMに追加します。
　先ほどのOpenAMの画面で、エンティティープロバイダという項目に「エンティティーのインポート」というボタンがあるので、それを押します。

![register_entity.png](https://qiita-image-store.s3.amazonaws.com/0/213990/2b9d91c9-32bc-01ee-b0f6-5bcbefc3b663.png)

　メタデータファイルは先ほどダウンロードしてきたファイルをアップロードします。

## トラストサークルにSPを追加（OpenAM側作業）

 先ほど作成したトラストサークルに、追加したエンティティ（login.192.168.101.10.xip.io）を追加します。

![add_to_trust_circle.png](https://qiita-image-store.s3.amazonaws.com/0/213990/f3848f9c-8b4c-039d-e3ae-d71a31706d2c.png)

## IDPの作成（OpenAM側作業）

　OpenAMでは「ホストアイデンティティープロバイダの作成」ボタンから、SAML IDPを作成することができます。

　正式なものでは署名鍵なども自前で作成して、登録する必要があるのですが、今回は検証なのであらかじめ用意されている「test」という証明鍵を利用します。トラストサークルは先ほど作成したcfcotを指定して、IDPもトラストサークルに登録しましょう。「属性マッピング」はIDP側のユーザの属性をSP側に引き渡すときに属性間の対応付けを定義するものですが、今回は必要ありません。

![create_idp.png](https://qiita-image-store.s3.amazonaws.com/0/213990/47fff2db-9d93-5afc-69ad-5337e549334d.png)

## IDPのメタデータを取得（OpenAM側作業）

　次にSP側にIDPの情報を登録するために、IDPのメタデータを取得します。
　OpenAMも同様にSAMLメタデータを取得するためのエンドポイントを`/saml2/jsp/exportmetadata.jsp`で持っています。

　今回の検証サーバではhttp://openam.example.com:8080/openam/saml2/jsp/exportmetadata.jsp になります。

## UAAにIDPのメタデータを登録する（Cloud Foundry側作業）

　最後にCloud FoundryのUAAに対してIDPのメタデータを登録します。
　今回PCFdevなので、対象の設定ファイルは`/var/vcap/jobs/uaa/config/login.yml`以下になります。
　BOSHを使っている場合であれば、[login.saml.providers](https://bosh.io/jobs/uaa?source=github.com/cloudfoundry/cf-release&version=281#p=login.saml.providers)に設定を入れることになります。ここのBOSHの例では、idpMetadataにURLを入れているのですが、このURLはUAAとOpenAMがお互いに通信可能な場合でなければ、指定することができません。
　BOSHのドキュメントでもUAAのドキュメントでもあまり明らかになっていませんが、metaDataLocationというパラメータをDATAに設定することで、idpMetaDataに直接メタデータを書くことができます。

```
saml:
  providers:
    openam:
      nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
      metaDataLocation: DATA
      idpMetadata: *openam_metadata_xml
      showSamlLoginLink: true
      linkText: "OpenAM login"
      metadataTrustCheck: false
      assertionConsumerIndex: 0
```

各パラメータの意味は以下になります。

|名前|意味|
|:---|:---|
|nameID|IDPが認証のために識別できる属性を定義します。この値はSAMLプロトコルとして定義されていて、あらかじめ決めれられた値を使う必要があります。|
|metaDataLocation|idpMetadataに含まれる値を指定します。URLを指定するとメタデータ取得用のURL、DATAを指定すると、メタデータのテキストが入ることを示します。今回の例では、YAMLのエイリアスを使用して、別途記載しています。|
|showSamlLoginLink|trueを指定すると、ログイン画面にSAMLログイン用のリンクが表示されます|
|linkText|ログインのリンクテキストを設定します。|
|metadataTrustCheck|メタデータの妥当性を検証します。今回はfalseに設定しています。|
|assertionConsumerIndex|メタデータ内の表明コンシューマーのどれを使用するかインデックス番号で指定します。デフォルトは0です。|

　設定が完了したら、`monit restart uaa`(PCFDev環境)、もしくは`bosh deploy`（BOSH環境）で反映します。

　では、https://login.192.168.101.11.xip.io/ にアクセスしてみます。

![uaa_login.png](https://qiita-image-store.s3.amazonaws.com/0/213990/d96931ed-ccb3-09ed-bf2f-a0e4dc22015a.png)

　OpenAMのログインリンクが表示されました！ログインボタンをクリックするとOpenAMに移動します。OpenAMには事前に`testuser@example.com`というユーザを登録したので、それでログインします。

![openam_login.png](https://qiita-image-store.s3.amazonaws.com/0/213990/cd4c4709-f0a0-dbe6-600e-b1afc9355e39.png)

　認証が成功すると、UAAのログイン画面に戻ってきます。
　これで認証は成功です。

![login.png](https://qiita-image-store.s3.amazonaws.com/0/213990/a74573e2-01df-81ed-d11e-5fb16cf50233.png)

# cf-cliからのログイン

OpenAMでログインできるようになったので、cf-cliからログインしてみましょう。
cf-cliでSAML認証をするためには、--ssoオプションをつけます。
すると以下のように、`https://login.192.168.101.11.xip.io/passcode`にアクセスをするように求められます。

```
cf login -a https://api.192.168.101.11.xip.io --sso --skip-ssl-validation
API エンドポイント: https://api.192.168.101.11.xip.io

One Time Code ( Get one at https://login.192.168.101.11.xip.io/passcode )>
```

指定されたエンドポイントに行くとCloud Foundryのログイン画面が出るので、先ほどと同様にOpenAMのリンクをクリックして、OpenAMにログインすると以下のようにワンタイムパスワードが発行されます。

![passcode.png](https://qiita-image-store.s3.amazonaws.com/0/213990/794f1912-ba33-0f11-79ae-38571ec93d2b.png)

入手したワンタイムパスワードをcf-cliの入力欄に張り付ければ、認証完了です。

```
One Time Code ( Get one at https://login.192.168.101.11.xip.io/passcode )>
認証中です...
OK
```

# まとめと注意点

　このようにSAML認証に対応していれば、認証サーバとCloud Foundryが直接接続できるネットワークになくても認証連携することができます。

　SAML認証を使った認証連携の注意点としては、UAA上にユーザが作られるのは初回にSAML認証でログインした時点なので、初回ログインする前に、誰かがそのログイン前のユーザを、Orgに足したりすることはできないという点です。Orgにユーザを足す場合は、一度ログインしてもらって、UAA上にユーザが作成されてから操作する必要があります。

　企業ユースだと、パスワードポリシーの問題でUAAにユーザ管理できないケースもあるかもしれません。このようなときに別のプロバイダに認証機能を委譲できるのは、とても便利です。