
# Go API 보호

이 퀵스타트는 KeenAuth 액세스 토큰으로 Go `net/http` API를 보호합니다. Go SDK의 `protect` 미들웨어를 추가하고, 한 경로에 스코프를 요구하며, SDK 테스트 키트로 API를 오프라인에서 테스트합니다. 이 미들웨어는 표준 라이브러리만 사용하며 `http.Handler`를 받는 모든 라우터와 함께 동작합니다.

이 API에는 엔드포인트가 세 개 있습니다:

| 엔드포인트 | 필요 조건 |
|---|---|
| `GET /api/public` | 없음 |
| `GET /api/private` | API용 유효한 액세스 토큰 |
| `GET /api/messages` | `messages:read` 스코프가 포함된 유효한 액세스 토큰 |

## 시작하기 전에

- KeenAuth 환경. 발급자(issuer)는 `https://your-environment.eu.keenauth.app` 형태입니다.
- 환경에 등록된 API([API 만들기](/docs/api/management/apis/create)). API의 `identifier`(예: `https://api.example.com`)은 API가 허용하는 토큰의 대상(audience)입니다.
- 호출하는 애플리케이션에 부여된, 해당 API의 `messages:read` 스코프.
- Go 1.26 이상.

## 1. SDK 설치

```sh
go get github.com/keenauth/keenauth-go
```

SDK는 아직 배포되지 않았습니다. 배포되기 전까지 예제는 `go.mod`의 `replace` 지시문을 사용해 SDK 소스로 빌드됩니다.

## 2. 설정

API는 환경 변수에서만 설정을 읽습니다:

