API autoryzacja i JWT w Pythonie

Kacper Sieradziński
Kacper Sieradziński
18 kwietnia 2025Edukacja6 min czytania

Bezpieczeństwo API to jeden z kluczowych aspektów profesjonalnego developmentu backendu. Nie ma znaczenia, jak szybki i elegancki jest Twój kod — jeśli dane użytkowników mogą zostać przejęte, cała aplikacja traci wiarygodność.

Obraz główny API autoryzacja i JWT w Pythonie

W świecie nowoczesnych aplikacji rozproszonych JWT (JSON Web Token) stał się standardem autoryzacji, zapewniając bezstanowe i skalowalne uwierzytelnianie użytkowników. W połączeniu z OAuth2, używanym przez platformy takie jak Google, GitHub czy Facebook, pozwala tworzyć bezpieczne i nowoczesne systemy logowania oraz autoryzacji. W tym przewodniku poznasz, jak implementować autoryzację JWT i OAuth2 w Pythonie, jak zapewnić bezpieczeństwo tokenów i jakie są najlepsze praktyki.

Czym jest JWT (JSON Web Token)

JWT to zaszyfrowany token, który zawiera informacje o użytkowniku i jego uprawnieniach. Zamiast trzymać sesję po stronie serwera, JWT pozwala przekazać te dane w postaci podpisanego ciągu tekstowego.

Struktura JWT

JWT składa się z trzech części oddzielonych kropkami:

Bash
1 xxxxx.yyyyy.zzzzz
  1. Header – informacje o algorytmie i typie tokena (np. {"alg": "HS256", "typ": "JWT"}).
  2. Payload – dane użytkownika (np. user_id, role, exp, iat).
  3. Signature – podpis HMAC lub RSA, który gwarantuje, że token nie został zmodyfikowany.

Przykładowy payload:

JSON
1 2 3 4 5 6 7 { "user_id": 123, "username": "admin", "role": "admin", "exp": 1714000000, "iat": 1713996400 }

Token jest przesyłany w nagłówku HTTP:

Bash
1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJhZG1pbiJ9.signature

Zalety JWT

  • Bezstanowość — serwer nie musi przechowywać sesji w pamięci lub bazie danych.
  • Skalowalność — łatwe w środowiskach rozproszonych i mikroserwisach.
  • Przenośność — token zawiera wszystkie potrzebne informacje.
  • Kompatybilność — działa z różnymi platformami i językami.

Wady JWT

  • Trudność unieważniania — po wydaniu tokena trudno go unieważnić przed wygaśnięciem.
  • Rozmiar — tokeny mogą być większe niż sesje ID.
  • Bezpieczeństwo — jeśli token zostanie przejęty, atakujący może go używać do czasu wygaśnięcia.

Jak działa autoryzacja z JWT

Proces autoryzacji z JWT składa się z kilku kroków:

  1. Użytkownik wysyła dane logowania (email, password) do endpointu /login lub /token.
  2. Serwer weryfikuje dane — sprawdza, czy użytkownik istnieje i hasło jest poprawne.
  3. Serwer generuje token JWT — tworzy token z danymi użytkownika i podpisem.
  4. Klient zapisuje token — frontend lub aplikacja mobilna przechowuje token (najlepiej w HTTP-only cookie).
  5. Klient wysyła token w kolejnych zapytaniach — w nagłówku Authorization: Bearer <token>.
  6. Serwer weryfikuje token — sprawdza podpis i ważność tokena przy każdym żądaniu.

Dzięki temu serwer nie musi trzymać sesji w pamięci, a użytkownik pozostaje zalogowany aż do wygaśnięcia tokena.

Implementacja JWT w Pythonie (FastAPI)

FastAPI posiada wbudowane wsparcie dla OAuth2 i JWT, co czyni go idealnym narzędziem do budowy bezpiecznych API.

Instalacja zależności

Bash
1 pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] python-multipart

Podstawowa implementacja

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 from fastapi import FastAPI, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt from passlib.context import CryptContext from datetime import datetime, timedelta from typing import Optional app = FastAPI() # Konfiguracja SECRET_KEY = "your-secret-key-change-in-production" # W produkcji użyj zmiennych środowiskowych ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 # Kontekst do hashowania haseł pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # OAuth2 scheme oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # Funkcja do tworzenia tokena def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt # Funkcja do weryfikacji tokena async def get_current_user(token: str = Depends(oauth2_scheme)): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise credentials_exception except JWTError: raise credentials_exception return username # Endpoint logowania @app.post("/token") async def login(form_data: OAuth2PasswordRequestForm = Depends()): # W rzeczywistej aplikacji sprawdź dane w bazie danych if form_data.username != "admin" or form_data.password != "secret": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": form_data.username}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} # Chroniony endpoint @app.get("/users/me") async def read_users_me(current_user: str = Depends(get_current_user)): return {"user": current_user}

W tym przykładzie endpoint /token generuje token, a /users/me weryfikuje go przy każdym żądaniu.

