범위 기반 권한 부여와 역할 가드
OAuth2 범위와 재사용 가능한 의존성 가드로 엔드포인트별 권한을 적용해 역할 기반 접근 제어를 구현합니다.
범위 기반 권한 부여와 역할 가드은(는) CoddyKit의 무료 FastAPI Backend Development Bootcamp 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 FastAPI Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Authentication vs Authorization
Once a user proves who they are (authentication), you still need to decide what they are allowed to do. That second step is authorization.
- Authentication answers: who are you? (verify the JWT)
- Authorization answers: are you permitted to call this endpoint?
In OAuth2, fine-grained permissions are expressed as scopes — short strings like items:read or users:write. Each token carries the scopes that were granted, and each endpoint declares the scopes it requires.
What a Scope Looks Like
A scope is just a label for a permission. By convention they use a resource:action shape, which keeps them readable as your API grows.
items:read— list or fetch itemsitems:write— create or update itemsadmin— full administrative access
The JWT stores granted scopes, usually as a space-separated string in a scopes claim. Here is a tiny helper that parses and checks them.
def has_scope(token_scopes: str, required: str) -> bool:
granted = token_scopes.split()
return required in granted
claim = "items:read items:write"
print(has_scope(claim, "items:read")) # True
print(has_scope(claim, "admin")) # FalseDeclaring Scopes on the OAuth2 Scheme
FastAPI's OAuth2PasswordBearer accepts a scopes dictionary that documents every scope your API understands. This powers the interactive docs so testers can request specific permissions.
The mapping is { scope_name: human_description }. It does not grant anything by itself — it just describes what exists.
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
"items:read": "Read items.",
"items:write": "Create or update items.",
"admin": "Full administrative access.",
},
)Encoding Granted Scopes Into the JWT
When a user logs in, you decide which scopes they get (often based on their role) and embed them in the token. Store them in a scopes claim so every later request carries the permissions.
The token request form includes a scope field; you should grant only scopes the user is actually entitled to — never blindly echo what the client asked for.
from datetime import datetime, timedelta, timezone
import jwt # PyJWT
SECRET = "change-me"
def create_token(username: str, scopes: list[str]) -> str:
payload = {
"sub": username,
"scopes": scopes,
"exp": datetime.now(timezone.utc) + timedelta(minutes=30),
}
return jwt.encode(payload, SECRET, algorithm="HS256")Requiring Scopes with the Security Helper
To require a scope on an endpoint, declare the dependency with Security(...) (not plain Depends) and pass a scopes list. FastAPI collects all required scopes along the dependency tree and exposes them via a SecurityScopes object.
Below, the endpoint demands the items:read scope before read_items ever runs.
from fastapi import Depends, Security, FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(
user=Security(get_current_user, scopes=["items:read"]),
):
return {"owner": user["username"]}Validating Scopes in the Dependency
The dependency that resolves the current user receives a SecurityScopes argument listing every scope required by the route. You decode the JWT, read the granted scopes, and reject the request if any required scope is missing.
- Return 401 if the token is invalid or expired.
- Return 403 if the token is valid but lacks the scope (the user is known but not permitted).
from fastapi import Depends, HTTPException, status
from fastapi.security import SecurityScopes
import jwt
async def get_current_user(
security_scopes: SecurityScopes,
token: str = Depends(oauth2_scheme),
):
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
except jwt.PyJWTError:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
token_scopes = payload.get("scopes", [])
for scope in security_scopes.scopes:
if scope not in token_scopes:
raise HTTPException(
status.HTTP_403_FORBIDDEN,
detail=f"Not enough permissions: {scope}",
)
return {"username": payload["sub"], "scopes": token_scopes}The WWW-Authenticate Header
OAuth2 expects a 401 response to include a WWW-Authenticate header describing how to authenticate. When scopes are involved, that header should also list the required scopes so the client knows what to request.
FastAPI's SecurityScopes object builds this string for you via scope_str.
from fastapi.security import SecurityScopes
from fastapi import HTTPException, status
def auth_error(security_scopes: SecurityScopes, detail: str):
if security_scopes.scopes:
value = f'Bearer scope="{security_scopes.scope_str}"'
else:
value = "Bearer"
return HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=detail,
headers={"WWW-Authenticate": value},
)From Scopes to Roles
Scopes are flexible but granular. Most apps also think in roles — admin, editor, viewer — where each role bundles a set of scopes. A clean approach is to map roles to scopes at login time.
This keeps endpoints declaring fine-grained scopes while users are managed by coarse-grained roles.
ROLE_SCOPES = {
"admin": ["items:read", "items:write", "admin"],
"editor": ["items:read", "items:write"],
"viewer": ["items:read"],
}
def scopes_for_role(role: str) -> list[str]:
return ROLE_SCOPES.get(role, [])
print(scopes_for_role("editor")) # ['items:read', 'items:write']
print(scopes_for_role("guest")) # []A Reusable Role Guard
Sometimes you want to guard by role directly instead of by scope. A guard factory returns a dependency configured for a specific role, so you can reuse it across many endpoints.
Calling require_role("admin") produces a dependency that rejects anyone whose token role is not admin.
from fastapi import Depends, HTTPException, status
def require_role(required_role: str):
async def guard(user=Depends(get_current_user)):
if user.get("role") != required_role:
raise HTTPException(
status.HTTP_403_FORBIDDEN,
detail="Insufficient role",
)
return user
return guard
@app.delete("/items/{item_id}")
async def delete_item(item_id: int, user=Depends(require_role("admin"))):
return {"deleted": item_id, "by": user["username"]}Guarding by 'Any of' Several Roles
Real endpoints often allow more than one role. Generalize the guard to accept a set of acceptable roles and pass if the user matches any of them.
This pure-Python pattern is easy to unit test without spinning up a server.
def check_access(user_role: str, allowed: set[str]) -> bool:
return user_role in allowed
print(check_access("editor", {"editor", "admin"})) # True
print(check_access("viewer", {"editor", "admin"})) # False
print(check_access("admin", {"admin"})) # TruePutting It Together
A complete flow looks like this:
- Login maps the user's role to scopes and signs a JWT.
- Each endpoint declares required scopes with
Security(get_current_user, scopes=[...]). - The dependency decodes the token and verifies every required scope is present.
- Reusable
require_roleguards handle coarse role checks where scopes are overkill.
Because guards are just dependencies, they compose: an endpoint can require both a scope and a role, and FastAPI runs both before your handler.
@app.post("/admin/reports")
async def make_report(
admin=Depends(require_role("admin")),
user=Security(get_current_user, scopes=["admin"]),
):
return {"status": "generated", "by": user["username"]}Quick Check
Choose the response that correctly distinguishes authentication failure from authorization failure for scope checks.
Recap
You now know how to enforce per-endpoint permissions in FastAPI:
- Scopes are granular
resource:actionpermissions stored in the JWT'sscopesclaim. - Declare required scopes with
Security(dep, scopes=[...]); FastAPI gathers them intoSecurityScopes. - The dependency decodes the token, returns 401 for invalid credentials and 403 when a required scope is missing.
- Add a
WWW-Authenticateheader (withscope_str) on 401 responses for OAuth2 compliance. - Roles bundle scopes; map role to scopes at login, and use a reusable
require_roleguard factory for coarse role-based access control.
Because guards are ordinary dependencies, they compose cleanly and keep authorization logic out of your handlers.
자주 묻는 질문
“범위 기반 권한 부여와 역할 가드” 강의는 무료인가요?
네 — “범위 기반 권한 부여와 역할 가드” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 FastAPI Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
“범위 기반 권한 부여와 역할 가드”에서 뭘 배우나요?
OAuth2 범위와 재사용 가능한 의존성 가드로 엔드포인트별 권한을 적용해 역할 기반 접근 제어를 구현합니다. 브라우저에서 직접 실행하는 실습 코드로 FastAPI Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
FastAPI Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 FastAPI Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“범위 기반 권한 부여와 역할 가드” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 FastAPI Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 FastAPI Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- OAuth2 비밀번호 흐름과 토큰 발급
- python-jose를 활용한 JWT 서명과 검증
- 새로 고침 토큰과 토큰 순환
- 범위 기반 권한 부여와 역할 가드