본문 바로가기

FastAPI

인증

웹 서비스들의 가장 대표적인 기능 중 하나인 인증입니다. 정말 다양한 웹 인증 방식이 존재하고 이름도 비슷해 보이입니다. OAuth, OAuth2OpenID, OpenID Connect 등 똑같이 생겼지만 사실 다른 방식들입니다.

 

인증 방법은 지금도 바뀌고 있습니다. 간단하게 철저히 서버 사이드 개발자 입장에서 그 역사를 간단히 훑겠습니다. 읽지 않고 다음으로 넘어가셔도 괜찮습니다

 

초창기에는 Form에 아이디와 비밀번호를 입력 받아 서버에 전송을 했습니다. 당연하게도 패킷을 살펴보면 그 내용이 전부 보였고 심각한 보안 이슈였습니다. 물론, SSL(TSL)을 추가한 HTTPS가 퍼져 지금은 그렇지 않습니다.

SSL은 패킷을 암호화 하기 때문에 공격자는 내용을 볼 수 없습니다. 그러니 HTTPS를 사용하지 않는 사이트에서 비밀번호를 입력하라 했을때는 "보안이 정말 허술하네"라고 말하고 절대로 사용하지 마시기 바랍니다. 요즘은 이러한 경우 현대 브라우저들은 경고를 표시합니다.

다음에는 서버에서 세션을 이용해 보안을 한층 높였습니다. 서버측에서 사용자의 패턴 등을 파악하는 등, 강제로 로그아웃 시킬 수 있게 되었습니다. 하지만 사용자가 많아지면 서버에 부담이 되었죠.

그래서 토큰 방식이 도입 되었습니다. 인증 정보는 클라이언트가 가지고 있고, 서버는 매 요청마다 유효한 토큰인지 검사해서 사용자 인증을 했습니다. 하지만 토큰 탈취의 가능성이 있습니다. 이를 해결하고자 토큰의 만료 기간이 생겼고, 현재 가장 많이 쓰는 토큰 방식은 JSON Web Token, JWT입니다.

이에 더해 액세스 토큰과 리프레시 토큰, 사용자 경험을 위한 슬라이딩 세션 등 수많은 기술들이 생겼습니다. 요즘에는 ID 공급자(IdP: Identity Provider)에게 인증을 위임하기도 합니다. 구글, 페이스북 로그인 등이 대표적인 예입니다.

보안 중에서도 한 분야임에도 알아야 할 부분이 정말 많죠! 현재 가장 많이 발생하는 보안 문제에 관심이 있다면 OWASP 사이트를 방문해보길 바랍니다.

 

일단, 가장 간단하고 오래된 웹 인증을 먼저 보겠습니다.

 

HTTP를 기반으로한 main.py를 작성해 보겠습니다.

import uvicorn
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials


app = FastAPI()
security = HTTPBasic()


@app.get("/users/me")
def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):
    return {"username": credentials.username, "password": credentials.password}

if __name__ == '__main__':
	uvicorn.run("main:app", reload = True)

 

http://localhost:8000/docs 에서 보면 갑자기 없던 자물쇠 모양과 Authorize 버튼이 추가된 모습을 확인할 수 있습니다.

 

 

클릭하면 Username과 Password를 입력하라는 창이 뜹니다.

이는 HTTPie로도 실행 할 수 있습니다.

http -v :8000/users/me Authorization:'Basic c3Bpa2U6MTIzNA=='

 

Authorization 헤더를 이용하고, Basic이라는 단어로 기본 인증을 하겠다고 전달합니다. 뒤에 오는 문자열은 spike:1234를 base64 인코딩 한 결과입니다. 기본 인증은 이처럼 username:password와 같이 작성합니다.

 

다음으로 살펴볼 인증 방법은 OAuth2입니다.

 

OAuth2는 현대적인 웹 인증 방식 중 하나입니다. 웹 개발자라면 반드시 숙지하고 있어야 하죠. 물론 OAuth2를 이용한 OpenID Connect도 있지만, OAuth2보다는 많이 퍼지지 않았습니다. 사실 OAuth2도 사이트마다 조금씩 차이가 있습니다. 자세한 정보는 구글링을 하시는게 좋으리라 생각합니다.

 

여기서는 다른 걸 전부 생략하고 저희는 생성된 JWT만 확인하는 방법을 보겠습니다. JWT를 이용하기 위해 다음을 설치합니다.

 

물론 https://jwt.io/ 에서 사용가능한 다른 라이브러리도 찾을 수 있습니다. 추가로 비밀번호 해시를 위해 bcrypt 라이브러리도 설치합니다.

 

pip install python-jose bcrypt

 

main.py 코드입니다.

import uvicorn
from datetime import datetime, timedelta
from typing import Optional

import bcrypt
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import (
    HTTPBearer,
    HTTPAuthorizationCredentials,
    OAuth2PasswordRequestForm,
)
from pydantic import BaseModel
from jose import jwt
from jose.exceptions import ExpiredSignatureError


app = FastAPI()
security = HTTPBearer()

ALGORITHM = "HS256"
SECRET_KEY = "e9f17f1273a60019da967cd0648bdf6fd06f216ce03864ade0b51b29fa273d75"
fake_user_db = {
    "fastcampus": {
        "id": 1,
        "username": "fastcampus",
        "email": "fastcampus@fastcampus.com",
        "password": "$2b$12$kEsp4W6Vrm57c24ez4H1R.rdzYrXipAuSUZR.hxbqtYpjPLWbYtwS",
    }
}


class User(BaseModel):
    id: int
    username: str
    email: str


class UserPayload(User):
    exp: datetime


async def create_access_token(data: dict, exp: Optional[timedelta] = None):
    expire = datetime.utcnow() + (exp or timedelta(minutes=30))
    user_info = UserPayload(**data, exp=expire)

    return jwt.encode(user_info.dict(), SECRET_KEY, algorithm=ALGORITHM)


async def get_user(cred: HTTPAuthorizationCredentials = Depends(security)):
    token = cred.credentials
    try:
        decoded_data = jwt.decode(token, SECRET_KEY, ALGORITHM)
    except ExpiredSignatureError:
        raise HTTPException(401, "Expired")
    user_info = User(**decoded_data)

    return fake_user_db[user_info.username]


@app.post("/login")
async def issue_token(data: OAuth2PasswordRequestForm = Depends()):
    user = fake_user_db[data.username]

    if bcrypt.checkpw(data.password.encode(), user["password"].encode()):
        return await create_access_token(user, exp=timedelta(minutes=30))
    raise HTTPException(401)


@app.get("/users/me", response_model=User)
async def get_current_user(user: dict = Depends(get_user)):
    return user

if __name__ == '__main__':
	uvicorn.run('main:app', reload = True)

 

간단하게 JWT를 발급하고, JWT를 이용하여 인증 시스템을 간단히 구현했습니다.

'FastAPI' 카테고리의 다른 글

백그라운드 작업 - 2  (0) 2024.03.13
백그라운드 작업 - 1  (2) 2024.03.05
의존성 주입 - 2  (0) 2024.02.18
의존성 주입 - 1  (1) 2024.02.15
에러처리  (0) 2024.02.15