Pełna implementacja z bazą danych

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 from sqlalchemy.orm import Session from datetime import datetime, timedelta from jose import jwt from passlib.context import CryptContext pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: return pwd_context.hash(password) def authenticate_user(db: Session, username: str, password: str): user = db.query(User).filter(User.username == username).first() if not user: return False if not verify_password(password, user.hashed_password): return False return user @app.post("/token") async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user.username, "user_id": user.id}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"}

Odświeżanie tokenów i bezpieczeństwo

Tokeny JWT mają ograniczony czas życia (exp). Aby użytkownik nie musiał logować się ponownie po każdym wygaśnięciu tokena, wprowadza się refresh tokeny.

Strategia Access Token + Refresh Token

Najczęstsze podejście:

  • access_token – krótko żyjący (np. 15-30 minut), używany do autoryzacji żądań.
  • refresh_token – długowieczny (np. 7-30 dni), używany tylko do odświeżenia access tokena.

Implementacja:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 from datetime import datetime, timedelta import secrets REFRESH_TOKEN_EXPIRE_DAYS = 7 def create_refresh_token(user_id: int) -> str: # Refresh token można przechowywać w bazie danych token = secrets.token_urlsafe(32) # Zapisz token w bazie z datą wygaśnięcia db_refresh_token = RefreshToken( token=token, user_id=user_id, expires_at=datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) ) db.add(db_refresh_token) db.commit() return token @app.post("/refresh") async def refresh_token(refresh_token: str, db: Session = Depends(get_db)): db_token = db.query(RefreshToken).filter( RefreshToken.token == refresh_token, RefreshToken.expires_at > datetime.utcnow() ).first() if not db_token: raise HTTPException(status_code=401, detail="Invalid refresh token") # Wygeneruj nowy access token access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": db_token.user.username, "user_id": db_token.user_id}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} @app.post("/logout") async def logout(refresh_token: str, db: Session = Depends(get_db)): # Unieważnij refresh token db_token = db.query(RefreshToken).filter(RefreshToken.token == refresh_token).first() if db_token: db.delete(db_token) db.commit() return {"message": "Logged out successfully"}

Bezpieczne przechowywanie tokenów

Pamiętaj, by:

  1. Refresh tokeny przechowywać bezpiecznie po stronie klienta — najlepiej w HTTP-only cookies (chroni przed XSS).
  2. Nie trzymać tokenów w localStorage — są narażone na ataki XSS.
  3. Stosować HTTPS — tokeny nie mogą być przesyłane po HTTP.
  4. Używać Secure cookies — cookies powinny być dostępne tylko przez HTTPS.
  5. Implementować SameSite attribute — chroni przed atakami CSRF.
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from fastapi import Response @app.post("/token") async def login(form_data: OAuth2PasswordRequestForm = Depends(), response: Response = None): # ... logika autoryzacji ... access_token = create_access_token(data={"sub": form_data.username}) refresh_token = create_refresh_token(user.id) # Ustaw refresh token w HTTP-only cookie response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=True, # Tylko HTTPS samesite="lax", max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60 ) return {"access_token": access_token, "token_type": "bearer"}

OAuth2 – standard autoryzacji

OAuth2 to protokół używany przez największe platformy internetowe. Nie służy bezpośrednio do logowania, lecz do delegowania dostępu do zasobów w imieniu użytkownika.

Jak działa OAuth2?

Przykład: aplikacja A (Twój system) chce uzyskać dostęp do danych użytkownika z konta Google.

  1. Użytkownik klika „Zaloguj przez Google”.
  2. Następuje przekierowanie do Google OAuth (wraz z client_idredirect_uri).
  3. Użytkownik wyraża zgodę na dostęp.
  4. Google przekierowuje z powrotem z authorization_code.
  5. Serwer backendowy wymienia kod na access_tokenrefresh_token.
  6. Aplikacja używa access_token do pobrania danych użytkownika z Google.
  7. Aplikacja zapisuje dane użytkownika i tworzy własną sesję/JWT.

Implementacja OAuth2 w Pythonie

W Pythonie możesz użyć bibliotek:

  • Authlib (pip install authlib) — nowoczesna biblioteka z dobrym wsparciem dla FastAPI.
  • Requests-OAuthlib — dla prostszych integracji.

