Go 서버 SDK
모듈 github.com/keenauth/keenauth-go는 KeenAuth 관리 API의 Go 클라이언트입니다. 모든 /v1 리소스에 대한 타입이 지정된 메서드는 OpenAPI 문서에서 생성됩니다(패키지 management, keenauth.Client에 포함). 직접 작성한 핵심 부분은 인증, 재시도, 멱등성 키, 타입이 지정된 오류 및 지원 중단 경고를 추가합니다. 이 모듈은 표준 라이브러리만 사용합니다. 아직 게시되지 않았습니다.
net/http 앱에서 로그인하려면 Go net/http 세션을 참조하십시오. 직접 만든 API를 보호하려면 API 보호를 참조하십시오.
API 키로 클라이언트 생성
import (
"context"
"fmt"
"os"
keenauth "github.com/keenauth/keenauth-go"
)
c, err := keenauth.NewClient("https://api.eu.keenauth.com/v1", os.Getenv("KEENAUTH_API_KEY"))
if err != nil {
panic(err)
}
user, _, err := c.UsersAPI.GetUser(context.Background(), "usr_01j9zzzzzzzzzzzzzzzzzzzz").Execute()
if err != nil {
panic(err)
}
fmt.Println(user.GetEmail())
기본 URL은 리전의 API 주소에 /v1을 붙인 것입니다. 키(idp_sk_… 또는 idp_rk_…)는 Authorization 헤더에만 담기며, 해당 주소로만 전송됩니다. 다른 호스트나 일반 http로의 리디렉션에는 키가 절대 포함되지 않습니다. 일반 http는 localhost에 대해서만 허용됩니다.
옵션:
keenauth.WithHTTPClient(h): 직접 만든http.Client입니다(해당 트랜스포트는 변경되지 않고 감싸집니다).keenauth.WithTimeout(d): 재시도를 포함한 호출 한 번의 전체 제한 시간입니다(기본값 30초).keenauth.WithClientName("my-tool", "1.4.0"): 도구 이름을User-Agent와Idp-Client에 추가합니다.keenauth.WithLogger(l): SDK 경고에 사용할*slog.Logger입니다(기본값slog.Default()).
private_key_jwt로 인증
정적 키 대신 관리 애플리케이션은 자체 키 쌍으로 로그인할 수 있습니다. SDK는 환경의 토큰 엔드포인트에서 관리 토큰을 받아 메모리에 보관하고, 만료되기 60초 전에 교체합니다. 동시에 들어온 호출은 하나의 토큰 요청을 공유합니다.
- 대시보드에서 관리 애플리케이션을 만들고 공개 키를 등록합니다(JWKS URL 또는 직접 입력, 활성 키는 최대 3개).
- 개인 키는 서버에 보관합니다(예: 비밀 파일 또는 KMS).
import (
"os"
keenauth "github.com/keenauth/keenauth-go"
"github.com/keenauth/keenauth-go/assertion"
)
pemBytes, err := os.ReadFile("/run/secrets/keenauth-mgmt-key.pem")
if err != nil {
panic(err)
}
key, err := assertion.ParsePrivateKeyPEM(pemBytes)
if err != nil {
panic(err)
}
c, err := keenauth.NewClientWithPrivateKeyJWT("https://api.eu.keenauth.com/v1", keenauth.PrivateKeyJWT{
Issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
ClientID: "app_01j9zzzzzzzzzzzzzzzzzzzz",
Key: key,
KeyID: "mgmt-key-2026-11", // the kid you registered; optional with a single key
Scope: "users:read users:write", // optional; default: the grant's default scopes
})
SDK가 전송하는 내용:
POST <Issuer>/oauth2/token,grant_type=client_credentials,client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer, 새client_assertion, 그리고 API 주소(https://api.eu.keenauth.com)로 설정된resource입니다(Audience로 변경 가능).- 어서션은
iss와sub가 클라이언트 ID로,aud가 발급자로 설정되고, 무작위jti와 2분의 수명을 가진 JWT입니다. ES256(P-256 키), EdDSA(Ed25519), RS256 또는 PS256(RSA, 2048비트 이상; PSS를 쓰려면Algorithm: "PS256"설정)으로 서명됩니다.
Key는 모든 crypto.Signer이므로 내보내지 않고도 KMS 또는 HSM에 보관된 키를 사용할 수 있습니다. 관리 토큰의 수명은 10분입니다. API가 401을 응답하면 SDK는 캐시된 토큰을 버리고, 다음 호출에서 새 토큰을 받습니다.
토큰 요청이 거부되면 HTTP 상태와 OAuth error 코드가 담긴 *keenauth.TokenError가 반환됩니다. 예를 들어 알 수 없는 키나 잘못된 KeyID에는 invalid_client가 반환됩니다. 어서션이나 토큰은 절대 포함되지 않습니다.
var te *keenauth.TokenError
if errors.As(err, &te) {
log.Printf("token request refused: %d %s", te.Status, te.Code)
}
재시도와 멱등성 키
SDK는 다음 두 조건을 모두 만족할 때만 호출을 최대 총 3회까지 재시도합니다.
- 응답이
429,502,503또는504이거나 연결이 실패한 경우(네트워크 오류이며 인증서 오류는 제외); - 메서드가
GET,PUT,DELETE이거나,Idempotency-Key를 가진POST인 경우.
Idempotency-Key를 받는 모든 POST 작업(부작용이 없는 users:search와 …:preview 호출, users/{id}:unlock을 제외한 전부)은 새 UUIDv4 키를 받으며, 해당 호출의 모든 재시도는 같은 키와 같은 본문을 보내므로 작업은 최대 한 번만 실행됩니다. 자체 재시작에도 호출을 멱등하게 만들려면 직접 키(A-Z a-z 0-9 _ - 중 16~64자)를 전달하십시오(예: .IdempotencyKey(orderKey)). SDK는 이 키를 절대 바꾸지 않습니다. If-Match를 포함한 PATCH(버전이 지정된 모든 업데이트)는 429에서만 재시도됩니다. 속도 제한기는 실행 전에 요청을 거부하며, If-Match는 변경이 두 번 적용되지 않도록 막습니다. 그 밖의 PATCH는 재시도되지 않습니다.
시도 사이에 SDK는 Retry-After가 지정한 시간(초 또는 HTTP 날짜)만큼 기다립니다. 헤더가 없으면 0.5초, 이어서 1초를 기다리며, 각각 ±25%의 지터가 적용됩니다. Retry-After가 60초를 초과하거나 대기 시간이 컨텍스트의 마감 시간을 넘기면, SDK는 기다리지 않고 응답을 반환합니다. 컨텍스트를 취소하면 대기가 중단됩니다.
목록 순회
목록 메서드는 한 페이지를 반환합니다. 이때 data와 before 및 after 커서가 담긴 list_metadata가 포함됩니다. keenauth.All은 after를 따라가며 모든 항목을 생성합니다. 다음 페이지는 도달했을 때에만 가져오므로, 반복문을 빠져나오면 추가 요청이 중단됩니다. 평소처럼 요청에 페이지 크기와 필터를 설정하십시오.
for u, err := range keenauth.All(c.UsersAPI.ListUsers(ctx).Limit(100).Status("active")) {
if err != nil {
return err // the iteration ends after an error
}
fmt.Println(u.GetId(), u.GetEmail())
}
keenauth.AllBefore는 대신 before를 따라갑니다. 예를 들어 ListLogs의 더 최근 항목을 향해 진행합니다. 생성된 모든 목록 메서드는 두 방식 모두에서 동작하며, next_cursor와 has_more로 페이지를 나누는 Events API의 ListEvents만 예외입니다. range-over-func 반복문에는 Go 1.23 이상이 필요합니다.
웹훅 검증
패키지 github.com/keenauth/keenauth-go/webhook은 웹훅 전송의 서명을 확인합니다(Standard Webhooks). 엔드포인트의 서명 비밀(idp_whsec_…)을 전달하고, 받은 그대로의 원본 본문을 넘기십시오.
import (
"io"
"net/http"
"os"
"github.com/keenauth/keenauth-go/webhook"
)
verifier, err := webhook.NewVerifier([]string{os.Getenv("KEENAUTH_WEBHOOK_SECRET")})
if err != nil {
panic(err) // webhook.ErrSecret: the secret is not well formed
}
http.HandleFunc("POST /webhooks/keenauth", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
if err := verifier.Verify(r.Header, body); err != nil {
http.Error(w, "invalid webhook", http.StatusBadRequest)
return
}
// Handle the event. Store webhook-id and skip a delivery you have already processed.
w.WriteHeader(http.StatusNoContent)
})
Verify는 webhook-timestamp가 현재 시각과 5분 이내(양방향)이고 webhook-signature의 v1 서명 중 하나가 일치하면 전송을 수락합니다. 서명은 상수 시간에 비교되며, 다른 서명 버전은 무시됩니다. 오류는 전송이 거부된 이유를 알려 줍니다. 각 오류는 errors.Is로 webhook.ErrHeaders(헤더 누락), webhook.ErrTimestamp, webhook.ErrSignature 또는 webhook.ErrSecret 중 하나와 일치합니다.
비밀을 교체할 때는 이전 비밀이 더 이상 사용되지 않을 때까지 새 비밀과 이전 비밀을 모두 전달하십시오: webhook.NewVerifier([]string{newSecret, oldSecret}). 교체 기간에는 KeenAuth가 하나의 헤더에 서명을 두 개 보낼 수도 있으며, 둘 중 하나만 일치해도 충분합니다.
타입이 지정된 이벤트와 클레임
패키지 github.com/keenauth/keenauth-go/events에는 공개 이벤트 스키마에서 생성된, 고객 이벤트 유형마다 하나씩의 구조체가 있습니다. webhook.Verifier.VerifyEvent는 전송을 검증하고 파싱하며, events.Parse는 이미 검증한 본문을 파싱합니다. 타입 switch를 사용하면 한 유형의 필드를 얻을 수 있습니다.
ev, err := verifier.VerifyEvent(r.Header, body)
if err != nil {
http.Error(w, "invalid webhook", http.StatusBadRequest) // webhook.Err… or events.ErrParse
return
}
switch e := ev.(type) {
case *events.UserCreated:
log.Printf("user %s created", e.Data.Object.ID)
case *events.EmailSendingSuspended:
log.Printf("sending suspended: %s", e.Data.Object.Reason)
case *events.Unknown:
// A type this SDK version does not know yet. e.Type and e.Raw() are still available.
}
Parse는 봉투(id,type,schema_version,environment_id,occurred_at,recorded_at,seq,data,context)와 해당 유형의 스키마가 선언한 멤버를 확인합니다. 누락되었거나 타입이 잘못된 멤버는data.object.reason과 같은 경로를 담은*events.ParseError를 반환합니다.- 알 수 없는 유형이나, 이 SDK가 모델링하지 않은
schema_version을 가진 알려진 유형은*events.Unknown으로 반환됩니다. 이는 오류가 아닙니다. - 스키마가 선언하지 않은 멤버는 유지됩니다.
Raw()는 전체 이벤트를,ObjectJSON()은data.object를 반환합니다(전체 페이로드 모드에서는 리소스의 관리 API 표현). - 열거형 필드의 값은 알려진 값과 대조하지 않으므로, 서버의 새 값 때문에 파싱이 실패하지 않습니다.
패키지 github.com/keenauth/keenauth-go/claims는 검증된 토큰의 클레임에 타입을 부여합니다. ID 토큰(OpenID Connect Core)에는 claims.ParseIDToken을, 액세스 토큰(RFC 9068)에는 claims.ParseAccessToken을 사용합니다. 이 함수들은 클레임 타입만 확인하고 검증은 하지 않으므로, 패키지 verify가 반환한 Claims 맵에 대해 호출하십시오.
at, err := v.VerifyAccessToken(ctx, token, nil)
if err != nil {
return err
}
c, err := claims.ParseAccessToken(at.Claims)
if err != nil {
return err
}
log.Printf("client %s, org %s, roles %v", c.ClientID, c.OrgID, c.OrgRoles)
_ = c.Custom["tenant_tier"] // your own claims
aud는 항상 슬라이스(claims.Audience)입니다. 계약에 포함되지 않은 클레임은 직접 만든 클레임이며 Custom에 보관됩니다.
오류
API 오류는 RFC 9457 문제 문서입니다. keenauth.AsProblem(err)은 기계가 읽을 수 있는 Code와 함께 이를 반환합니다. 알 수 없는 코드는 그대로 반환되므로, 서버의 새 코드가 클라이언트를 깨뜨리지 않습니다.
if _, _, err := c.UsersAPI.GetUser(ctx, id).Execute(); err != nil {
if pe, ok := keenauth.AsProblem(err); ok && pe.Code == "not_found" {
// …
}
}
지원 중단 경고
응답에 Deprecation(RFC 9745) 또는 Sunset(RFC 8594) 헤더가 있으면, SDK는 프로세스 전체에서 작업과 헤더 값마다 경고를 한 번씩 기록합니다. 이 경고는 WithLogger로 설정한 로거에 WARN 수준으로 기록됩니다. 경고에는 메서드, 식별자를 {id}로 바꾼 경로, 헤더 값, 그리고 rel="deprecation" 또는 rel="sunset"인 Link가 포함됩니다. ID, 토큰, 키는 절대 포함되지 않으며, 호출 자체에는 영향을 주지 않습니다.
level=WARN msg="keenauth: the API operation is deprecated" operation="GET /users/{id}" deprecation=@1767225600 sunset="Wed, 01 Jul 2026 00:00:00 GMT"