Django API 보호하기

이 빠른 시작 가이드에서는 KeenAuth 액세스 토큰으로 Django API를 보호합니다. Python SDK의 @protect 뷰 데코레이터를 추가하고, 한 뷰에 스코프를 요구하며, SDK 테스트 키트로 API를 오프라인에서 테스트합니다. 예제는 Django 5.2를 사용하며 데이터베이스는 사용하지 않습니다.

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

엔드포인트 요구 사항
GET /api/public 없음
GET /api/private API에 대한 유효한 액세스 토큰
GET /api/messages messages:read 스코프가 포함된 유효한 액세스 토큰

시작하기 전에

1. SDK 설치

pip install "keenauth-python[django]"

SDK는 아직 공개되지 않았습니다. 공개 전까지 예제는 SDK 소스에서 설치합니다. 예제에서는 requirements.txt에서 모든 패키지를 정확한 버전으로 고정합니다.

2. 구성

project/settings.py는 모든 값을 환경 변수에서 읽습니다:

변수 필수 여부 의미
DJANGO_SECRET_KEY 예 Django의 시크릿 키입니다. 하나 생성해 두고 절대 커밋하지 마세요
DJANGO_ALLOWED_HOSTS 아니요 쉼표로 구분된 호스트 이름입니다. 기본값은 localhost,127.0.0.1
KEENAUTH_ISSUER 예 환경의 발급자입니다
KEENAUTH_AUDIENCE 예 API의 식별자입니다
PUBLIC_BASE_URL 예 클라이언트가 호출하는 URL입니다(프록시 뒤에서 참조)
KEENAUTH_WEBHOOK_SECRET 권장 token.revoked를 전달하는 웹훅 엔드포인트의 idp_whsec_ 서명 시크릿입니다(교체하는 동안에는 쉼표로 구분해 두 개를 입력합니다). 이 값이 없으면 API가 폐기된 토큰을 알 수 없습니다

3. 뷰 보호하기

@protect(protector)는 유효한 토큰을 요구하고 검증된 클레임을 request.auth에 넣습니다. scopes=[...]는 스코프도 요구합니다. 검사에 실패하면 뷰가 실행되기 전에 application/problem+json 응답을 반환합니다.

@require_GET
def public(request: HttpRequest) -> JsonResponse:
    # No token needed.
    return JsonResponse({"message": "Hello from a public endpoint."})


@require_GET
@protect(protector)
def private(request: HttpRequest) -> JsonResponse:
    # Any valid access token for this API; the verified claims are in request.auth.
    return JsonResponse({"message": "Hello from a private endpoint.", "sub": request.auth.subject})


@require_GET
@protect(protector, scopes=["messages:read"])
def messages(request: HttpRequest) -> JsonResponse:
    # A valid access token that also carries the messages:read scope.
    return JsonResponse({"messages": ["Hello from a scoped endpoint."]})

api/auth.py는 시작할 때 설정에서 protector를 만듭니다(api/apps.py의 ApiConfig.ready). 따라서 설정이 누락되면 첫 요청이 오기 전에 서버가 중지됩니다. verifier는 토큰을 처음 확인할 때 환경에서 서명 키를 가져옵니다.

@cache
def get_protector() -> Protector:
    if not settings.KEENAUTH_ISSUER or not settings.KEENAUTH_AUDIENCE or not settings.KEENAUTH_PUBLIC_BASE_URL:
        raise ImproperlyConfigured("KEENAUTH_ISSUER, KEENAUTH_AUDIENCE and PUBLIC_BASE_URL are required")
    # The verifier refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
    verifier = Verifier(VerifierConfig(issuer=settings.KEENAUTH_ISSUER, audience=settings.KEENAUTH_AUDIENCE))
    return Protector(
        verifier,
        public_base_url=settings.KEENAUTH_PUBLIC_BASE_URL,
        revocation=revocation,
        # The error never contains the token or claim values; never log the Authorization or DPoP header.
        on_error=lambda err: log.warning("rejected: %s", err),
    )


