カスタム OIDC 連携(デプロイメントプラン)¶
概要¶
Guanceデプロイメントプランは、カスタム OIDC 方式によるサードパーティの ID プロバイダー(IdP)との連携をサポートしています。以下のユースケースに対応します。
- 単一の IdP でありながら、OIDC フローまたは返却構造が標準実装と異なる場合。
- 複数の IdP が併存し、ログインエントリで発信元を区別する必要がある場合。
code → token → userinfoの過程で、アドレス生成、アカウント情報の変換、またはアカウントの統合を自前で処理する必要がある場合。
本ドキュメントは「複数 IDP における OIDC 設定説明」をもとに整理し、単一 IDP の利用方法も補足しています。両シナリオは同一のカスタム OIDC 機能を共有し、違いは主にフロントエンドのログインエントリと Func のルーティングロジックにあります。
前提条件¶
- Guanceベースバージョンが
1.123.216以上であること。 - デプロイメントプランの Launcher 設定権限を有していること。
- 組み込み Func が有効であり、関数 API を作成可能であること。
- 対象の IdP から
client_id、client_secret、認可エンドポイント、トークンエンドポイント、ユーザー情報エンドポイントなどの情報を取得済みであること。 - ユーザー情報フィールドと Guanceアカウントフィールドのマッピング関係を明確にしていること。
実装の仕組み¶
カスタム OIDC 方式の核心は、OIDC の主要フローを Func に委譲することです。
OIDCClientSet.wellKnowURLは、IdP の.well-known/openid-configurationを直接参照せず、Func のwell_know関数を指します。- ログイン時、Guance内部は
authorization_endpointを介して Func のget_auth_urlを呼び出し、その関数が最終的なログインアドレスを返します。 - コールバック時、Guance内部は
userinfo_endpointを介して Func のget_userinfoを呼び出し、その関数が内部でcode → token → userinfoを実行します。 - Func は異なる IdP のユーザー情報を正規化した後、直接 Guanceに返し、システムがアカウントのマッチングとログインを処理します。
単一 IDP と複数 IDP の違い¶
| シナリオ | フロントエンドのログインエントリ | Func のルーティング方法 | コールバックアドレス |
|---|---|---|---|
| 単一 IDP | ログインボタンは 1 つだけ | type が未指定の場合はデフォルトの IdP を直接使用 |
type を省略可能。固定の値を付与しても可 |
| 複数 IDP | 複数のログインボタン。各ボタンは異なる type を保持 |
main_oidc が type に応じて異なる IdP サブスクリプトに振り分け |
対応する IdP のコールバックアドレスと一致させる必要があり、通常は type=xxx を含む |
単一 IDP の場合は、メインスクリプトでデフォルト値を直接ハードコードすることを推奨します。例:xtype = args.get("type") or "keycloak"。そうすれば、後で複数 IDP に拡張する際に、フロントエンドエントリと振り分けロジックを追加するだけで済みます。
操作手順¶
1. forethought-core/core の OIDCClientSet を設定する¶
Launcher で 名前空間:forethought-core > core に移動し、config.yaml に以下の設定を追加または変更します。
OIDCClientSet:
# カスタム OIDC を有効化
enableCustomOIDC: true
# Func の well_know 関数の API アドレスを指定
wellKnowURL: "<Func well_know API アドレス>"
mapping:
username: preferred_username
mobile: mobile
email: email
exterId: sub
説明:
enableCustomOIDC: trueはカスタム OIDC を有効にするためのキースイッチです。wellKnowURLは Func のwell_know関数を指す必要があり、IdP のサービスディスカバリアドレスを直接指定してはいけません。mapping内のフィールド名は、Func が最終的に返すユーザー情報の構造と一致している必要があります。
2. フロントエンドのログインエントリを設定する¶
Launcher で 名前空間:forethought-webclient > front-web-config に移動し、config.js を変更します。
単一 IDP の例¶
window.DEPLOYCONFIG = {
...
paasCustomLoginInfo: [
{
label: "OIDC ログイン",
url: "https://<デプロイメントプラン Web ドメイン>/oidc/login",
desc: "カスタム OIDC ログイン"
}
],
paasCustomLoginUrl: "https://<IdP ログアウトアドレス>?redirect_url=https://<デプロイメントプラン Web ドメイン>/oidc/login"
}
複数 IDP の例¶
window.DEPLOYCONFIG = {
...
paasCustomLoginInfo: [
{
iconUrl: "https://<アイコンアドレス>",
label: "Keycloak ログイン",
url: "https://<デプロイメントプラン Web ドメイン>/oidc/login?type=keycloak",
desc: "OIDC Keycloak ログイン"
},
{
iconUrl: "https://<アイコンアドレス>",
label: "Authing ログイン",
url: "https://<デプロイメントプラン Web ドメイン>/oidc/login?type=authing",
desc: "OIDC Authing ログイン"
}
]
}
説明:
- 複数 IDP のシナリオでは、
typeパラメータで発信元を識別することを推奨します。 - この
typeは Func に透過され、メインスクリプトが異なる IdP を区別するために使用します。 - IdP のコールバックアドレスも
typeに依存する場合、フロントエンドのログインエントリ、IdP の設定、Func サブスクリプト内のredirect_uriの 3 か所で一致させる必要があります。
補足:SSO ログアウトアドレスの設定¶
ユーザーが Guanceからログアウトしたときに、サードパーティの認証センターからも同時にログアウトさせたい場合は、front-web-config の config.js に paasCustomLoginUrl を追加します。
window.DEPLOYCONFIG = {
...
paasCustomLoginInfo: [
{
label: "OIDC ログイン",
url: "https://<デプロイメントプラン Web ドメイン>/oidc/login",
desc: "カスタム OIDC ログイン"
}
],
paasCustomLoginUrl: "https://<IdP end_session_endpoint>?redirect_url=https://<デプロイメントプラン Web ドメイン>/oidc/login"
}
説明:
paasCustomLoginUrlはサードパーティのログアウトアドレスです。通常、IdP の.well-known/openid-configurationにあるend_session_endpointを参照します。- カスタム OIDC 方式では、
OIDCClientSet.wellKnowURLは Func のwell_knowインターフェースを指しますが、paasCustomLoginUrlには最終的なサードパーティ IdP のログアウトアドレスを記入し、Func API アドレスは記入しません。 - PDF の初期設定例では、サードパーティのログアウトアドレスは
redirect_urlパラメータを使用してhttps://<デプロイメントプラン Web ドメイン>/oidc/loginにリダイレクトするのが一般的です。 - お使いの IdP が
post_logout_redirect_uri、returnToなどの別のパラメータ名を使用している場合は、その IdP の実際のプロトコル要件に従ってください。 paasCustomLoginUrlは 1 つの設定値しか持てないため、複数 IDP のシナリオでは、例えば中間ログアウトアドレスを用意して、そこからコンテキストに応じて対応する IdP にリダイレクトするなど、統一的なログアウト戦略を別途設計する必要があります。- 未ログイン状態でサイトにアクセスしたときに直接サードパーティ認証を起動したい場合は、
paasCustomLoginUrlを/oidc/loginに直接設定することもできます。
3. Web Nginx の転送ルールを設定する¶
名前空間:forethought-webclient > front-web-config で nginx.conf を変更し、OIDC ログインとコールバックのリクエストを inner サービスに転送します。
location /oidc/login {
proxy_connect_timeout 5;
proxy_send_timeout 5;
proxy_read_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "keep-alive";
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
proxy_pass http://inner.forethought-core:5000/api/v1/inner/oidc/login;
}
location /oidc/callback {
proxy_connect_timeout 5;
proxy_send_timeout 5;
proxy_read_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "keep-alive";
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
proxy_pass http://inner.forethought-core:5000/api/v1/inner/oidc/callback;
}
この手順の目的は、ブラウザがアクセスする /oidc/login と /oidc/callback を最終的にデプロイメントプラン内部の OIDC 処理ロジックに到達させることです。
4. Func でメインスクリプトを作成する¶
メインスクリプトは外部に統一エントリを提供し、type に応じて異なる IdP のサブスクリプトに振り分けます。少なくとも以下の 3 つの関数を含めることを推奨します。
well_know:OIDCClientSet.wellKnowURLに使用するサービスディスカバリ情報を返します。get_auth_url:最終的に IdP にリダイレクトする認証アドレスを生成して返します。get_userinfo:コールバックパラメータを受け取り、内部でcode → token → userinfoを実行します。
例:
import __keycloak as keycloak_client
import __authing as authing_client
@DFF.API('OIDC サービスディスカバリインターフェース')
def well_know():
return {
"authorization_endpoint": "<Func get_auth_url API アドレス>",
"token_endpoint": "",
"userinfo_endpoint": "<Func get_userinfo API アドレス>"
}
@DFF.API('ログインアドレス情報を取得')
def get_auth_url(**kwargs):
args = kwargs.get("args", {})
xtype = args.get("type") or "keycloak"
if xtype == "keycloak":
return keycloak_client.get_auth_url(**kwargs)
elif xtype == "authing":
return authing_client.get_auth_url(**kwargs)
else:
raise Exception(f"不正な値 type=`{xtype}`")
@DFF.API('ユーザー情報取得インターフェース')
def get_userinfo(**kwargs):
args = kwargs.get("args", {})
xtype = args.get("type") or "keycloak"
if xtype == "keycloak":
return keycloak_client.get_userinfo(**kwargs)
elif xtype == "authing":
return authing_client.get_userinfo(**kwargs)
else:
raise Exception(f"不正な値 type=`{xtype}`")
説明:
well_knowが返すauthorization_endpointとuserinfo_endpointは、実際には Func API のアドレスです。token_endpointはこの方式では通常空欄で構いません。code → tokenの処理はget_userinfo内部で行われるためです。- 単一 IDP のシナリオでも、このメインスクリプトの層を残しておくことを推奨します。後で拡張しやすくなります。
5. IdP ごとにサブスクリプトを作成する¶
各 IdP のサブスクリプトは個別に管理することを推奨します。例:__keycloak.py、__authing.py。サブスクリプトには少なくとも以下の内容を実装する必要があります。
OIDC_SET:現在の IdP のクライアント設定。well_know:現在の IdP の実際の OIDC エンドポイント。get_auth_url:認証アドレスを生成。turn_token:コールバックのcodeからトークンを取得。turn_userinfo:トークンからユーザー情報を取得。get_userinfo:外部向けの統一エントリ。内部でturn_tokenとturn_userinfoを順に呼び出します。
構造例:
OIDC_SET = {
"client_id": "<OIDC クライアント ID>",
"client_secret": "<OIDC クライアントシークレット>",
"scope": "openid profile email address",
"redirect_uri": "https://<デプロイメントプラン Web ドメイン>/oidc/callback?type=authing",
"grant_type": "authorization_code"
}
def well_know():
return {
"authorization_endpoint": "https://<IdP 認可アドレス>",
"token_endpoint": "https://<IdP トークンアドレス>",
"userinfo_endpoint": "https://<IdP ユーザー情報アドレス>",
"end_session_endpoint": "https://<IdP ログアウトアドレス>",
"jwks_uri": "https://<IdP JWKS アドレス>"
}
!!! warning
上記の `OIDC_SET` は設定構造の説明のみを目的としており、Func スクリプト内で `client_secret`、トークン、その他の機密資格情報を直接ハードコードすることは推奨しません。
鍵情報を管理する必要がある場合は、まず Func 側のパスワード型環境変数を使用して注入および読み取りを行ってください。特定の連携シナリオで依然として `OIDCClientSet.clientSecret` を使用する必要がある場合も、同様の方法で安全に管理し、平文でスクリプトや設定例に記述しないようにしてください。
重要な注意点:
- 複数 IDP のシナリオでは、
redirect_uriに対応するtype=xxxパラメータを必ず含めてください。 - IdP 側に登録するコールバックアドレスは、
OIDC_SET.redirect_uriと完全に一致している必要があります。 get_auth_urlの戻り値は{"url": "..."}の固定形式です。get_userinfoの戻り値はアカウント情報の JSON であり、ユーザー属性は第一階層の構造に直接配置することを推奨します。
SSO ログアウトの説明¶
PDF「001-デプロイメントプラン【OIDC】サードパーティ認証ログイン設定」の説明に従い、Guanceが現在サポートしている SSO ログアウト方式は以下のとおりです。
- ユーザーが Guance側でログアウトをクリックします。
- システムがまずローカルセッションを破棄します。
- ローカルセッションの破棄が完了した後、ブラウザが
paasCustomLoginUrlで指定されたサードパーティのログアウトアドレスにリダイレクトします。 - サードパーティの認証センターがログアウトを完了した後、そのパラメータルールに従ってログインページまたは指定されたページにリダイレクトします。
制限事項:
- 現在のシステムは、サードパーティの認証センターから能動的に発行されるログアウト要求を処理できません。
- 言い換えれば、Guanceが能動的にトリガーするログアウトフローのみをサポートし、IdP がシングルログアウトを発行してから Guanceにコールバックして連動ログアウトを完了する方式はサポートしていません。
- そのため、連携段階では、「誰がログアウトを発行するのか」「ログアウト後にどこに戻るのか」という 2 点を事前にクライアントと確認することを推奨します。
6. ユーザー情報フィールドを統一する¶
Func が最終的に Guanceに返すユーザー情報は、OIDCClientSet.mapping と一対一で対応する必要があります。少なくとも以下のフィールドが存在することを確認してください。
| Guanceアカウントフィールド | IdP フィールド例 |
|---|---|
username |
preferred_username |
email |
email |
mobile |
mobile |
exterId |
sub |
複数の IdP で同一人物が同じメールアドレスを使用している場合、Func 内で統一して同じアカウント識別子として処理することを推奨します。例:
emailを統一して企業メールアドレスに設定します。subまたはexterIdを統一してメールアドレス値に設定します。- 異なる IdP で返却構造が一致しない場合は、まず Func 内で統一 JSON に変換してから返却します。
この手順は、複数 IdP ログイン時に重複アカウントが生成されるのを防ぐための鍵です。
7. 必要に応じて過去の OIDC アカウントをクリーンアップする¶
環境に既存の OIDC アカウントが存在し、異なる IdP によって同じメールアドレスで複数のアカウントが生成されている場合は、以下の方法で対応できます。
-- OIDC アカウントに同じメールアドレスの重複アカウントが存在するか確認
select email, count(id) as num
from `main_account`
where status = 0 and `exterId` <> ""
group by email
having num > 1;
-- 不要なアカウントをクリーンアップした後、exterId を email に統一
update `main_account`
set exterId = email
where status = 0 and `exterId` <> "";
実行前に必ずバックアップを取得し、実際のアカウント状態を確認した上で慎重に処理してください。
デバッグに関する推奨事項¶
- 結合テストの初期段階では、
get_userinfoの最後にアカウント情報を出力し、一時的に例外を発生させて、まずフィールドが期待通りかを確認してからログインを許可してください。 - 重複アカウントが発生した場合は、まず異なる IdP が返す
email、sub、exterIdが統一されているかを確認してください。 - コールバック後にログインが失敗する場合は、まず
redirect_uriが IdP 側に登録した値と完全に一致しているかを確認してください。 - 単一 IDP から複数 IDP に変更する場合は、まずデフォルトの
typeを残し、徐々に新しいログインエントリとサブスクリプトを追加してください。
よくある質問¶
1. なぜ wellKnowURL に IdP の .well-known アドレスを直接指定できないのですか?¶
この方式の目的は、標準の OIDC エンドポイントを読み取るだけでなく、ログインアドレスの生成、コールバック処理、アカウントの正規化といったロジックを Func に委ねることにあるため、wellKnowURL はカスタムの well_know 関数を指す必要があります。
2. 単一 IDP でもこの方式を推奨する理由は何ですか?¶
多くの単一 IDP シナリオは、実質的に「非標準 OIDC」連携だからです。例えば、ログインアドレス、コールバックパラメータ、ユーザー情報構造、アカウント主キーなどを変更する必要がある場合があります。この方式を使用すると、これらの互換性問題を統一的に処理できます。
3. IdP が返すユーザー情報の構造が深い階層になっている場合はどうすればよいですか?¶
Func 内で解析してフラット化し、最終結果を第一階層の構造に整理してから Guanceに返すことを推奨します。複雑なネスト構造をそのままシステムのアカウントマッピングに渡さないでください。