mod_auth_openidcによりApache HTTP ServerをOpenID Connect Relying Partyにする -- Azure AD編

こんにちは、滝澤です。

Apache HTTP ServerをOpenID Connect Relying Partyにするmod_auth_openidcというモジュールを使ってみる機会がありましたので、本記事で情報共有します。

なお、記事が長くなったので本編と設定例である3編に分けました。

本記事では、OP(OpenID Provider)としてAzure Active Directory (以降、Azure ADと略す)を利用する場合の設定例を紹介します。

設定例

ウェブサイトURI

まず、ウェブサイトURIとリダイレクトURIを決めます。

ウェブサイトURIには、利用するウェブサイトのURIを指定します。このとき、パス名を付けない形式にしてください。

リダイレクトURIには、リダイレクトURI(redirect_uri)として使用されるURIを指定します。コンテンツが存在するURIに被らないものにしてください。このリダイレクトURI経由でリクエストのやりとりが行われます。

ウェブサイトURIhttps://example.com
リダイレクトURIhttps://example.com/redirect_uri

アプリケーションの登録とクライアントIDとクライアントシークレットの取得

Azure ADを使う場合は次のヘルプのページに従って、アプリケーションの登録を行います。

このとき、以下のような値を指定します。

アプリケーションの種類Web アプリ/API
サインオン URL先の「ウェブサイトURI」で指定した値(例: https://example.com)
応答 URL先の「リダイレクトURI」で指定した値(例: https://example.com/redirect_uri)

ここで、「プロパティ」のブレードで表示される「アプリケーション ID」がクライアントIDになります。

さらに、「APIアクセス」の「キー」をクリックすると、「キー」のブレードが表示されるので、ここで「説明」と「有効期限」を入力して保存すると値が表示されます。これがクライアントシークレットになります。

設定ファイルの編集

設定ファイル /etc/httpd/conf.d/auth_openidc.conf を編集して、各ディレクティブを設定します。

以降、ディレクティブ毎に説明します。ただし、デフォルト値で特に問題ないと思われるものについては説明はしません。

OIDCRedirectURI

リダイレクトURI(request_uri)として指定した値を記述します。

OIDCRedirectURI https://example.com/redirect_uri

OIDCCryptoPassphrase

cookieやキャッシュエントリーなどの暗号化に利用するパスフレーズの文字列を指定します。

OIDCCryptoPassphrase XXXXXXXXXXXXXXXXXXXXXXXX

OIDCProviderMetadataURL

OpenID Connect ProviderメタデータのURIを記述します。 Azure ADの場合は次のようになります。

OIDCProviderMetadataURL https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration

ここで、{tenant}をAzure ADのテナント名に置き換えます。

ちなみに、執筆時点でのAzure ADのメタデータの内容は次のようになります。 エンドポイントの情報だけでなく、サポートしているタイプやアルゴリズムなども取得できます。

$ curl https://login.microsoftonline.com/common/.well-known/openid-configuration 2>/dev/null | jq
{
  "authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/authorize",
  "token_endpoint": "https://login.microsoftonline.com/common/oauth2/token",
  "token_endpoint_auth_methods_supported": [
    "client_secret_post",
    "private_key_jwt"
  ],
  "jwks_uri": "https://login.microsoftonline.com/common/discovery/keys",
  "response_modes_supported": [
    "query",
    "fragment",
    "form_post"
  ],
  "subject_types_supported": [
    "pairwise"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "http_logout_supported": true,
  "frontchannel_logout_supported": true,
  "end_session_endpoint": "https://login.microsoftonline.com/common/oauth2/logout",
  "response_types_supported": [
    "code",
    "id_token",
    "code id_token",
    "token id_token",
    "token"
  ],
  "scopes_supported": [
    "openid"
  ],
  "issuer": "https://sts.windows.net/{tenantid}/",
  "claims_supported": [
    "sub",
    "iss",
    "cloud_instance_name",
    "cloud_graph_host_name",
    "aud",
    "exp",
    "iat",
    "auth_time",
    "acr",
    "amr",
    "nonce",
    "email",
    "given_name",
    "family_name",
    "nickname"
  ],
  "microsoft_multi_refresh_token": true,
  "check_session_iframe": "https://login.microsoftonline.com/common/oauth2/checksession",
  "userinfo_endpoint": "https://login.microsoftonline.com/common/openid/userinfo",
  "tenant_region_scope": null,
  "cloud_instance_name": "microsoftonline.com",
  "cloud_graph_host_name": "graph.windows.net"
}

OIDCScope

scopeを指定します。

Azure ADでは"openid"しかサポートされていません。

$ curl https://login.microsoftonline.com/common/.well-known/openid-configuration 2>/dev/null | jq .scopes_supported
[
  "openid"
]

そのため、次のように設定します。

OIDCScope "openid"

参考までに、Azure AD v2.0では"openid", "profile", "email", "offline_access"がサポートされていると思われます。実際に利用できるかは未確認です。

$ curl https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration 2>/dev/null | jq .scopes_supported
[
  "openid",
  "profile",
  "email",
  "offline_access"
]

OIDCAuthRequestParams

Authentication Requestの際に追加で要求するパラメータを指定します。

サインイン要求を送信する(Microsoft)を見ると、promptを追加で要求することができます。

promptパラメータには再認証および同意を要求するかを指定します。次の値を指定することができます。

  • none(認証や同意の画面を表示しない)
  • login(再認証を求める認証画面を表示する)
  • consent(クライアントにレスポンスを返す前に同意画面を表示する)

何も指定しなければ、同意画面やログイン画面なしに再認証されます。

promptパラメータを制御しない場合は、OIDCAuthRequestParamsを指定する必要はありません。

同意画面を表示させたい場合は次のようにします。

OIDCAuthRequestParams prompt=consent

OIDCResponseType

レスポンスタイプ(response_type)を指定します。

OpenID Connect Core 1.0 -- 3. Authenticationに次の内容の"response_type"とFlowの対応が記述されています。

"response_type" valueFlow
code Authorization Code Flow
id_token Implicit Flow
id_token token Implicit Flow
code id_token Hybrid Flow
code token Hybrid Flow
code id_token token Hybrid Flow

サインイン要求を送信する(Microsoft)には次のような記述があります。

OpenID Connect サインインでは、 id_token を指定する必要があります。 code などの他の response_types が含まれていてもかまいません。

しかし、"id_token"のみの指定では「Implicit Flow」になり、(RPをOPに対するクライアントとする)クライアント認証が行われません。

今回のケースでは、ウェブアプリケーションであるため、「Authorization Code Flow」を利用したいです。

次のように調べてみると、"code"をサポートしているので、「Authorization Code Flow」が使えそうです。

$ curl https://login.microsoftonline.com/common/.well-known/openid-configuration 2>/dev/null | jq .response_types_supported
[
  "code",
  "id_token",
  "code id_token",
  "token id_token",
  "token"
]

実際に試してみると、普通に使えるので、"code"を指定するのがよいでしょう。

「Authorization Code Flow」の場合は次のように"code"を指定します。

OIDCResponseType "code"

私見ですが、先のヘルプの記述はscopeと勘違いされているような気がします(scopeは"id_token"が必須)。

OIDCClientID

クライアントID(client_id)を指定します。

OIDCClientID XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

OIDCClientSecret

クライアントシークレット(client_secret)を指定します。

OIDCClientSecret XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

OIDCPKCEMethod

PKCE code challenge method(code_challenge_method)を指定します。 "S256"でよいでしょう。

OIDCPKCEMethod S256

OIDCSessionInactivityTimeout

無操作タイムアウトの秒数を指定します。デフォルトは300秒です。

OIDCSessionInactivityTimeout 300

OIDCSessionMaxDuration

セッション有効期間を秒数で指定します。デフォルトは28800秒(8時間)です。 0を指定すると、IDトークンの期限と同じになります。

OIDCSessionMaxDuration 28800

OIDCCacheType

キャッシュのストレージのタイプを指定します。

本記事の動作確認ではRedisを使うので、"redis"を指定します。

OIDCCacheType redis

さらに、関連するディレクティブの設定も行ってください。

例えば、redisを指定した場合は下記のようにOIDCRedisCacheServerとOIDCRedisCachePasswordの指定も行う必要があります。

OIDCRedisCacheServer 127.0.0.1
#OIDCRedisCachePassword 8dTjsdkngM4Y2okg3XjYgWRX

OIDCHTMLErrorTemplate

エラーページのテンプレートファイルのパスを指定します。

OIDCHTMLErrorTemplate /etc/httpd/conf.d/auth_openidc.error.html

上記の設定を行った場合は、ファイル"/etc/httpd/conf.d/auth_openidc.error.html"に次のような内容の記述を行ってください。1つ目の%sにはエラーメッセージが、2つ目の%sにはエラーの説明が埋め込まれます。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8"/>
<title>OpenID Connect: Error</title>
</head>
<body>
<h1>OpenID Connect: Error</h1>
<p>Message: %s</p>
<p>Description: %s</p>
<p><a href="/">Home</a></p>
</body>
</html>

OIDCDefaultLoggedOutURL

ログアウト後に遷移するURIを指定します。

OIDCDefaultLoggedOutURL https://example.com/loggedout.html

上記のように設定した場合は、次のよう内容のファイルloggedout.htmlをドキュメントルートに配置します。 再びログインできるようにトップページへにリンクを用意します。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8"/>
<title>OpenID Connect: Logged out</title>
</head>
<body>
<h1>OpenID Connect: Logged out</h1>
<p><a href="/">Login</a></p>
</body>
</html>

OIDCClaimPrefix

claimを環境変数やHTTPヘッダーフィールドにする際に付与するプレフィックスを指定します。

OIDCClaimPrefix OIDC_CLAIM_

例えば、claimの一つ"name"は環境変数あるいはHTTPヘッダーフィールドの名前としては"OIDC_CLAIM_name"になります。

リバースプロキシーとして動かしている場合は、バックエンド側ウェブサーバー側でHTTPヘッダーを環境変数に変換する際の"-"と"_"の曖昧性を排除するために、次のように"_"を使わないようにした方がよいと思われます。

OIDCClaimPrefix OIDC-CLAIM-

OIDCRemoteUserClaim

apacheの認証されたユーザーIDを示す変数REMOTE_USERとして設定するclaimの名前を指定します。

デフォルトでは「sub@iss」の形式になります。 subはエンドユーザーの識別子で、issはIssuerから"https://"を除いたものになります。

Azure ADの場合は「sub@iss」の形式では「ユーザーを識別できる約40桁の文字列@sts.windows.net/テナントID/」となり、誰がログインしたのかわかりにくいので、一目で識別できる他の情報を指定してもよいでしょう。 この例では、unique_nameが実質的にメールアドレスになるため、これを利用して、unique_nameの@の左側の文字列を取り出してユーザーIDとします。

OIDCRemoteUserClaim unique_name ^(.*)@

なお、scopeとしてemailが利用できないため、claimとしてemailを取得できません。 そのため、ここでemailを利用することができません。

OIDCPassClaimsAs

claimをアプリケーション環境にどのようにして渡すかを指定します。

  • none
  • environment (環境変数)
  • headers (HTTPヘッダー)
  • both (両方) (default)
OIDCPassClaimsAs both

OIDCAuthNHeader

リバースプロキシーとして動かしている場合は、REMOTE_USERの情報をバックエンドに送信するHTTPヘッダーに追加するヘッダーフィールド名を指定します。

OIDCAuthNHeader X-Remote-User

動作確認用コンテンツの配置

動作確認として取得した変数を表示するために、次の内容のSSIを使ったページindex.shtmlをドキュメントルートに配置してみます。

REMOTE_USERはユーザーIDに、OIDC_CLAIM_nameはユーザーのフルネームに置き換わります。

明示的にログアウトできるように、ログアウトするパス「/redirect_uri?session=logout」へのリンクを用意します。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8"/>
<title>OpenID Connect</title>
</head>
<body>
<h1>OpenID Connect</h1>
<p>
<!--#echo var="OIDC_CLAIM_name" -->
(REMOTE_USER=<!--#echo var="REMOTE_USER" -->)
</p>
<p><a href="/redirect_uri?session=logout">Logout</a></p>
</body>
</html>

apacheの設定

apacheの設定は以下のようになります。

    <Location />
        # OpenID Connectによる認証・認可を要求する。
        AuthType openid-connect
        # Azure AD用の認可設定: 
        #   claimのissが下記の値(https://sts.windows.net/〜)の場合のみ認可する。
        #   issuerはテナント毎にユニークであるため、組織内のユーザーに制限できる。
        Require claim iss:https://sts.windows.net/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/

        # 動作確認のためのSSI用の設定を行う。
        Options +Includes
        DirectoryIndex index.shtml index.html
    </Location>

    # ログアウト後に遷移するページでは認証されていないユーザーにもアクセスを許可する。
    <Location /loggedout.html>
        Require all granted
    </Location>

設定の反映

設定が完了したら、設定を反映するために、apacheを再起動してみましょう。

$ sudo apachectl configtest
$ sudo systemctl reload httpd.service

実際にアクセスしてみて何か問題があれば、エラーログに出力されるので確認しましょう。

$ sudo less /var/log/httpd/error_log

動作確認

ウェブブラウザで動作確認用のサイトにアクセスすると、OpenID Connectによる認証・認可の処理が行われます。

Office 365やAzureなどのMicrosoftのサービスで認証済みでなければ、Microsoftのログインページにリダイレクトされるのでログインの操作を行います。

認証済みのユーザーの場合、あるいはログインが完了すると次のようなページが表示されます。

サイト画面

氏名とユーザーIDが表示されていれば成功です。

参考サイト

株式会社ハートビーツのインフラエンジニアから、ちょっとした情報をお届けします。