@cache
def get_webhook() -> Optional[RevocationWebhook]:
    """The token.revoked webhook, or None when KEENAUTH_WEBHOOK_SECRET is not set. A malformed secret raises
    WebhookSecretError (without echoing it), so ApiConfig.ready stops the server at startup."""
    if not settings.KEENAUTH_WEBHOOK_SECRETS:
        return None
    return RevocationWebhook(settings.KEENAUTH_WEBHOOK_SECRETS, revocation)


class ProtectorProxy:
    """Hands each request to `get_protector()`, so the `@protect` decorators can be applied at import time."""

    def authenticate(self, req: RequestInfo, requirement: Optional[Requirement] = None) -> AccessClaims:
        return get_protector().authenticate(req, requirement)


protector = ProtectorProxy()

4. API 실행 및 호출

export DJANGO_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(50))')"
export KEENAUTH_ISSUER=https://your-environment.eu.keenauth.app
export KEENAUTH_AUDIENCE=https://api.example.com
export PUBLIC_BASE_URL=https://api.example.com
python manage.py runserver 8080

runserver는 개발 전용입니다. 프로덕션에서는 WSGI 서버에서 project.wsgi를 실행하세요.

첫 호출을 하려면 API에 messages:read 스코프가 있는 머신 간(M2M) 애플리케이션의 클라이언트 자격 증명 그랜트로 토큰을 받으세요:

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. 네트워크 없이 테스트

keenauth.testing는 공개 테스트 키로 서명한 토큰을 발급하며, testing.verifier는 이런 토큰만 수락합니다. 테스트에서는 get_protector를 이 verifier를 사용하는 protector로 대체하므로 네트워크, KeenAuth 계정, 설정의 테스트 전환 스위치가 필요하지 않습니다. 자세한 내용은 오프라인 테스트 키트를 참조하세요.

class ProtectedAPITest(SimpleTestCase):
    def setUp(self) -> None:
        self.cache = MemoryRevocationCache()
        test_protector = Protector(testing.verifier(audience=AUDIENCE), public_base_url=BASE_URL, revocation=self.cache)
        patcher = mock.patch.object(auth, "get_protector", return_value=test_protector)
        patcher.start()
        self.addCleanup(patcher.stop)
    def test_scope(self) -> None:
        self.assert_problem(self.get("/api/messages", testing.mint_access_token(AUDIENCE)), 403, "insufficient_scope",
                            'scope="messages:read"')
        tok = testing.mint_access_token(AUDIENCE, claims={"scope": "profile messages:read"})
        self.assertEqual(self.get("/api/messages", tok).status_code, 200)
set -a; . ./.env.example; set +a
DJANGO_SECRET_KEY=insecure-test-only python manage.py test

예제의 테스트는 유효한 토큰이 수락되는지 확인합니다. 또한 누락되었거나 만료되었거나 대상이 잘못된 토큰, 누락된 스코프, URL 쿼리에 포함된 토큰, 폐기된 토큰이 아래 상태 코드로 거부되는지 확인합니다. 또한 PUBLIC_BASE_URL에 대한 증명(proof)이 포함된 DPoP 바인딩 토큰을 보내고, 프로덕션 protector가 테스트 발급자를 거부하는지도 확인합니다. 테스트는 서명된 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 헤더에는 표준 챌린지가 담깁니다. Django는 여러 챌린지를 하나의 헤더로 합칩니다. 전체 표는 미들웨어로 API 보호하기에 있습니다.

프록시 뒤에서

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

폐기된 토큰

액세스 토큰은 만료될 때까지 유효하므로, KeenAuth는 토큰이 폐기되면 API에 알려줍니다. 예제는 인메모리 폐기 캐시를 protector에 전달하고, 이 캐시에 값을 기록하는 token.revoked 웹훅을 POST /webhooks/keenauth(api/webhook.py)에서 제공합니다.

  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, 셀렉터 유형, 사유를 기록하며, 본문, 서명, 시크릿은 기록하지 않습니다. 각 워커 프로세스에는 고유한 캐시가 있습니다. 워커나 인스턴스가 여러 개라면 대신 공유 저장소를 사용하세요. 자세한 내용은 폐기 캐시를 참조하세요.

다음 단계