
# FastAPI 세션

`keenauth.bff.fastapi`는 FastAPI 애플리케이션에 로그인 기능을 추가합니다. 서버가 OAuth 클라이언트 역할을 하며 모든 토큰을 서버에 보관합니다. 브라우저에는 암호화되고 HttpOnly 속성이 설정된 세션 쿠키만 전달되며, 액세스 토큰, 리프레시 토큰, ID 토큰은 브라우저에 전달되지 않습니다. 이는 BFF(Backend-for-Frontend) 패턴입니다. 이 기능은 아직 PyPI에 게시되지 않은 `keenauth-python`의 일부입니다.

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

## 설치

```sh
pip install "keenauth-python[fastapi]"
```

## 시작하기 전에

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

## 인스턴스 생성

```python
import os
from keenauth.bff import BffOptions, ClientSecretBasic, KeenAuthBff

bff = KeenAuthBff(BffOptions(
    issuer="https://acme.eu.keenauth.app",  # your environment's issuer, exactly
    client_id="app_01j9zzzzzzzzzzzzzzzzzzzz",
    client_auth=ClientSecretBasic(os.environ["KEENAUTH_CLIENT_SECRET"]),
    app_base_url="https://app.acme.example",
    secret=os.environ["KEENAUTH_COOKIE_SECRET"],
))
```

`app_base_url`은 애플리케이션의 공개 주소입니다. SDK는 리디렉션 URI와 모든 리디렉션을 이 값으로부터 생성하며, 요청의 `Host` 또는 `X-Forwarded-*` 헤더에서는 가져오지 않습니다. 일반 `http`는 `localhost`에서만 허용됩니다. 옵션은 `KeenAuthBff`를 생성할 때 검사되므로, 설정 오류가 있으면 시작 단계에서 애플리케이션이 중지됩니다.

## private_key_jwt 사용

`private_key_jwt`를 사용하면 애플리케이션에 공유 시크릿이 없습니다. `PrivateKeyJwt`에 개인 키를 전달하면, SDK가 토큰 엔드포인트에 대한 요청마다 새로운 클라이언트 어설션(RFC 7523)을 서명합니다.

```python
from keenauth.bff import PrivateKeyJwt

client_auth = PrivateKeyJwt(key=os.environ["KEENAUTH_PRIVATE_KEY"])  # unencrypted PEM
```

- 지원되는 키: EC P-256(`ES256`), EC P-384(`ES384`), Ed25519(`EdDSA`), 그리고 2048비트 이상의 RSA(기본값 `PS256`; `RS256`을 사용하려면 `algorithm="RS256"`을 전달하십시오).
- 각 어설션의 `iss`와 `sub`는 클라이언트 ID로, `aud`는 발급자로 설정되며, 무작위 `jti`와 60초의 유효 기간을 갖습니다. `kid`는 공개 키의 RFC 7638 썸프린트입니다.
- 공개 키를 애플리케이션에 등록하십시오. `keenauth.m2m.load_private_key(pem).public_jwk()`는 이를 JWK 형식으로 반환합니다.

자체 키 서비스(KMS 또는 HSM)로 서명하려면 함수를 대신 전달하십시오. `PrivateKeyJwt(assertion=sign)`처럼 설정합니다. `sign()`은 호출될 때마다 새로 서명된 어설션을 반환해야 합니다.

## 미들웨어 설치

```python
from fastapi import Depends, FastAPI
from keenauth.bff import SessionContext
from keenauth.bff.fastapi import RequireSession, install

app = FastAPI()
install(app, bff)

@app.get("/orders")
def orders(ctx: SessionContext = Depends(RequireSession(bff))):
    return {"email": ctx.session.user.email}
```

`install`은 아래의 라우트를 제공하고, 그 밖의 모든 요청에서 세션을 읽어 `request.state.keenauth`에 저장합니다. SDK의 네트워크 호출은 동기식이므로, `install`은 이를 Starlette의 스레드 풀에서 실행하여 이벤트 루프를 차단하지 않도록 합니다.

## 라우트

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

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

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

## 보호된 뷰

경로 작업에 `Depends(RequireSession(bff))`를 추가하십시오. 이 의존성은 세션 컨텍스트를 반환합니다.

세션이 없는 경우:

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

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

## 세션 사용 및 API 호출

세션 컨텍스트에는 다음이 포함됩니다.

- `session`: `None`이거나 로그인한 사용자입니다. `session.user`에는 `subject`, `email`, `name`, `org_id`, `org_roles` 같은 타입이 지정된 ID 토큰 클레임이 있으며, 모든 클레임은 `session.user.claims`에 있습니다. 세션 객체에는 토큰이 절대 포함되지 않습니다.
- `access_token`: 서버 측 호출에 사용할 유효한 액세스 토큰이며, 없으면 `None`입니다. 이를 브라우저로 보내지 마십시오.

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

## 조직 전환

`bff.switch_organization_path("org_01j9…", "/dashboard")`는 `/auth/login`으로 연결되는 링크를 반환하며, 이 링크는 사용자가 해당 조직으로 다시 로그인하게 합니다. KeenAuth가 멤버십을 확인합니다. 로그인 후 `session.user.org_id`에 해당 조직이 저장됩니다.

## 로그아웃

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

## 세션 저장소

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

토큰을 서버에 보관하려면 `get(id)`, `set(id, session, ttl_seconds)`, `delete(id)` 메서드를 가진 객체를 `store=`로 전달하십시오. 예를 들어 Redis를 기반으로 구현할 수 있습니다. 이 경우 쿠키에는 암호화된 무작위 세션 ID만 저장됩니다. 로그인할 때마다 새 ID가 생성되고 이전 레코드는 삭제됩니다. 저장소에는 토큰이 포함되므로 접근을 제한하십시오. `MemorySessionStore`는 개발과 테스트 용도로만 사용하십시오.

세션은 최대 7일(`session_max_age`) 동안 유지되며, 갱신해도 연장되지 않습니다. 시크릿을 변경하려면 목록을 전달하십시오: `secret=[new_secret, old_secret]`. `session_max_age`가 지난 후 이전 시크릿을 제거하십시오.

## 현재 제한 사항

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