
# Yerel sahte sunucu

`keenauth mock serve`, ağ erişimi olmayan CI işleri ve yerel geliştirme için makinenizde küçük bir OpenID Connect, SAML 2.0 ve SCIM 2.0 sunucusu ile `/v1` yönetim API'sinin bellek içi sürümünü çalıştırır. KeenAuth ortamı, API anahtarı ya da internet gerekmez. Bütün veri bellektedir ve süreç bitince silinir.

Sahte sunucu KeenAuth değildir. Yalnız aşağıdaki akışları uygular, her isteği kendiliğinden onaylar ve gerçek hizmetten çok daha az şey denetler. **Gerçek davranışın tek ölçütü barındırılan `development` ortamınızdır**: yayımlamadan önce uçtan uca testlerinizi ona karşı çalıştırın.

## Sunucuyu başlatma

```sh
keenauth mock serve
```

Sunucu varsayılan olarak `127.0.0.1:4010` adresini dinler; bu, başka makinelerin erişemediği bir loopback adresidir. `--port` portu değiştirir (`0` boş bir port seçer). `--host` adresi değiştirir; loopback dışındaki her adres sahte sunucuyu ağınıza açar ve CLI bir uyarı basar. Sunucuyu Ctrl+C ile durdurun.

Komut kimlik bilgisi okumaz ve KeenAuth'a hiçbir şey göndermez.

## Uçlar

