
# Go net/http 세션

패키지 `github.com/keenauth/keenauth-go/bff`는 Go `net/http` 애플리케이션에 로그인 기능을 추가합니다. 서버가 OAuth 클라이언트이며 모든 토큰을 서버에 보관합니다. 브라우저는 암호화된 HttpOnly 세션 쿠키만 받으며 액세스 토큰, 갱신 토큰, ID 토큰을 전혀 보지 않습니다. 이것은 BFF(backend-for-frontend) 패턴입니다. 이 패키지는 표준 라이브러리만 사용합니다. 아직 공개되지 않았습니다.

대신 베어러 토큰을 받는 API를 보호하려면 [API 보호](/docs/sdk/protect-api)를 참조하십시오.

## 시작하기 전에

1. 대시보드에서 환경에 웹 애플리케이션을 생성하십시오.
2. 클라이언트 인증 방식을 선택하십시오. 클라이언트 비밀 값(`bff.ClientSecretBasic`) 또는 `private_key_jwt`(`bff.PrivateKeyJWT`)를 사용할 수 있습니다. `private_key_jwt`를 사용하는 경우 애플리케이션의 공개 키를 등록하고 개인 키는 서버에 보관하십시오.
3. 리디렉션 URI `https://app.acme.example/auth/callback`과 로그아웃 후 리디렉션 URI `https://app.acme.example/`를 추가하십시오.
4. 최소 32자 이상의 쿠키 비밀 값을 생성하십시오. 예를 들어 `openssl rand -base64 32`를 사용할 수 있습니다. 저장소가 아닌 비밀 값 관리 도구에 보관하십시오.

## 설정

```go
package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/keenauth/keenauth-go/bff"
)

func main() {
	auth, err := bff.New(bff.Options{
		Issuer:     "https://acme.eu.keenauth.app", // 환경의 issuer를 정확히 입력하십시오
		ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
		ClientAuth: bff.ClientSecretBasic{Secret: os.Getenv("KEENAUTH_CLIENT_SECRET")},
		AppBaseURL: "https://app.acme.example",
		Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
	})
	if err != nil {
		panic(err) // 설정 오류는 시작 시점에 애플리케이션을 중단합니다
	}

	mux := http.NewServeMux()
	mux.Handle("GET /orders", auth.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s := bff.FromContext(r.Context()).Session
		fmt.Fprintf(w, "Signed in as %s", s.User.Email)
	})))

	http.ListenAndServe(":8080", auth.Middleware(mux))
}
```

클라이언트 비밀 값 대신 `private_key_jwt`를 사용하려면 개인 키를 전달하십시오. 패키지는 토큰 및 PAR 요청마다 새 클라이언트 assertion을 서명합니다(`iss`와 `sub`는 클라이언트 ID, `aud`는 issuer, 유효 기간 2분, 매번 새 `jti`):

```go
pemBytes, err := os.ReadFile("/run/secrets/keenauth-app-key.pem")
if err != nil {
	panic(err)
}
key, err := assertion.ParsePrivateKeyPEM(pemBytes) // import "github.com/keenauth/keenauth-go/assertion"
if err != nil {
	panic(err)
}
auth, err := bff.New(bff.Options{
	Issuer:     "https://acme.eu.keenauth.app",
	ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
	ClientAuth: bff.PrivateKeyJWT{Key: key, KeyID: "app-key-2026-11"},
	AppBaseURL: "https://app.acme.example",
	Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
})
```

ES256(P-256), EdDSA(Ed25519), RS256 및 PS256(RSA, 2048비트 이상)을 지원합니다. 알고리즘은 키를 따르며, `Algorithm: "PS256"`을 지정하면 RSA 키에 대해 PSS를 사용합니다. `Key`에는 모든 `crypto.Signer`를 사용할 수 있으므로 KMS나 HSM에 있는 키도 사용할 수 있습니다. 다른 곳에서 서명하는 경우에는 `bff.PrivateKeyJWT{Assertion: f}`를 설정하십시오. 여기서 `f`는 호출될 때마다 새로 서명된 assertion을 반환합니다. `Key`와 `Assertion` 중 하나만 설정하고 둘 다 설정하지 마십시오.

`AppBaseURL`은 애플리케이션의 공개 주소입니다. 패키지는 리디렉션 URI와 모든 리디렉션을 이 값으로부터 생성하며, 요청의 `Host`나 `X-Forwarded-*` 헤더로부터 생성하지 않습니다. 일반 `http`는 `localhost`에서만 허용됩니다.

## 라우트

`Middleware`는 세 개의 라우트를 제공하고 그 밖의 모든 요청에서 세션을 읽습니다. `mux.Handle("/auth/", auth)`로 라우트만 따로 마운트할 수도 있습니다.

| 라우트 | 기능 |
|---|---|
| `GET /auth/login` | 로그인을 시작합니다. 선택 쿼리 매개변수: `returnTo`(사용자가 도착할 위치)와 `organization`(조직으로 로그인). |
| `GET /auth/callback` | 로그인을 완료하고 세션 쿠키를 설정합니다. |
| `GET` 또는 `POST /auth/logout` | KeenAuth에서 로그아웃하고 애플리케이션으로 돌아갑니다. |

로그인에는 PKCE(S256)를 사용하는 인증 코드 흐름이 사용됩니다. 환경에서 지원하는 경우 요청은 서버로 푸시됩니다(PAR). 콜백에서 패키지는 `state`, `iss`, ID 토큰(`nonce` 포함)을 확인합니다. 로그인이 성공할 때마다 새 세션이 생성되고 이전 세션은 폐기됩니다.

