로컬 모의 서버
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(
typat+jwt)이며 한 시간 동안 유효합니다.sub,client_id,scope, 무작위jti를 포함합니다. 대상(audience)은 인가 요청의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>, 서명 인증서,persistent및emailAddressNameID 형식, 두 바인딩용 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과 배타적 정규화로 두 번 서명됩니다. 먼저 어설션, 그다음 응답 전체입니다.
InResponseTo는 요청의ID,Destination과 주체 확인의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이 필요합니다. 그 밖의 요청은WWW-Authenticate: Bearer질의와 함께401을 받습니다. 값은 고정되어 있고 공개되어 있으며, KeenAuth의 SCIM 토큰이 아닙니다. - 사용자와 그룹.
/Users와/Groups에 대한POST,GET,PUT,PATCH,DELETE(RFC 7644)입니다.userName과 그룹의displayName은 필수이며, 대소문자를 구분하지 않고 디렉터리 안에서 고유해야 합니다. 중복되면scimTypeuniqueness와 함께409를 받습니다. 그룹 구성원은 같은 디렉터리의 사용자여야 합니다. 사용자를 삭제하면 그 사용자는 그룹에서도 제거됩니다.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를 지원합니다. 한 요청의 작업들은 모두 적용되거나 전혀 적용되지 않습니다. - 버전. 모든 리소스는
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이 필요합니다. 그 밖의 요청은 알 수 없는 경로에서도WWW-Authenticate: Bearer질의와 함께401을 받습니다. 값은 고정되어 있고 공개되어 있으며, KeenAuth 관리 키가 아닙니다. - 응답. 각 작업은 문서가 선언한 성공 상태(
200,201,202,204; 두 다운로드 작업은 해석되지 않는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_…)을 따르며 문서의 패턴과 일치합니다. 문서에 없으면 접두사는 매개변수 이름에서 유도됩니다. 환경마다 한 번만 존재하는 설정(예:/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를 반환하지 않습니다. 권한, 요금제 한도, 할당량, 속도 제한, 리전은 검사하지 않고, 개인 데이터를 가리지 않으며, 이메일을 보내지 않고, 작업을 실행하지 않습니다(접수된 작업은 생성된 그대로 남습니다). 환경은 하나뿐입니다. 레코드는 프로세스가 사는 동안만 존재하며 최대 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 환경에서 테스트하십시오.