| 변수 | 필수 여부 | 의미 |
|---|---|---|
| `KEENAUTH_ISSUER` | 예 | 환경의 발급자 |
| `KEENAUTH_AUDIENCE` | 예 | API의 식별자 |
| `PUBLIC_BASE_URL` | 예 | 클라이언트가 호출하는 URL([프록시 뒤에서](#프록시-뒤에서) 참조) |
| `KEENAUTH_WEBHOOK_SECRET` | 권장 | `token.revoked`를 전달하는 웹훅 엔드포인트의 `idp_whsec_` 서명 시크릿(교체하는 동안에는 쉼표로 구분된 두 개). 이 값이 없으면 API는 폐기된 토큰을 알 수 없습니다 |
| `ADDR` | 아니요 | 수신 주소, 기본값 `:8080` |

## 3. 라우트 보호하기

`NewHandler`는 미들웨어와 라우트를 만듭니다. `m.Handler`는 유효한 토큰을 요구하고, `m.Require`는 스코프도 요구합니다. 핸들러는 `protect.ClaimsFrom`로 검증된 클레임을 읽습니다.

```go
// NewHandler builds the API. The verifier is a parameter so that tests can pass the offline test kit's verifier;
// main passes a real one. The revocation cache is fed from the token.revoked webhook.
func NewHandler(v protect.AccessTokenVerifier, publicBaseURL string, revocation protect.RevocationChecker) (http.Handler, error) {
	m, err := protect.New(v, protect.Options{
		PublicBaseURL: publicBaseURL,
		Revocation:    revocation,
		OnError: func(r *http.Request, err error) {
			// The error never contains the token or claim values; never log the Authorization or DPoP header.
			log.Printf("rejected %s %s: %v", r.Method, r.URL.Path, err)
		},
	})
	if err != nil {
		return nil, err
	}

	mux := http.NewServeMux()
	// No token needed.
	mux.HandleFunc("GET /api/public", func(w http.ResponseWriter, _ *http.Request) {
		writeJSON(w, map[string]string{"message": "Hello from a public endpoint."})
	})
	// Any valid access token for this API.
	mux.Handle("GET /api/private", m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		claims, _ := protect.ClaimsFrom(r.Context())
		writeJSON(w, map[string]string{"message": "Hello from a private endpoint.", "sub": claims.Subject})
	})))
	// A valid access token that also carries the messages:read scope.
	mux.Handle("GET /api/messages", m.Require(protect.Requirement{Scopes: []string{"messages:read"}})(
		http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			writeJSON(w, map[string]any{"messages": []string{"Hello from a scoped endpoint."}})
		})))
	return mux, nil
}
```

`main`는 설정으로부터 실제 검증기를 만듭니다. 검증기는 처음 토큰을 보면 환경에서 서명 키를 가져옵니다.

```go
func main() {
	cfg, err := ConfigFromEnv(os.Getenv)
	if err != nil {
		log.Fatal(err)
	}
	// verify.New refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
	v, err := verify.New(verify.Config{Issuer: cfg.Issuer, Audience: cfg.Audience})
	if err != nil {
		log.Fatal(err)
	}
	revocation := protect.NewMemoryRevocationCache(0, nil)
	api, err := NewHandler(v, cfg.PublicBaseURL, revocation)
	if err != nil {
		log.Fatal(err)
	}
	h := api
	if len(cfg.WebhookSecrets) > 0 {
		wv, err := webhook.NewVerifier(cfg.WebhookSecrets) // the error never contains the secret
		if err != nil {
			log.Fatalf("KEENAUTH_WEBHOOK_SECRET: %v", err)
		}
		h = WithWebhook(api, NewWebhookHandler(wv, revocation, nil))
	} else {
		log.Print("KEENAUTH_WEBHOOK_SECRET is not set: token.revoked events are not received, so revoked tokens stay valid until they expire")
	}
	srv := &http.Server{Addr: cfg.Addr, Handler: h, ReadHeaderTimeout: 10 * time.Second}
	log.Printf("listening on %s", cfg.Addr)
	log.Fatal(srv.ListenAndServe())
}
```

## 4. API 실행 및 호출

```sh
export KEENAUTH_ISSUER=https://your-environment.eu.keenauth.app
export KEENAUTH_AUDIENCE=https://api.example.com
export PUBLIC_BASE_URL=https://api.example.com
go run .
```

첫 호출을 위해, API에 `messages:read` 스코프를 가진 머신 간 애플리케이션의 클라이언트 자격 증명 부여(client credentials grant)로 토큰을 받으세요:

```sh
ACCESS_TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d resource=https://api.example.com \
  -d scope=messages:read \
  https://your-environment.eu.keenauth.app/oauth2/token | jq -r .access_token)

curl -i http://localhost:8080/api/messages -H "Authorization: Bearer $ACCESS_TOKEN"
```

## 5. 네트워크 없이 테스트

테스트 키트 패키지 `keenauthtest`는 공개 테스트 키로 서명된 토큰을 발급하고, 그 토큰만 허용하는 검증기를 만듭니다. 테스트는 이 검증기를 `NewHandler`에 전달하므로 네트워크나 KeenAuth 계정이 필요하지 않습니다. [오프라인 테스트 키트](/docs/sdk/testing)를 참조하세요.

```go
func newTestAPI(t *testing.T) (http.Handler, *protect.MemoryRevocationCache) {
	t.Helper()
	v, err := keenauthtest.NewVerifier(verify.Config{Audience: audience})
	if err != nil {
		t.Fatal(err)
	}
	cache := protect.NewMemoryRevocationCache(0, nil)
	h, err := NewHandler(v, baseURL, cache)
	if err != nil {
		t.Fatal(err)
	}
	return h, cache
}
```

```sh
go test ./...
```

예제 테스트는 유효한 토큰이 허용되는지, 그리고 토큰이 없거나 만료되었거나 대상(audience)이 다른 토큰, 스코프가 없는 토큰, URL 쿼리에 담긴 토큰, 폐기된 토큰이 아래 상태 코드로 거부되는지 확인합니다. 또한 `PUBLIC_BASE_URL`에 대한 증명(proof)과 함께 DPoP에 바인딩된 토큰도 보냅니다. 테스트는 서명된 `token.revoked` 전달도 보냅니다. 유효한 전달은 토큰을 폐기하고, 위조되었거나 변조되었거나 오래된 전달은 `400`를 받고 아무것도 폐기하지 않으며, 반복된 전달은 한 번만 효과가 있습니다.

## 응답

| 요청 | 상태 | 본문의 `code` |
|---|---|---|
| `Authorization` 헤더가 없음 | `401` | `missing_token` |
| 만료된 토큰, 다른 API용 토큰, 잘못된 서명, 폐기된 토큰 | `401` | `invalid_token` |
| 필요한 스코프가 없는 토큰 | `403` | `insufficient_scope` |
| URL 쿼리에 있는 토큰(`?access_token=`) | `400` | `invalid_request` |

본문은 `application/problem+json`이며 `WWW-Authenticate` 헤더에 표준 챌린지가 담깁니다. 전체 표는 [미들웨어로 API 보호하기](/docs/sdk/protect-api#응답)에 있습니다.

## 프록시 뒤에서

클라이언트가 DPoP를 사용하면 미들웨어는 각 증명이 클라이언트가 호출한 URL에 대해 만들어졌는지 확인합니다. `PUBLIC_BASE_URL`를 클라이언트가 호출하는 외부 URL로 설정하세요(예: `https://api.example.com`). 프록시나 로드 밸런서 뒤에서도 마찬가지입니다. 이 값은 필수이며 `https`을 사용해야 합니다(`http`는 `localhost`, `127.0.0.1`, `[::1]`에만 허용). 이 값이 없으면 앱은 시작을 거부합니다. 미들웨어는 `X-Forwarded-*` 헤더를 절대 신뢰하지 않습니다.

## 폐기된 토큰

액세스 토큰은 만료될 때까지 유효하므로, KeenAuth는 토큰이 폐기되었을 때 API에 알려 줍니다. 예제는 인메모리 폐기 캐시를 미들웨어에 전달하고, 이 캐시에 데이터를 공급하는 `token.revoked` 웹훅을 `POST /webhooks/keenauth`(`main.go, webhook.go`)에서 제공합니다.

1. 대시보드에서 URL `https://<your host>/webhooks/keenauth`로 `token.revoked`을 구독하는 웹훅 엔드포인트를 만들고, 서명 시크릿을 `KEENAUTH_WEBHOOK_SECRET`로 내보내세요.
2. 핸들러는 원본 본문에 대해 SDK로 서명을 검증하며, 서명이 잘못되었거나 타임스탬프가 5분 이상 차이 나면 `400`으로 응답합니다. 이벤트의 셀렉터(`access_token`, `session`, `user`, `client`, `org`, `org_user`, `client_user`)를 폐기 마커로 매핑하고 `204`로 응답합니다. 그러면 미들웨어는 `not_before` 이전에 발급된 일치하는 모든 토큰을 거부합니다.
3. KeenAuth는 최소 한 번 전달하므로, 핸들러는 각 `webhook-id`을 한 번만 적용합니다. 마커를 저장할 수 없으면 `503`로 응답하고 KeenAuth는 이벤트를 다시 전달합니다. 다른 이벤트 유형과 어떤 액세스 토큰과도 일치할 수 없는 셀렉터(`refresh_family`, `api`)는 무시합니다.

핸들러는 이벤트 ID, 셀렉터 유형, 사유를 기록하며, 본문, 서명, 시크릿은 절대 기록하지 않습니다. 각 프로세스는 자체 캐시를 가집니다. 여러 인스턴스를 운영한다면 같은 `Revoke` 메서드 뒤의 공유 저장소에 마커를 저장하세요. [폐기 캐시](/docs/sdk/protect-api#폐기-캐시tokenrevoked)를 참조하세요.

## 다음 단계

- [미들웨어로 API 보호하기](/docs/sdk/protect-api): 단계별 인증 요구 사항, DPoP, 모든 옵션.
- [토큰 검증](/docs/sdk/token-verification): 미들웨어 아래에서 동작하는 검증기.
- [오프라인 테스트 키트](/docs/sdk/testing).