`returnTo`는 `/orders?tab=open`과 같이 애플리케이션 자체의 경로여야 합니다. `//evil.example`이나 전체 URL과 같은 다른 값은 무시되며 사용자는 `/`로 이동합니다.

## 핸들러 보호

`auth.RequireAuth(handler)`는 세션이 있는 요청만 통과시킵니다.

- 세션이 없는 페이지 요청(`GET` 또는 `HEAD`)은 `returnTo`와 함께 `/auth/login`으로 리디렉션됩니다.
- `/api/…`에 대한 요청이나 다른 메서드의 요청은 문제 문서와 함께 `401`을 받습니다. 모든 경로에서 `401`로 응답하려면 `bff.GuardOptions{API: true}`를 전달하십시오.

`POST`, `PUT`, `PATCH`, `DELETE` 등 안전하지 않은 메서드의 경우 브라우저는 요청이 애플리케이션에서 왔다고 표시해야 합니다. 즉, `Sec-Fetch-Site: same-origin`이거나 `Origin` 헤더가 애플리케이션의 origin과 같아야 합니다. 그 밖의 요청은 `403`을 받습니다. 이는 `SameSite=Lax`에 더해 사이트 간 요청 위조를 방지합니다.

## 세션 사용 및 API 호출

`bff.FromContext(r.Context())`는 다음을 반환합니다.

- `Session`: `nil`이거나 로그인한 사용자입니다. `Session.User`에는 `Subject`, `Email`, `Name`, `OrgID`, `OrgRoles`와 같은 형식이 지정된 ID 토큰 클레임이 있으며, 모든 클레임은 `Session.User.Claims`에 있습니다. 세션에는 토큰이 전혀 포함되지 않습니다.
- `AccessToken`: 서버 측 호출에 사용할 수 있는 유효한 액세스 토큰이거나 `""`입니다. 이를 브라우저로 보내지 마십시오.

액세스 토큰이 60초 이내에 만료되면 패키지는 핸들러가 실행되기 전에 이를 갱신합니다. 갱신할 때마다 갱신 토큰이 교체되고 응답에 새 세션 쿠키가 설정됩니다. 서버가 갱신 토큰을 거부하면 세션이 종료됩니다. 일시적인 서버 오류의 경우 세션은 변경되지 않습니다. 한 프로세스 안의 동시 요청은 하나의 갱신을 공유합니다.

## 조직 전환

`auth.SwitchOrganizationPath("org_01j9…", "/dashboard")`는 사용자가 해당 조직으로 다시 로그인하도록 하는 `/auth/login` 링크를 반환합니다. KeenAuth가 멤버십을 확인합니다. 로그인 후 `Session.User.OrgID`에 조직이 담깁니다.

## 로그아웃

양식에서 `/auth/logout`으로 `POST` 요청을 보내십시오. 패키지는 세션 쿠키를 삭제하고(저장소를 사용하는 경우 저장된 세션도 삭제), 갱신 토큰을 폐기한 뒤, 브라우저를 환경의 로그아웃 엔드포인트로 보내 애플리케이션으로 돌아오게 합니다. 브라우저가 다른 사이트에서 온 요청으로 표시한 요청은 거부됩니다. `Sec-Fetch-Site`를 보내지 않는 브라우저는 자체 페이지에서 보낸 `POST`로만 로그아웃할 수 있습니다.

## 세션 저장소

기본적으로 전체 세션은 쿠키에 있으며, 비밀 값에서 파생된 키를 사용해 AES-256-GCM으로 암호화되고 인증됩니다. 변경되거나 복사된 쿠키는 거부됩니다. 쿠키 이름은 `__Host-keenauth_session.0`이며 `Secure`, `HttpOnly`, `SameSite=Lax`, `Path=/`로 설정됩니다. `http://localhost`에서는 `__Host-` 접두사와 `Secure`가 제외됩니다. 큰 세션은 최대 세 개의 쿠키로 나뉩니다.

토큰을 서버에 보관하려면 `Options.Store`를 `bff.SessionStore`(`Get`, `Set`, `Delete`)로 설정하십시오. 예를 들어 Redis를 기반으로 할 수 있습니다. 이 경우 쿠키에는 암호화된 무작위 세션 ID만 담깁니다. 로그인할 때마다 새 ID가 생성되고 이전 레코드는 삭제됩니다. 저장소에는 토큰이 담기므로 접근을 제한하십시오. `bff.MemoryStore`는 개발 및 테스트 용도로만 사용하십시오.

세션은 최대 7일(`SessionMaxAge`) 동안 유지되며, 갱신해도 이 기간은 연장되지 않습니다. 비밀 값을 변경하려면 새 값을 먼저 나열하십시오: `Secrets: []string{newSecret, oldSecret}`. `SessionMaxAge`가 지난 후 이전 값을 제거하십시오.

## 현재 제한 사항

- DPoP에 바인딩된 토큰과 백채널 로그아웃은 아직 처리되지 않습니다.
- 갱신은 하나의 서버 프로세스 안에서만 공유됩니다. 여러 인스턴스를 사용하는 경우 애플리케이션 설정 `refresh_reuse_interval_s`(기본값 5초)를 사용하면 병렬 갱신이 성공할 수 있습니다. 이 값을 `0`으로 설정하면 두 번째 갱신이 세션을 종료합니다.
