ローカルモックサーバー
keenauth mock serve は、ネットワークにアクセスできない CI ジョブとローカル開発のために、OpenID Connect、SAML 2.0、SCIM 2.0 の小さなサーバーと、/v1 管理 API のメモリ内版を手元のマシンで起動します。KeenAuth の環境、API キー、インターネット接続は必要ありません。すべてのデータはメモリに保持され、プロセスが終了すると削除されます。
モックサーバーは KeenAuth そのものではありません。以下に示すフローのみを実装し、すべてのリクエストを自動的に承認し、実際のサービスよりはるかに少ない検査しか行いません。実際の動作の基準は、ホスト型の development 環境だけです。リリースする前に、その環境に対してエンドツーエンドテストを実行してください。
サーバーの起動
keenauth mock serve
既定では、サーバーは 127.0.0.1:4010 で待ち受けます。これはループバックアドレスで、他のマシンからは到達できません。--port でポートを変更できます(0 を指定すると空いているポートが選ばれます)。--host でアドレスを変更できますが、ループバック以外のアドレスを指定するとモックサーバーがネットワークに公開され、CLI は警告を表示します。Ctrl+C で停止します。
このコマンドは認証情報を読み込まず、KeenAuth へ何も送信しません。
エンドポイント
| パス | 用途 |
|---|---|
/.well-known/openid-configuration |
OIDC ディスカバリードキュメント(/.well-known/oauth-authorization-server でも提供) |
/.well-known/jwks.json |
公開テスト鍵セット(鍵 ID keenauth-test-1) |
/oauth2/authorize |
認可エンドポイント:PKCE(S256)を使った認可コードフロー |
/oauth2/token |
トークンエンドポイント:authorization_code と refresh_token の grant |
/oauth2/userinfo |
UserInfo エンドポイント(スコープ openid を持つ Bearer アクセストークン) |
/saml-idp/<application_id>/metadata |
SAML 2.0 アイデンティティプロバイダーのメタデータ |
/saml-idp/<application_id>/sso |
SAML シングルサインオンエンドポイント(HTTP-Redirect および HTTP-POST バインディング) |
/scim/v2/<directory_id>/… |
SCIM 2.0 エンドポイント:Users、Groups、ServiceProviderConfig、ResourceTypes、Schemas |
/v1/… |
メモリ内の管理 API:管理 API の OpenAPI ドキュメントの全操作 |
ディスカバリードキュメントと SAML メタデータのエンドポイント URL は、サーバーが待ち受けているローカルアドレスを指します。<application_id> と <directory_id> は任意の名前です(A-Z、a-z、0-9、-、_ からなる 1〜64 文字)。事前に登録する必要はありません。
動作
- 同意画面もサインイン画面もありません。 有効な認可リクエストはすべて、固定の架空のテストユーザーとして即座に承認され、
code、state、issを付けてリダイレクトされます。ユーザーのsubはusr_01m3vnbwg0e8mbe7h7xxp6rmkc、メールアドレスはtest.user@example.test(検証済み)、名前はTest Userです。 - 登録済みクライアントはありません。 どの
client_idも受け付けます。クライアントシークレットは検証しません。redirect_uriは絶対httpsURI、またはループバックホスト(127.0.0.1、[::1]、localhost)上のhttpURI である必要があり、フラグメントを含めることはできません。無効なredirect_uriやclient_idの欠落は、リダイレクトではなくエラーページになります。 - PKCE は必須です。
code_challenge_method=S256を指定したcode_challengeを送る必要があります。plainは拒否されます。トークンリクエストでは、対応するcode_verifierを送る必要があります。 - コードは一度しか使えず、60 秒で期限切れになります。同じコードを二度提示すると
invalid_grantで失敗し、そのコードから発行されたすべてのトークンが取り消されます。 - トークン。 アクセストークンは RFC 9068 の JWT(
typはat+jwt)で、有効期間は 1 時間です。sub、client_id、scope、ランダムなjtiを含みます。オーディエンスは認可リクエストのresourceパラメーターの値で、送られていない場合はhttps://test.keenauth.invalid/oauth2/userinfoです。スコープopenidを指定すると ID トークンも得られます(nonceとauth_timeを含み、スコープemailとprofileがあればemailとnameのクレームも含みます)。offline_accessを指定するとリフレッシュトークンが得られます。 - リフレッシュトークンはローテーションされます。 リフレッシュのたびに新しいリフレッシュトークンが返されます。古いものを提示すると失敗し、チェーン全体が取り消されます。リフレッシュでスコープを絞り込むことはできますが、広げることはできません。
- UserInfo は、このプロセスが発行し、期限切れや取り消しになっておらず、スコープ
openidを持つアクセストークンのみを受け付けます。
トークンと検証器
トークンはオフラインテストキットの公開テスト鍵で署名され、ローカルアドレスではなく発行者 https://test.keenauth.invalid を持ちます。トップレベルドメイン .invalid は予約されているため、本番の検証器がこのトークンを受け付けることは決してありません。SDK の検証器は、テストコードで明示的に有効にしたテストモードでのみこれらを受け付けます。
// Go: accept tokens from the mock server in a test.
v, err := keenauthtest.NewVerifier(verify.Config{Audience: "https://test.keenauth.invalid/oauth2/userinfo"})
if err != nil {
t.Fatal(err)
}
claims, err := v.VerifyAccessToken(ctx, accessToken, nil)
TypeScript(createTestVerifier)と Python(keenauth.testing.verifier)でも同じテストモードを使用します。ディスカバリードキュメントの issuer が取得元 URL と一致することを要求する汎用の OIDC クライアントライブラリは、モックサーバーのディスカバリードキュメントを拒否します。その場合はエンドポイント URL を直接設定するか、development 環境に対してテストしてください。
例:curl による完全なフロー
この例には curl、openssl、jq が必要です。先に別のターミナルでサーバーを起動してください。
BASE=http://127.0.0.1:4010
REDIRECT=http://127.0.0.1:8080/callback
VERIFIER=$(openssl rand -hex 32)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
# 1. Authorize: the server answers with a redirect that carries the code.
LOCATION=$(curl -s -o /dev/null -w '%{redirect_url}' --get "$BASE/oauth2/authorize" \
--data-urlencode response_type=code --data-urlencode client_id=app_local \
--data-urlencode redirect_uri="$REDIRECT" --data-urlencode 'scope=openid email offline_access' \
--data-urlencode state=xyz --data-urlencode code_challenge="$CHALLENGE" \
--data-urlencode code_challenge_method=S256)
CODE=$(printf '%s' "$LOCATION" | sed -E 's/.*[?&]code=([^&]+).*/\1/')
# 2. Exchange the code for tokens.
TOKENS=$(curl -s "$BASE/oauth2/token" -d grant_type=authorization_code -d client_id=app_local \
--data-urlencode code="$CODE" --data-urlencode redirect_uri="$REDIRECT" -d code_verifier="$VERIFIER")
# 3. Call UserInfo with the access token.
curl -s "$BASE/oauth2/userinfo" -H "Authorization: Bearer $(printf '%s' "$TOKENS" | jq -r .access_token)"
# 4. Refresh.
curl -s "$BASE/oauth2/token" -d grant_type=refresh_token -d client_id=app_local \
--data-urlencode refresh_token="$(printf '%s' "$TOKENS" | jq -r .refresh_token)"
手順 3 はテストユーザーを表示します:
{"email":"test.user@example.test","email_verified":true,"sub":"usr_01m3vnbwg0e8mbe7h7xxp6rmkc"}
SAML アイデンティティプロバイダー
開発中の SAML サービスプロバイダーのアイデンティティプロバイダーとして、モックサーバーを使用できます。サービスプロバイダーにメタデータ URL を指定してサインインすると、モックサーバーがテストユーザー向けの署名付きレスポンスを返します。パスは、KeenAuth が後のリリースで予定しているホスト型 SAML アイデンティティプロバイダーに準拠しています。
- メタデータ。
GET /saml-idp/<application_id>/metadataは、エンティティ IDhttps://test.keenauth.invalid/saml-idp/<application_id>、署名証明書、NameID 形式persistentとemailAddress、両バインディング用の SSO エンドポイントを含むEntityDescriptorを返します。署名付き AuthnRequest は必須ではありません(WantAuthnRequestsSigned="false")。リクエストの署名は無視されます。 - SSO。 AuthnRequest は HTTP-Redirect バインディング(
GET、SAMLRequestに DEFLATE + base64)または HTTP-POST バインディング(POST、base64)で送信します。サインイン画面はありません。有効なリクエストは即座にテストユーザーとして承認され、SAMLResponse(とRelayState、最大 80 バイト)をサービスプロバイダーの ACS URL へ自動的に送信する HTML ページが返されます。 - AuthnRequest に必要な要素。
ID、Version="2.0"、IssueInstant、saml:Issuer(サービスプロバイダーのエンティティ ID)、AssertionConsumerServiceURL。モックサーバーには登録済みのサービスプロバイダーがないためです。ACS URL は絶対httpsURL、またはループバックホスト上のhttpURL で、フラグメントを含めることはできません。ProtocolBindingを指定する場合は HTTP-POST、Destinationを指定する場合は SSO URL でなければなりません。それ以外のリクエストは400を受け取り、どこにも送信されません。DTD は拒否されます。 - レスポンス。 RSA-SHA256 と排他的正規化で 2 回署名されます。まずアサーション、次にレスポンス全体です。
InResponseToはリクエストのID、Destinationと Subject Confirmation のRecipientは ACS URL、AudienceはリクエストのIssuerです。アサーションは発行時刻から 5 分間有効です。NameID は(リクエストでemailAddressを指定しない限り)persistent(アプリケーションごとに安定した値)です。属性はemail、given_name、family_name、nameです。
署名鍵は公開されています。 公開されたラベルから導出されているため、誰でもこの証明書で検証されるレスポンスに署名できます。SHA-256 フィンガープリントは fcc2fc37b9ddb800721283156f9eb26ff1b80f099d996a65650f8cb469a9be81 で、サブジェクトには TEST ONLY と書かれています。サービスプロバイダーの開発環境およびテスト環境の設定にのみ登録してください。本番環境には決して登録しないでください。KeenAuth 自身の SAML 接続では、この制限を強制しています。ステージングおよび本番環境では、メタデータのインポートや証明書の追加時、さらにサインイン時にも、この鍵と .invalid 配下のエンティティ ID を拒否します。開発環境では受け付けます。
例:curl による SAML サインイン
この例には curl と openssl が必要です。先に別のターミナルでサーバーを起動してください。
BASE=http://127.0.0.1:4010
APP=app_local
ACS=http://127.0.0.1:8080/saml/acs
# 1. IdP metadata: entity ID, signing certificate and SSO URL for your service provider.
curl -s "$BASE/saml-idp/$APP/metadata" -o idp-metadata.xml
# 2. Send an AuthnRequest with the HTTP-POST binding (base64, no compression).
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
REQUEST=$(printf '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_req1" Version="2.0" IssueInstant="%s" AssertionConsumerServiceURL="%s"><saml:Issuer>https://sp.example.com</saml:Issuer></samlp:AuthnRequest>' "$NOW" "$ACS" | openssl base64 -A)
curl -s "$BASE/saml-idp/$APP/sso" --data-urlencode SAMLRequest="$REQUEST" --data-urlencode RelayState=xyz -o form.html
# 3. The page posts SAMLResponse to the ACS URL by itself; decode it to look at the signed response.
sed -n 's/.*name="SAMLResponse" value="\([^"]*\)".*/\1/p' form.html | openssl base64 -d -A > response.xml
grep -o 'InResponseTo="[^"]*"' response.xml | head -1
最後のコマンドは InResponseTo="_req1" を表示します。サービスプロバイダーは、idp-metadata.xml の証明書を使って response.xml を検証します。
SCIM 2.0 エンドポイント
SCIM エンドポイントは、プロビジョニングコネクターや、ユーザーとグループを作成するスクリプトなど、SCIM クライアントをネットワークなしでテストするために使用します。各 <directory_id> は、プロセスが動いている間だけ存在する、独立した空のディレクトリです。
- 認証。 すべてのリクエストは
Authorization: Bearer keenauth-mock-scim-tokenを必要とします。それ以外は401とWWW-Authenticate: Bearerのチャレンジで拒否されます。値は固定かつ公開されたものであり、KeenAuth の SCIM トークンではありません。 - ユーザーとグループ。
/Usersと/Groupsに対するPOST、GET、PUT、PATCH、DELETE(RFC 7644)。userNameとグループのdisplayNameは必須で、大文字と小文字を区別せずディレクトリ内で一意です。重複すると409とscimTypeuniquenessが返されます。グループのメンバーは同じディレクトリのユーザーでなければなりません。ユーザーを削除すると、そのユーザーはグループから削除されます。passwordは受け付けられますが破棄されます。 - フィルターとページ。
filterはandで連結したeq、ne、co、sw、ew、prをサポートします(例:userName eq "ada@example.test")。加えてstartIndexとcount(最大 200)をサポートします。attributesとexcludedAttributesはトップレベル属性を選択します。POST /Users/.searchと/Groups/.searchは、同じパラメーターを本文で受け付けます。 - PATCH は、
active、name.givenName、emails[type eq "work"].value、members[value eq "<id>"]、エンタープライズ拡張属性などのパスでadd、remove、replaceをサポートします。1 つのリクエストの操作は、すべて適用されるかまったく適用されないかのどちらかです。 - バージョン。 すべてのリソースは
meta.versionとETagヘッダー(W/"1"、W/"2"、…)を持ちます。PUT、PATCH、DELETEのIf-Matchは、バージョンが変わっていれば412を返します。GETのIf-None-Matchは304を返します。 - ディスカバリー。
ServiceProviderConfig、ResourceTypes、Schemasは、この動作を正確に記述します。/Bulkと/Meは、KeenAuth と同様に501を返します。
例:curl でユーザーをプロビジョニングする
この例には curl と jq が必要です。先に別のターミナルでサーバーを起動してください。
SCIM=http://127.0.0.1:4010/scim/v2/dir_local
AUTH='Authorization: Bearer keenauth-mock-scim-token'
CT='Content-Type: application/scim+json'
# 1. Create a user; the response carries the id, meta.version and an ETag header.
USER_ID=$(curl -s "$SCIM/Users" -H "$AUTH" -H "$CT" -d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "ada@example.test",
"name": {"givenName": "Ada", "familyName": "Lovelace"},
"emails": [{"value": "ada@example.test", "type": "work", "primary": true}]
}' | jq -r .id)
# 2. Find it with a filter.
curl -s -G "$SCIM/Users" -H "$AUTH" --data-urlencode 'filter=userName eq "ada@example.test"' | jq .totalResults
# 3. Deactivate it with PATCH, guarded by the version you read.
curl -s -X PATCH "$SCIM/Users/$USER_ID" -H "$AUTH" -H "$CT" -H 'If-Match: W/"1"' -d '{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{"op": "replace", "path": "active", "value": false}]
}' | jq -c '{active, version: .meta.version}'
# 4. Create a group with the user as a member, then delete the user.
curl -s "$SCIM/Groups" -H "$AUTH" -H "$CT" -d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName": "Engineering",
"members": [{"value": "'"$USER_ID"'"}]
}' | jq -c '.members[0] | {display, type}'
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE "$SCIM/Users/$USER_ID" -H "$AUTH"
コマンドは 1、{"active":false,"version":"W/\"2\""}、{"display":"ada@example.test","type":"User"}、204 を表示します。
管理 API(/v1)
管理 API のエンドポイントは、KeenAuth の管理 API を呼び出すコード(SDK、CLI、デプロイスクリプトなど)を、ネットワークなしの CI でテストするために使用します。パスはホスト型のものと同じです。ホスト型の代わりに基底 URL http://127.0.0.1:4010/v1 を使うだけで、他は変わりません。エンドポイントは CLI に組み込まれた管理 API の OpenAPI ドキュメントから生成されるため、そのバージョンの API の全操作に応答します。
- 認証。 すべてのリクエストは
Authorization: Bearer keenauth-mock-management-tokenを必要とします。それ以外は、未知のパスであっても401とWWW-Authenticate: Bearerのチャレンジで拒否されます。値は固定かつ公開されたものであり、KeenAuth の管理キーではありません。 - レスポンス。 各操作は、ドキュメントが宣言する成功ステータス(
200、201、202、204。2 つのダウンロード操作は、解決しないLocationを持つ302を返します)で応答します。本文は、ドキュメント内の成功レスポンスのexample、またはそれがまだない場合はレスポンススキーマから作られる最小の有効な例です。必須フィールドのみを含み、null 可能なフィールドはnull、列挙型は最初の値、文字列はpatternに一致する値になります。 - 単純な状態。 レコードを作成できるコレクション(
POST /v1/users、/v1/organizations、/v1/applications、/v1/organizations/<org_id>/rolesなど)はレコードを保持します。作成したレコードは、自身のパスと一覧のGETで返されます。PATCHとPUTで変更でき、DELETEは配下のものも含めて削除します。存在しないレコードの下のパスは404です。識別子は KeenAuth のプレフィックス形式(usr_…、org_…、app_…)で、ドキュメントのパターンに一致します。ドキュメントに指定がない場合は、パラメーター名からプレフィックスが導出されます。環境ごとに 1 つだけ存在する設定(例:/v1/identity-settings)は、PATCHまたはPUTで書き込んだ内容を保持します。API から作成できないリソース(セッションやレポートなど)は、常にサンプルで応答します。 - 一覧。
limit(1〜100、既定値 20)、カーソルとしてレコード識別子を指定するafterまたはbefore、order(既定はdesc、新しいものから)が機能します。レスポンスにはlist_metadataが含まれます。statusなどのフィルターは適用されません。 - エラー は、ドキュメントどおり
application/problem+jsonで、codeと、Idp-Request-Idヘッダーにも含まれるrequest_idを持ちます。未知のパスは404 not_found、パスが持たないメソッドはAllowヘッダー付きの405 method_not_allowed、有効な JSON(またはオブジェクト)ではない本文は400 malformed_request、JSON 以外のContent-Typeはそれを列挙する操作で415、必須フィールドの欠落はerrors[]付きの422 validation_failed、1 MiB を超える本文は413、不正なlimitやカーソルは400です。
development 環境との違い。 モックサーバーが検証するのは、本文が必須フィールドを持つ JSON オブジェクトであることだけです。フィールドの形式、長さ、列挙値、一意性は検査されず、型の誤ったフィールドは無視されます。If-Match、Idempotency-Key、その他のヘッダーは無視され、ETag は返されません。権限、プランの上限、クォータ、レート制限、リージョンは検査されず、個人データはマスクされず、メールは送信されず、ジョブは実行されません(受け付けられたジョブは作成時の状態のままです)。環境は 1 つだけです。レコードはプロセスの存続期間中だけ存在し、最大 10,000 件です。実際の API の動作を確認するには、development 環境を使用してください。
例:curl で作成、読み取り、変更、削除する
この例には curl と jq が必要です。先に別のターミナルでサーバーを起動してください。
API=http://127.0.0.1:4010/v1
AUTH='Authorization: Bearer keenauth-mock-management-token'
CT='Content-Type: application/json'
# 1. Create an organization.
ORG_ID=$(curl -s "$API/organizations" -H "$AUTH" -H "$CT" -d '{"name": "Acme"}' | jq -r .id)
# 2. Read it back and list.
curl -s "$API/organizations/$ORG_ID" -H "$AUTH" | jq -c '{id: (.id | .[0:4]), name}'
curl -s "$API/organizations" -H "$AUTH" | jq -c '{count: (.data | length), list_metadata}'
# 3. Rename it with a merge patch.
curl -s -X PATCH "$API/organizations/$ORG_ID" -H "$AUTH" -H 'Content-Type: application/merge-patch+json' -d '{"name": "Acme Inc"}' | jq -r .name
# 4. Delete it; a second read is a 404 problem.
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE "$API/organizations/$ORG_ID" -H "$AUTH"
curl -s "$API/organizations/$ORG_ID" -H "$AUTH" | jq -c '{status, code}'
# 5. Without the token.
curl -s "$API/organizations" | jq -c '{status, code}'
コマンドは {"id":"org_","name":"Acme"}、{"count":1,"list_metadata":{"after":null,"before":null}}、Acme Inc、204、{"status":404,"code":"not_found"}、{"status":401,"code":"unauthenticated"} を表示します。
非対応
テスト用クロック、DPoP、PAR、リクエストオブジェクト、デバイスフロー、クライアントクレデンシャル、トークン交換、ログアウトや取り消し、ホスト型ページはありません。SAML アイデンティティプロバイダーには、シングルログアウト、暗号化アサーション、アーティファクトバインディング、IdP 起点のサインインはありません。SCIM エンドポイントには、一括操作、ソート、/Me、入れ子のグループはなく、フィルターは or、not、括弧、gt/ge/lt/le を扱えません。これらのテストには development 環境を使用してください。