| Yol | Amaç |
|---|---|
| `/.well-known/openid-configuration` | OIDC keşif belgesi (`/.well-known/oauth-authorization-server` adresinde de sunulur) |
| `/.well-known/jwks.json` | Herkese açık test anahtar kümesi (anahtar kimliği `keenauth-test-1`) |
| `/oauth2/authorize` | Yetkilendirme ucu: PKCE'li (`S256`) yetkilendirme kodu akışı |
| `/oauth2/token` | Token ucu: `authorization_code` ve `refresh_token` grant'leri |
| `/oauth2/userinfo` | UserInfo ucu (`openid` scope'lu Bearer access token) |
| `/saml-idp/<application_id>/metadata` | SAML 2.0 kimlik sağlayıcısı meta verisi |
| `/saml-idp/<application_id>/sso` | SAML tek oturum açma ucu (HTTP-Redirect ve HTTP-POST bağlamaları) |
| `/scim/v2/<directory_id>/…` | SCIM 2.0 uçları: `Users`, `Groups`, `ServiceProviderConfig`, `ResourceTypes`, `Schemas` |
| `/v1/…` | Bellek içi yönetim API'si: yönetim API'sinin OpenAPI belgesindeki her işlem |

Keşif belgesindeki ve SAML meta verisindeki uç URL'leri sunucunun dinlediği yerel adresi gösterir. `<application_id>` ve `<directory_id>` sizin seçtiğiniz herhangi bir addır (`A-Z`, `a-z`, `0-9`, `-`, `_` karakterlerinden 1–64 karakter); önceden bir şey kaydetmek gerekmez.

## Nasıl davranır

- **Onay ekranı ve giriş sayfası yok.** Geçerli her yetkilendirme isteği sabit, kurgusal bir test kullanıcısı için hemen onaylanır ve `code`, `state` ve `iss` ile geri yönlendirilir. Kullanıcının `sub` değeri `usr_01m3vnbwg0e8mbe7h7xxp6rmkc`, e-posta adresi `test.user@example.test` (doğrulanmış), adı `Test User`'dır.
- **Kayıtlı istemci yok.** Her `client_id` kabul edilir. İstemci sırları denetlenmez. `redirect_uri` mutlak bir `https` URI'si ya da loopback host'ta (`127.0.0.1`, `[::1]`, `localhost`) bir `http` URI'si olmalı ve parça (fragment) taşımamalıdır; geçersiz `redirect_uri` ya da eksik `client_id` yönlendirme yerine hata sayfası alır.
- **PKCE zorunludur.** `code_challenge_method=S256` ile `code_challenge` gönderilmelidir; `plain` reddedilir. Token isteği eşleşen `code_verifier`'ı göndermelidir.
- **Kodlar tek kullanımlıktır** ve 60 saniyede sona erer. Bir kodu ikinci kez sunmak `invalid_grant` ile başarısız olur ve o koddan verilen bütün token'ları iptal eder.
- **Token'lar.** Access token bir saat geçerli bir RFC 9068 JWT'sidir (`typ` `at+jwt`); `sub`, `client_id`, `scope` ve rastgele bir `jti` taşır. Hedef kitlesi yetkilendirme isteğindeki `resource` parametresi, gönderilmediyse `https://test.keenauth.invalid/oauth2/userinfo`'dur. `openid` scope'uyla bir ID token da alırsınız (`nonce` ve `auth_time` ile; `email` ve `profile` scope'larıyla `email` ve `name` claim'leri). `offline_access` ile refresh token alırsınız.
- **Refresh token'lar döner.** Her refresh yeni bir refresh token döndürür; eskisini sunmak başarısız olur ve bütün zinciri iptal eder. Refresh scope'u daraltabilir, genişletemez.
- **UserInfo** yalnız bu sürecin verdiği, süresi geçmemiş ya da iptal edilmemiş ve `openid` scope'u taşıyan access token'ları kabul eder.

## Token'lar ve doğrulayıcınız

Token'lar [çevrim dışı test kitinin](/docs/sdk/testing) herkese açık test anahtarıyla imzalanır ve yerel adresi değil, `https://test.keenauth.invalid` issuer'ını taşır. `.invalid` üst alan adı ayrılmıştır; bu yüzden üretim doğrulayıcısı bu token'ları hiçbir zaman kabul edemez: SDK doğrulayıcıları onları yalnız test kodunda açıkça açtığınız test kipinde kabul eder.

```go
// Go: bir testte sahte sunucunun token'larını kabul et.
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'te (`createTestVerifier`) ve Python'da (`keenauth.testing.verifier`) aynı test kipini kullanın. Keşif belgesindeki `issuer`'ın belgenin alındığı URL'ye eşit olmasını isteyen genel OIDC istemci kütüphaneleri sahte sunucunun keşif belgesini reddeder; onları doğrudan uç URL'leriyle yapılandırın ya da `development` ortamınıza karşı test edin.

## Örnek: curl ile bütün akış

Örnek `curl`, `openssl` ve `jq` ister. Önce sunucuyu başka bir terminalde başlatın.

```sh
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. Yetkilendirme: sunucu kodu taşıyan bir yönlendirmeyle yanıt verir.
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. Kodu token'larla değiştir.
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. Access token ile UserInfo'yu çağır.
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. adım test kullanıcısını basar:

```json
{"email":"test.user@example.test","email_verified":true,"sub":"usr_01m3vnbwg0e8mbe7h7xxp6rmkc"}
```

## SAML kimlik sağlayıcısı

Sahte sunucuyu geliştirdiğiniz bir SAML servis sağlayıcısının kimlik sağlayıcısı olarak kullanın: servis sağlayıcıyı meta veri URL'sine yöneltin, giriş yapın; sahte sunucu test kullanıcısı için imzalı bir yanıt döndürür. Yollar, KeenAuth'un sonraki bir sürüm için planladığı barındırılan SAML kimlik sağlayıcısının yollarını izler.

- **Meta veri.** `GET /saml-idp/<application_id>/metadata`; entity ID'si `https://test.keenauth.invalid/saml-idp/<application_id>` olan, imza sertifikasını, `persistent` ve `emailAddress` NameID biçimlerini ve iki bağlamanın SSO ucunu taşıyan bir `EntityDescriptor` döndürür. İmzalı AuthnRequest istenmez (`WantAuthnRequestsSigned="false"`); istekteki imza yok sayılır.
- **SSO.** AuthnRequest'i HTTP-Redirect bağlamasıyla (`GET`, `SAMLRequest` içinde DEFLATE + base64) ya da HTTP-POST bağlamasıyla (`POST`, base64) gönderin. Giriş sayfası yoktur: geçerli istek test kullanıcısı için hemen onaylanır ve `SAMLResponse`'u (ve en çok 80 bayt olan `RelayState`'inizi) servis sağlayıcının ACS URL'sine kendiliğinden gönderen bir HTML sayfası alırsınız.
- **AuthnRequest şunları taşımalıdır:** `ID`, `Version="2.0"`, `IssueInstant`, bir `saml:Issuer` (servis sağlayıcınızın entity ID'si) ve `AssertionConsumerServiceURL`; sahte sunucuda kayıtlı servis sağlayıcı yoktur. ACS URL'si mutlak bir `https` URL'si ya da loopback host'ta bir `http` URL'si olmalı ve parça taşımamalıdır. `ProtocolBinding` varsa HTTP-POST, `Destination` varsa SSO URL'si olmalıdır. Başka her istek `400` alır ve hiçbir yere gönderilmez. DTD reddedilir.
- **Yanıt** RSA-SHA256 ve exclusive kanonikleştirme ile iki kez imzalanır: önce assertion, sonra bütün yanıt. `InResponseTo` isteğinizin `ID`'si, `Destination` ve özne onayının `Recipient`'ı ACS URL'si, `Audience` isteğinizin `Issuer`'ıdır; assertion verildiği andan başlayarak 5 dakika geçerlidir. NameID, istek `emailAddress` istemedikçe `persistent`'tir (uygulama başına sabit bir değer). Öznitelikler `email`, `given_name`, `family_name` ve `name`'dir.

**İmza anahtarı herkese açıktır.** Yayımlanmış bir etiketten türetilir; bu yüzden herkes bu sertifikanın doğruladığı yanıtlar imzalayabilir. SHA-256 parmak izi `fcc2fc37b9ddb800721283156f9eb26ff1b80f099d996a65650f8cb469a9be81`, konusu `TEST ONLY` ibaresini taşır. Onu servis sağlayıcınızın yalnız geliştirme ve test yapılandırmalarına kaydedin, üretime asla. KeenAuth bunu kendi SAML bağlantıları için uygular: staging ve production ortamlarında bu anahtarı ve `.invalid` altındaki her entity ID'yi metadata içe aktarırken, sertifika eklerken ve girişte yeniden reddeder; development ortamları kabul eder.

### Örnek: curl ile SAML girişi

Örnek `curl` ve `openssl` ister. Önce sunucuyu başka bir terminalde başlatın.

```sh
BASE=http://127.0.0.1:4010
APP=app_local
ACS=http://127.0.0.1:8080/saml/acs

# 1. IdP meta verisi: servis sağlayıcınız için entity ID, imza sertifikası ve SSO URL'si.
curl -s "$BASE/saml-idp/$APP/metadata" -o idp-metadata.xml

# 2. HTTP-POST bağlamasıyla bir AuthnRequest gönder (base64, sıkıştırmasız).
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. Sayfa SAMLResponse'u ACS URL'sine kendisi gönderir; imzalı yanıtı görmek için çöz.
sed -n 's/.*name="SAMLResponse" value="\([^"]*\)".*/\1/p' form.html | openssl base64 -d -A > response.xml
grep -o 'InResponseTo="[^"]*"' response.xml | head -1
```

Son komut `InResponseTo="_req1"` yazar. Servis sağlayıcınız `response.xml`'i `idp-metadata.xml`'deki sertifikaya göre doğrular.

## SCIM 2.0 uçları

SCIM uçlarıyla bir SCIM istemcisini (provizyon bağlayıcısı ya da kullanıcı ve grup oluşturan bir betik) ağsız sınayın. Her `<directory_id>`, süreç yaşadıkça duran ayrı ve boş bir dizindir.

- **Kimlik doğrulama.** Her istek `Authorization: Bearer keenauth-mock-scim-token` ister. Başka her şey `WWW-Authenticate: Bearer` meydan okumasıyla `401` alır. Değer sabit ve herkese açıktır; bir KeenAuth SCIM token'ı değildir.
- **Kullanıcılar ve gruplar.** `/Users` ve `/Groups` üzerinde `POST`, `GET`, `PUT`, `PATCH` ve `DELETE` (RFC 7644). `userName` ve grubun `displayName`'i zorunludur ve dizinde büyük/küçük harf gözetmeden tekildir; tekrar `uniqueness` `scimType`'ıyla `409` alır. Grup üyeleri aynı dizinin kullanıcıları olmalıdır. Kullanıcı silinince gruplarından çıkar. `password` kabul edilir ve atılır.
- **Filtre ve sayfalar.** `filter`, `and` ile bağlanmış `eq`, `ne`, `co`, `sw`, `ew` ve `pr` işleçlerini destekler (örneğin `userName eq "ada@example.test"`); ayrıca `startIndex` ve `count` (en çok 200). `attributes` ve `excludedAttributes` üst düzey öznitelikleri seçer. `POST /Users/.search` ve `/Groups/.search` aynı parametreleri gövdede alır.
- **PATCH** `add`, `remove` ve `replace` işlemlerini `active`, `name.givenName`, `emails[type eq "work"].value`, `members[value eq "<id>"]` gibi yollarla ve enterprise uzantısı öznitelikleriyle destekler; bir isteğin işlemleri ya hep birlikte uygulanır ya hiç.
- **Sürümler.** Her kaynak `meta.version` ve bir `ETag` başlığı taşır (`W/"1"`, `W/"2"`, …). `PUT`, `PATCH` ve `DELETE`'te `If-Match`, sürüm değiştiyse `412`; `GET`'te `If-None-Match` `304` döndürür.
- **Keşif.** `ServiceProviderConfig`, `ResourceTypes` ve `Schemas` tam olarak bu davranışı anlatır. `/Bulk` ve `/Me`, KeenAuth'ta olduğu gibi `501` döndürür.

### Örnek: curl ile kullanıcı provizyonu

Örnek `curl` ve `jq` ister. Önce sunucuyu başka bir terminalde başlatın.

```sh
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. Kullanıcı oluştur; yanıt id'yi, meta.version'ı ve ETag başlığını taşır.
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. Filtreyle bul.
curl -s -G "$SCIM/Users" -H "$AUTH" --data-urlencode 'filter=userName eq "ada@example.test"' | jq .totalResults

# 3. Okuduğun sürüme bağlı PATCH ile pasifleştir.
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. Kullanıcıyı üye yapan bir grup oluştur, sonra kullanıcıyı sil.
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"
```

Komutlar `1`, `{"active":false,"version":"W/\"2\""}`, `{"display":"ada@example.test","type":"User"}` ve `204` yazar.

## Yönetim API'si (`/v1`)

KeenAuth yönetim API'sini çağıran kodu (bir SDK, CLI, bir dağıtım betiği) ağ olmadan CI'da sınamak için yönetim API'si uçlarını kullanın. Yollar barındırılan ortamla aynıdır: barındırılan taban URL yerine `http://127.0.0.1:4010/v1` kullanın, başka bir şey değişmez. Uçlar CLI'ya gömülü yönetim API'si OpenAPI belgesinden üretilir; yani o sürümdeki API'nin her işlemi yanıt verir.

- **Kimlik doğrulama.** Her istek `Authorization: Bearer keenauth-mock-management-token` ister. Başka her şey, bilinmeyen yollar dahil, `WWW-Authenticate: Bearer` başlığıyla `401` alır. Değer sabit ve herkese açıktır; bir KeenAuth yönetim anahtarı değildir.
- **Yanıtlar.** Her işlem belgenin bildirdiği başarı durumuyla (`200`, `201`, `202`, `204`; iki indirme ucu, çözümlenmeyen bir `Location` ile `302`) yanıt verir. Gövde, belgedeki başarı yanıtının `example` değeridir; henüz hiç yoksa yanıt şemasından kurulan en küçük geçerli örnektir: yalnız zorunlu alanlar, boş olabilen alanlar `null`, sabit listeler ilk değerleriyle, `pattern`'ine uyan dizeler.
- **Basit durum.** Kayıt oluşturulabilen bir koleksiyon (`POST /v1/users`, `/v1/organizations`, `/v1/applications`, `/v1/organizations/<org_id>/roles`, …) kayıtları tutar: oluşturduğunuz kayıt kendi yolundaki `GET` ve listede görünür, `PATCH` ve `PUT` onu değiştirir, `DELETE` onu ve altındaki her şeyi siler, olmayan bir kaydın altındaki yol `404` döner. Kimlikler KeenAuth önek biçimindedir (`usr_…`, `org_…`, `app_…`) ve belgedeki desene uyar; belge desen vermiyorsa önek parametre adından türetilir. Ortam başına tek olan ayarlar (örneğin `/v1/identity-settings`) `PATCH` ya da `PUT` ile yazdığınızı tutar. API ile oluşturulamayan kaynaklar (örneğin oturumlar ve raporlar) hep örnekle yanıt verir.
- **Listeler.** `limit` (1–100, varsayılan 20), imleç olarak kayıt kimliğiyle `after` ya da `before` ve `order` (varsayılan `desc`, en yeni önce) çalışır; yanıt `list_metadata` taşır. `status` gibi süzgeçler uygulanmaz.
- **Hatalar** belgedeki gibi `application/problem+json`'dır; `code` ve aynı zamanda `Idp-Request-Id` başlığında bulunan bir `request_id` taşır. Bilinmeyen yol `404 not_found`, yolda olmayan metot `Allow` başlıklı `405 method_not_allowed`, geçerli JSON olmayan (ya da nesne olmayan) gövde `400 malformed_request`, JSON dışı `Content-Type` bunu bildiren işlemlerde `415`, eksik zorunlu alan `errors[]` ile `422 validation_failed`, 1 MiB'tan büyük gövde `413`, hatalı `limit` ya da imleç `400` döner.

**`development` ortamından farkı.** Sahte sunucu yalnız gövdenin zorunlu alanları olan bir JSON nesnesi olduğunu denetler; alan biçimleri, uzunluklar, sabit listeler ve tekillik denetlenmez, türü yanlış alan yok sayılır. `If-Match`, `Idempotency-Key` ve diğer başlıkları yok sayar, `ETag` döndürmez. İzin, plan sınırı, kota, hız sınırı ve bölgeyi denetlemez, kişisel veriyi maskelemez, e-posta göndermez, iş çalıştırmaz (kabul edilen iş oluşturulduğu gibi kalır) ve tek ortamı vardır. Kayıtlar yalnız sürecin ömrü boyunca vardır, en çok 10.000 tane. Gerçek API'nin nasıl davrandığını öğrenmek için `development` ortamınızı kullanın.

### Örnek: curl ile oluştur, oku, değiştir, sil

Örnek `curl` ve `jq` ister. Önce sunucuyu başka bir terminalde başlatın.

```sh
API=http://127.0.0.1:4010/v1
AUTH='Authorization: Bearer keenauth-mock-management-token'
CT='Content-Type: application/json'

# 1. Bir kuruluş oluştur.
ORG_ID=$(curl -s "$API/organizations" -H "$AUTH" -H "$CT" -d '{"name": "Acme"}' | jq -r .id)

# 2. Geri oku ve listele.
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. Birleştirme yamasıyla adını değiştir.
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. Sil; ikinci okuma 404 problemidir.
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. Token'sız.
curl -s "$API/organizations" | jq -c '{status, code}'
```

Komutlar `{"id":"org_","name":"Acme"}`, `{"count":1,"list_metadata":{"after":null,"before":null}}`, `Acme Inc`, `204`, `{"status":404,"code":"not_found"}` ve `{"status":401,"code":"unauthenticated"}` yazdırır.

## Desteklenmeyenler

Test saati, DPoP, PAR, istek nesneleri, cihaz akışı, client credentials, token exchange, oturum kapatma ve iptal ile barındırılan sayfalar yoktur. SAML kimlik sağlayıcısında tek oturum kapatma, şifreli assertion, artifact bağlaması ve IdP tarafından başlatılan giriş yoktur. SCIM uçlarında toplu işlem, sıralama, `/Me` ve iç içe grup yoktur; filtrede `or`, `not`, parantez ve `gt`/`ge`/`lt`/`le` yoktur. Bunları `development` ortamınızda test edin.