Przykład z Authlib:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 from authlib.integrations.starlette_client import OAuth from starlette.config import Config config = Config('.env') oauth = OAuth(config) oauth.register( name='google', server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', client_kwargs={ 'scope': 'openid email profile' } ) @app.get("/login/google") async def login_google(request: Request): redirect_uri = request.url_for('auth_google') return await oauth.google.authorize_redirect(request, redirect_uri) @app.get("/auth/google") async def auth_google(request: Request, db: Session = Depends(get_db)): token = await oauth.google.authorize_access_token(request) user_info = token.get('userinfo') # Znajdź lub utwórz użytkownika user = db.query(User).filter(User.email == user_info['email']).first() if not user: user = User( email=user_info['email'], username=user_info['name'], provider='google' ) db.add(user) db.commit() # Utwórz własny JWT token access_token = create_access_token(data={"sub": user.username, "user_id": user.id}) return {"access_token": access_token, "token_type": "bearer"}

JWT vs OAuth2 – różnice

Zrozumienie różnicy między JWT a OAuth2 jest kluczowe:

CechaJWTOAuth2
TypToken autoryzacyjnyProtokół autoryzacji
SesjaBezstanowaMoże być stanowa lub bezstanowa
UżycieAutoryzacja użytkowników w Twoim APILogowanie z zewnętrznych dostawców (Google, GitHub, Facebook)
SkalowalnośćWysokaZależna od implementacji
BezpieczeństwoWymaga HTTPS, poprawnego TTLZłożony, ale bardzo bezpieczny
ZłożonośćProstaBardziej złożony

Często oba podejścia są łączone — OAuth2 generuje JWT używane w dalszej komunikacji. OAuth2 służy do uzyskania zgody użytkownika i dostępu do zewnętrznych zasobów, a JWT do autoryzacji w Twoim własnym API.

Dobre praktyki bezpieczeństwa

Przy implementowaniu autoryzacji warto przestrzegać następujących zasad:

1. Zawsze używaj HTTPS

Tokeny nie mogą być przesyłane po HTTP — są narażone na przechwycenie.

Python
1 2 3 # W produkcji wymuś HTTPS SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True

2. Ustaw krótki TTL dla access tokenów

Access tokeny powinny wygasać szybko (15-30 minut), aby zminimalizować szkody w przypadku przejęcia.

Python
1 ACCESS_TOKEN_EXPIRE_MINUTES = 15 # Krótki czas życia

3. Weryfikuj podpis i claimy przy każdym żądaniu

Nie tylko sprawdzaj podpis, ale też iss (issuer), aud (audience) i exp (expiration):

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def verify_token(token: str): try: payload = jwt.decode( token, SECRET_KEY, algorithms=[ALGORITHM], options={"verify_signature": True, "verify_exp": True}, issuer="your-app", # Weryfikuj issuer audience="your-api" # Weryfikuj audience ) return payload except jwt.ExpiredSignatureError: raise HTTPException(status_code=401, detail="Token expired") except jwt.InvalidTokenError: raise HTTPException(status_code=401, detail="Invalid token")

4. Nie trzymaj tokenów w localStorage

Używaj HTTP-only cookies — są chronione przed JavaScriptem i atakami XSS.

5. Loguj unieważnienia tokenów

Po wylogowaniu użytkownika unieważnij refresh token w bazie danych.

Python
1 2 3 4 5 6 7 8 9 10 @app.post("/logout") async def logout(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id = payload.get("user_id") # Usuń wszystkie refresh tokeny użytkownika db.query(RefreshToken).filter(RefreshToken.user_id == user_id).delete() db.commit() return {"message": "Logged out"}

6. Nie używaj tego samego klucza do podpisywania i szyfrowania

Używaj osobnych kluczy dla różnych celów.

7. Używaj algorytmów asymetrycznych (RS256) w środowiskach produkcyjnych

Dla większego bezpieczeństwa użyj RS256 zamiast HS256:

Python
1 2 3 4 5 6 7 8 9 10 11 12 # Generowanie kluczy from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) public_key = private_key.public_key() # Podpisywanie tokena token = jwt.encode(payload, private_key, algorithm="RS256") # Weryfikacja tokena payload = jwt.decode(token, public_key, algorithms=["RS256"])

8. Implementuj rate limiting

Ogranicz liczbę prób logowania, aby chronić przed brute force:

Python
1 2 3 4 5 6 7 8 9 from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @app.post("/token") @limiter.limit("5/minute") async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()): # ... logika logowania ...

9. Waliduj dane wejściowe

Zawsze waliduj dane logowania i tokeny przed przetworzeniem.

10. Monitoruj podejrzane aktywności

Loguj nieudane próby logowania, użycie nieprawidłowych tokenów i inne podejrzane aktywności.

Podsumowanie

JWT i OAuth2 to fundament bezpiecznej autoryzacji w nowoczesnych API. Dzięki nim możesz tworzyć skalowalne, bezstanowe systemy uwierzytelniania, które łatwo integrują się z frontendem, mikroserwisami i zewnętrznymi dostawcami.

Najważniejsze zasady:

  • JWT do prostych, wewnętrznych API — idealny dla własnych aplikacji i mikroserwisów.
  • OAuth2 do integracji z zewnętrznymi usługami — Google, GitHub, Facebook.
  • Zawsze stosuj szyfrowanie (HTTPS) — bez tego całe bezpieczeństwo traci sens.
  • Odświeżanie tokenów — używaj refresh tokenów dla lepszego UX.
  • Walidacja po stronie serwera — zawsze weryfikuj tokeny, nawet jeśli wydają się ważne.
  • Bezpieczne przechowywanie — HTTP-only cookies zamiast localStorage.
  • Krótkie TTL dla access tokenów — minimalizuj ryzyko w przypadku przejęcia.

Pamiętaj, że bezpieczeństwo to proces ciągły — regularnie aktualizuj zależności, monitoruj logi i reaguj na nowe zagrożenia. Dobrze zaprojektowana autoryzacja to podstawa zaufania użytkowników do Twojej aplikacji.