tWAS＋EntraのSAML SSO接続

## この記事の概要
業務アプリケーションでのSSOは広く浸透してきました。tWAS（WebSphere Traditional）とOpenAMとの連携は詳細な記事が[こちら](https://qiita.com/ch7821/items/b3311b8c6cfe1fb47b39)で公開されていますが、その他のIdPと連携するケースも多いと思います。
この記事では、tWAS＋Azureで提供されているIdPであるEntraの接続を試していきます。

### この記事でお伝えしたいこと
SAMLの連携方式としては、IdP-initiatedまたはSP-initiatedの2つがあります。tWASでは後者の代わりにSP-redirectという方式を採用しています。これはIdP-initiatedとリダイレクトの挙動を組み合わせて、SP-initiatedに近い処理を実現するものです。

この記事では、SAMLRequestを必要とするIdPとの連携事例を記載していきます。標準実装だけでは不足するため、リクエストを実現するクラスを作成・追加します。

疎通確認というレベルの内容ですので、実環境での利用時には各種設定やコードを改めて確認していただくようお願いいたします。

## ステップ1．IdP（Entra）の設定
[こちらの記事](https://qiita.com/takason/items/b23929781d5ef5eef6db)に記載されている「Azure上での作業」を参照しながら構築してください。
## ステップ2．tWASの環境構築
tWASで提供されているSAML連携機能を担うアプリケーションを追加で導入し、セキュリティに関する設定を行ないます。この章に記載された手順の詳細は[WAS V8.5.5 SAML Web SSO 構成ガイド](https://community.ibm.com/community/user/wasdevops/viewdocument/was-v855-saml-web-sso)をご確認ねがいます。
### ACSアプリケーションの導入
ACS(Assertion Consumer Service)は、IdPとの連携を行なうための機能です。このアプリケーションを導入することで、直接SPとなるアプリにSAMLの実装を行わずに役割を任せることができます。

管理コンソールからACSアプリケーションの追加を行ないます。結果、下の画像のようにアプリケーションがインストール、起動されていることを確認します
- EARファイル：<was_install_root>/installableApps/WebSphereSamlSP.ear
- アプリケーション名：WebSphereSamlSP
- コンテキストルート：samlsps

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536604/74dcab62-7b23-34ea-eb90-b59b474a4796.png">

インストール対象は WebSphereSamlSP ですが、この記事ではDefaultApplicationを検証に使うため、こちらもインストール、起動されていることを確認します。

### トラストアソシエーションの設定
SAMLに関連する各種設定を行ないます。管理コンソール上で対応可能です。
- トラストアソシエーションのON＋インターセプタークラスの登録
- インターセプタークラスの各プロパティー設定
- グローバル・セキュリティーのカスタム・プロパティーの設定
- トラステッド認証レルムの追加

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536604/0688b127-a262-10fd-1865-d62aaf53568b.png">

sso_1.sp.login.error.page には、次のステップで作成するクラス名を指定します。

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536604/5683c427-051d-2e21-d6d2-a9f1b355ca3f.png">

### AuthnRequestProvider の実装
未認証状態での連携先として、sso_<id>.sp.login.error.pageを指定します。参考資料ではIdPのリダイレクト先URLを指定していますが、固定のリクエストで対応できない場合にカスタムマッピングクラスを作成して動的な連携リクエストを送るようにします。

IdPが要求する形式に合わせてリクエストを作成する必要があります。リクエストの編集要領は[こちら](https://qiita.com/shinsaka/items/0b0d126a59021c11da59)の記事を参考にさせていただきました。

```samlSpEntra.java
import java.util.ArrayList;
import java.util.HashMap;

import javax.servlet.http.HttpServletRequest;
import com.ibm.websphere.security.NotImplementedException;
import com.ibm.ws.wssecurity.token.UTC;
import com.ibm.wsspi.security.web.saml.AuthnRequestProvider;
import com.ibm.websphere.wssecurity.wssapi.WSSUtilFactory;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;

public class samlSpEntra implements com.ibm.wsspi.security.web.saml.AuthnRequestProvider {
                                                                                    
    private static String ssoUrl_default = "https://login.microsoftonline.com/xxxxxxxx/saml2";
    private static String issuer = "https://xxx.xxx.x.xxx:9443/samlsps/acs";
    private static String nameQualifier = "aNameQualifier";

	String ssoUrl = "";

    public HashMap <String, String> getAuthnRequest(HttpServletRequest req, String errorMsg, 
                                                    String acsUrl, ArrayList<String> ssoUrls)
    throws NotImplementedException {

        HashMap <String, String> map = new HashMap <String, String>();

        ssoUrl = ssoUrl_default;
        map.put(AuthnRequestProvider.SSO_URL, ssoUrl);

        String relayState = generateRandom();
        map.put(AuthnRequestProvider.RELAY_STATE, relayState);

        String requestId = generateRandom();
        map.put(AuthnRequestProvider.REQUEST_ID, requestId);

        String authnMessage = createAuthnRequest(requestId);

        try {
            WSSUtilFactory wssuf = WSSUtilFactory.getInstance();
            String encAuthnMsg = wssuf.encode(authnMessage.getBytes());

            map.put(AuthnRequestProvider.AUTHN_REQUEST, encAuthnMsg);     
        } catch ( Exception  e ) {
        }
        return map;
    }

    private String createAuthnRequest(String reqId) {
        String AuthnRequestElementStart = "<samlp:AuthnRequest ";
        String AuthnRequestAttr = "xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" "
            + "ID=\"" + reqId + "\" Version=\"2.0\" "
            + "IssueInstant=\"" + UTC.format(new java.util.Date()) + "\" ForceAuthn=\"false\" IsPassive=\"false\" "
            + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">"
            + "";
        String issuerElement = "<Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">" + issuer + "</Issuer>";
        String AuthnRequestElementEnd = "</samlp:AuthnRequest>";

        return AuthnRequestElementStart + AuthnRequestAttr + issuerElement + AuthnRequestElementEnd;
    }

    public String getIdentityProviderOrErrorURL(HttpServletRequest arg0, String arg1, String arg2,
               ArrayList<String> arg3) throws NotImplementedException {
        return null;
    }

    private String generateRandom() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        String rtn = "REQ" + sdf.format(new Date()) + "_" + UUID.randomUUID().toString();
        return rtn.trim();
    }
}
```
作成したクラスをコンパイルして、lib/extなどアプリケーションから参照可能な場所に配置します。

### アプリケーションの認可設定
対象アプリケーションに対する権限をSAML認証したユーザーに割り当てる設定を行ないます。管理コンソールのアプリケーション一覧からDefaultApplicationを選択し、「Security role to user/group mapping」を選択します。

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536604/187d3792-b0d5-b1b7-ba52-10f93e84ba18.png">

「All Role」がリスト上に存在するので、行選択のうえ「Map Special Subjects」から「All Authenticated in Trusted Realms」を選択します。リスト上に選択が反映されたことを確認してsaveします。

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536604/c206a61f-f865-35ea-26ad-6addc5d7d1f2.png">

## ステップ3．メタデータ交換
IdPとSPで連携に関するメタデータをエクスポートし、相手側でインポートします。
- Entraのメタデータエクスポートおよびインポートは[こちらの記事](https://qiita.com/takason/items/b23929781d5ef5eef6db)を参考にしてください
    - Entraのメタデータは「フェデレーション メタデータXML」が該当します。ここからダウンロードしてください
- tWASのメタデータエクスポート/インポートは管理コンソールではなくwsadminコマンドを使用する必要があります

## ステップ4．アプリケーションへのアクセス
今回使用する SnoopServlet は、リクエストの内容を解析した結果をHTMLで即時に返却します。このServletは (対象ホスト):9443/snoop でアクセスします。
今回は認証が行われていない場合に呼び出されるsso_1.sp.login.error.pageに、AuthnRequestProviderを実装したクラスを指定しているため、この実装にあわせたリダイレクトが行われます。

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536604/4f1ebb86-7599-f438-51a8-24d949361c6d.png">

結果として、Entraの提供するログイン画面が表示されました。URL欄から読み取れるように、Entraが提供する認証ページに接続しています。
この画面でアカウントをクリックすると、検証した環境ではメールで認証コードが送信され、認証コードを入力した後に SnoopServlet の画面に遷移しました。

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536604/f4839714-0799-e0da-9449-36bb94e384a0.png">

URL欄から、アプリケーションのサイトにアクセスされていることが分かります。

## まとめ
tWASで実現するSAML連携の実装を確認しました。SAMLRequestを要求するサイトにも接続することができています。
ここでは連携方式にフォーカスしていますが、IdPに合わせたAuthnRequestProviderの実装や疎通検証、インターセプターの配置、認可関連の処理実現など、多くの考慮事項があります。そのなかで、まずはSAML連携実現のファーストステップとしてご利用いただければと思います。

## 参考
[WAS V8.5.5 SAML Web SSO 構成ガイド](https://community.ibm.com/community/user/wasdevops/viewdocument/was-v855-saml-web-sso)
[WebSphere＋OpenAMのSAML SSO環境構築（2/3）（SP構築編）](https://qiita.com/ch7821/items/b7e55ea2c194a5380f2d)
[WebSphere＋OpenAMのSAML SSO環境構築（3/3）（SSO実践編）](https://qiita.com/ch7821/items/c01d1fdb23bb5d8caec8)
[WebSphere Application ServerでSAML SSO（SP-redirected編）](https://qiita.com/ch7821/items/143ee26678532ec86f74)
[Azure ADで IBM Cloudポータルにシングルサインオンする](https://qiita.com/takason/items/b23929781d5ef5eef6db)
[AzureAD SAML SSOを使うときの SAMLRequestを生成する](https://qiita.com/shinsaka/items/0b0d126a59021c11da59)