OAuth2로 보안 첫걸음 떼기

OAuth2로 보안 첫걸음 떼기

백엔드 API와 프론트엔드가 분리되어 있는 구조에서, 프론트가 사용자 이름과 비밀번호로 백엔드에 인증을 요청하고 싶을 때가 있어요. OAuth2 스펙 전체를 처음부터 읽지 않아도, FastAPI가 미리 준비해 둔 도구를 쓰면 아주 적은 코드로 보안의 뼈대를 만들 수 있어요. 이번 글에서는 그 첫걸음으로 OAuth2PasswordBearer라는 도구를 직접 써 보면서 흐름을 이해해 볼게요.

출처: FastAPI 공식 문서 - Security - First Steps

코드부터 보고 이해하기

당장 모든 원리를 외우려 하지 말고, 먼저 동작하는 코드를 살펴봐요. main.py 파일에 아래 예시를 그대로 넣고 실행해 봐요.

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}

실행하려면 python-multipart 패키지가 필요해요. OAuth2가 usernamepassword폼 데이터로 보내기 때문인데요, uv add "fastapi[standard]"로 FastAPI를 설치했다면 함께 들어오지만, uv add fastapi만 했다면 따로 설치해야 해요.

$ uv add python-multipart

실행한 뒤 http://127.0.0.1:8000/docs에 가면, 이전에는 보이지 않던 Authorize 버튼이 새로 생긴 걸 볼 수 있어요. 그리고 path operation 오른쪽 위에는 자물쇠 아이콘이 생기죠. 그 버튼을 누르면 usernamepassword(그리고 선택 필드)를 입력하는 인증 폼이 나타나요. 지금은 아무 값을 넣어도 동작하지 않지만, 곧 채워질 거예요.

password 플로우가 뭔가요

password 플로우는 OAuth2가 정의한 여러 인증 방식(플로우) 중 하나예요. OAuth2는 원래 백엔드/API와 사용자를 인증하는 서버가 분리될 수 있다는 전제로 설계되었는데요, 여기서는 같은 FastAPI 애플리케이션이 API와 인증을 함께 처리한다고 단순화해서 보겠어요.

  • 사용자가 프론트엔드에 usernamepassword를 입력하고 Enter를 눌러요.
  • 프론트엔드(브라우저에서 동작)는 그 값을 tokenUrl="token"으로 선언한 API의 특정 URL로 보내요.
  • API는 그 usernamepassword를 확인하고 토큰을 응답으로 돌려줘요(아직 여기서는 구현하지 않았지만요). 토큰은 나중에 이 사용자를 검증할 때 쓸 수 있는 내용물이 담긴 문자열일 뿐이에요.
  • 프론트엔드는 그 토큰을 어딘가에 임시로 저장해 두고, API에서 데이터를 더 가져와야 할 때 Authorization 헤더에 Bearer 뒤에 토큰을 붙여 보내요. 토큰이 foobar라면 헤더 값은 Bearer foobar가 되는 식이에요.

FastAPI의 OAuth2PasswordBearer

FastAPI는 이런 보안 기능을 다양한 추상화 수준으로 제공하는데요, 이 예시에서는 Password 플로우 + Bearer 토큰을 쓰기 위해 OAuth2PasswordBearer 클래스를 이용해요. 이 클래스로 인스턴스를 만들 때 tokenUrl 파라미터를 넘기는데, 이 값은 클라이언트(브라우저의 프론트엔드)가 토큰을 얻기 위해 usernamepassword를 보낼 URL을 가리켜요.

여기서 tokenUrl="token"은 아직 만들지 않은 상대 URL token을 가리켜요. 상대 URL이므로 API가 https://example.com/에 있다면 https://example.com/token을, https://example.com/api/v1/에 있다면 https://example.com/api/v1/token을 가리키게 돼요. 상대 URL을 쓰는 건 Behind a Proxy 같은 고급 상황에서도 애플리케이션이 계속 동작하게 하기 위함이에요.

이 파라미터는 그 /token 엔드포인트(path operation)를 실제로 만들지는 않아요. 다만 클라이언트가 토큰을 얻기 위해 그 URL을 써야 한다는 사실을 선언할 뿐이죠. 그 정보는 OpenAPI에 기록되고, 인터랙티브 API 문서에도 반영돼요. 실제 path operation은 곧 만들 거예요.

oauth2_scheme 변수는 OAuth2PasswordBearer의 인스턴스이면서 호출 가능(callable) 한 객체예요. 그래서 Depends와 함께 쓸 수 있죠.

실제로 사용해 보기

그럼 이제 이 oauth2_schemeDepends로 의존성에 넘겨서 path operation의 token 파라미터에 주입받아요. FastAPI는 이 의존성을 보고 OpenAPI 스키마(그리고 자동 API 문서)에 보안 스킴을 정의할 수 있음을 알아채요.

기술적으로 FastAPI가 이걸 알아차리는 이유는, OAuth2PasswordBearerfastapi.security.oauth2.OAuth2를, 다시 그게 fastapi.security.base.SecurityBase를 상속받기 때문이에요. OpenAPI에 통합되는 모든 보안 유틸리티가 SecurityBase를 상속받고, 그 덕분에 FastAPI가 어떻게 OpenAPI에 통합할지 알 수 있어요.

무슨 일이 벌어지나요

FastAPI는 요청에서 Authorization 헤더를 찾아, 값이 Bearer 뒤에 어떤 토큰이 붙은 형태인지 확인하고 그 토큰을 str로 돌려줘요. 헤더가 없거나 Bearer 토큰 형태가 아니면 바로 401 상태 코드(UNAUTHORIZED)로 응답해요. 토큰이 존재하는지 직접 검사해 오류를 낼 필요조차 없어요. 함수가 실행됐다는 건 그 token 파라미터에 str이 들어 있다는 뜻이니까요.

아직 토큰의 유효성은 검증하지 않지만, 그건 다음 단계에서 다뤄요.

정리

불과 3~4줄을 추가했을 뿐인데, 이미 어느 정도 동작하는 보안의 기본 형태가 갖춰졌어요.

더 알아보기 (Learn more)