Skip to content

Commit 5c1928e

Browse files
committed
add fastapi auth examples
1 parent e725c1f commit 5c1928e

File tree

3 files changed

+299
-0
lines changed

3 files changed

+299
-0
lines changed
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# This example demonstrates how use Dante to implement basic auth for FastAPI.
2+
#
3+
# To run this example, you need to install FastAPI:
4+
#
5+
# $ pip install fastapi[standard]
6+
#
7+
# Then, you can run the FastAPI server:
8+
#
9+
# $ cd examples/
10+
# $ fastapi dev fastapi-example-basic-auth.py
11+
#
12+
# And visit http://localhost:8000/docs to interact with the API.
13+
14+
from __future__ import annotations
15+
16+
from typing import Annotated
17+
18+
from fastapi import Depends, FastAPI
19+
from fastapi_auth import User, create_user, get_current_user_basic, init
20+
from pydantic import BaseModel
21+
22+
from dante import Dante
23+
24+
app = FastAPI()
25+
db = Dante("users.db", check_same_thread=False)
26+
users = init(db)
27+
28+
29+
class SignupRequest(BaseModel):
30+
username: str
31+
password: str
32+
33+
34+
@app.get("/me")
35+
def read_current_user(current_user: Annotated[User, Depends(get_current_user_basic)]):
36+
return current_user
37+
38+
39+
@app.post("/signup")
40+
def signup(req: SignupRequest):
41+
return create_user(req.username, req.password)

examples/fastapi-example-oauth2.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# This example demonstrates how use Dante to implement OAuth2 for FastAPI.
2+
#
3+
# To run this example, you need to install FastAPI:
4+
#
5+
# $ pip install fastapi[standard]
6+
#
7+
# Then, you can run the FastAPI server:
8+
#
9+
# $ cd examples/
10+
# $ fastapi dev fastapi-example-oauth2.py
11+
#
12+
# And visit http://localhost:8000/docs to interact with the API.
13+
14+
from __future__ import annotations
15+
16+
from typing import Annotated
17+
18+
from fastapi import Depends, FastAPI
19+
from fastapi_auth import (
20+
User,
21+
create_user,
22+
get_current_user_oauth2,
23+
init,
24+
oauth2_login_flow,
25+
)
26+
from pydantic import BaseModel
27+
28+
from dante import Dante
29+
30+
app = FastAPI()
31+
db = Dante("users.db", check_same_thread=False)
32+
users = init(db)
33+
34+
35+
class SignupRequest(BaseModel):
36+
username: str
37+
password: str
38+
39+
40+
@app.post("/token")
41+
async def login(response: dict = Depends(oauth2_login_flow)):
42+
return response
43+
44+
45+
@app.get("/me")
46+
def read_current_user(current_user: Annotated[User, Depends(get_current_user_oauth2)]):
47+
return current_user
48+
49+
50+
@app.post("/signup")
51+
def signup(req: SignupRequest):
52+
return create_user(req.username, req.password, include_token=True)

examples/fastapi_auth.py

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Dante-backed user authentication for FastAPI
2+
#
3+
# Provides a simple user authentication system with a simple User
4+
# model, and implements basic and OAuth2 password flows. Copy and
5+
# adapt as needed for your project.
6+
#
7+
# For usage, check fastapi-example-basic-auth.py
8+
# and fastapi-example-oauth2.py.
9+
10+
from __future__ import annotations
11+
12+
from datetime import datetime
13+
from hashlib import sha512
14+
from typing import Annotated, Optional
15+
from uuid import uuid4
16+
17+
from fastapi import Depends, HTTPException, status
18+
from fastapi.security import (
19+
HTTPBasic,
20+
HTTPBasicCredentials,
21+
OAuth2PasswordBearer,
22+
OAuth2PasswordRequestForm,
23+
)
24+
from passlib.hash import pbkdf2_sha512
25+
from pydantic import BaseModel, Field
26+
27+
from dante.sync import Collection, Dante
28+
29+
users: Collection
30+
31+
32+
class User(BaseModel):
33+
""" "
34+
User model for authentication.
35+
36+
Password is stored hashed (use `User.set_password()`) and is never
37+
serialized back to the client.
38+
"""
39+
40+
username: str
41+
password_hash: str = ""
42+
created_at: datetime
43+
is_active: bool = True
44+
token: Optional[str] = None
45+
46+
def set_password(self, password: str):
47+
"""
48+
Set the user's password.
49+
50+
Note that this only updates the `password_hash` field on
51+
the object, and doesn't actually save/update the object in the
52+
database.
53+
54+
:param password: Password to hash and store
55+
"""
56+
self.password_hash = hash_pwd(password)
57+
58+
59+
class CurrentUser(User):
60+
"""
61+
User model for the current user, suitable to return to the API client.
62+
63+
This excludes the password hash field so it's not included
64+
in the response to the client. Note that you don't want to save this
65+
to the database, otherwise you'll reset (remove) the user's password.
66+
"""
67+
68+
password_hash: str = Field("", exclude=True)
69+
70+
71+
# Support for HTTP Basic auth with username/password in Authorization header
72+
basic_security = HTTPBasic()
73+
# Support for OAuth2 password flow with bearer token in Authorization header
74+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
75+
76+
77+
def init(db: Dante) -> Collection:
78+
"""
79+
Initialize the Users collection in the database.
80+
81+
:param db: Dante instance
82+
:return: Users collection
83+
"""
84+
global users
85+
users = db[User]
86+
return users
87+
88+
89+
def hash_pwd(password: str) -> str:
90+
"""
91+
Hash a password using PBKDF2 with SHA-512.
92+
93+
Internal method, should only be used in fastapi_auth module.
94+
95+
:param password: Password to hash
96+
:return: Hashed password
97+
"""
98+
return pbkdf2_sha512.hash(password)
99+
100+
101+
def verify_pwd(password: str, hash: str) -> bool:
102+
"""
103+
Verify a password against a hash using PBKDF2 with SHA-512.
104+
105+
Internal method, should only be used in fastapi_auth module.
106+
107+
:param password: Password to verify
108+
:param hash: Hashed password
109+
:return: True if the password matches the hash, False otherwise
110+
"""
111+
return pbkdf2_sha512.verify(password, hash)
112+
113+
114+
def create_user(username: str, password: str, include_token=False) -> CurrentUser:
115+
"""
116+
Create a new user with the given username and password.
117+
118+
Caveat: this simple implementation is vulnerable to race conditions
119+
in which many users with the same username are created at the
120+
same time. A full implementation would use a database transaction
121+
to ensure check and insert are atomic.
122+
123+
:param username: Username
124+
:param password: Password
125+
:param include_token: Whether to include a token in the response
126+
:return: Newly created user
127+
"""
128+
if users.find_one(username=username):
129+
raise ValueError("User already exists")
130+
user = User(username=username, created_at=datetime.now())
131+
user.set_password(password)
132+
if include_token:
133+
user.token = sha512(uuid4().bytes).hexdigest()
134+
users.insert(user)
135+
return CurrentUser(**user.model_dump())
136+
137+
138+
def get_current_user_basic(
139+
credentials: HTTPBasicCredentials = Depends(basic_security),
140+
) -> CurrentUser:
141+
"""
142+
Get current used assuming Basic HTTP auth.
143+
144+
If there's no current user, raises a 401 Unauthorized exception.
145+
146+
:param credentials: HTTP Basic credentials dependency from FastAPI
147+
:return: The current user
148+
"""
149+
user = users.find_one(username=credentials.username)
150+
if user is None or not verify_pwd(credentials.password, user.password_hash):
151+
raise HTTPException(
152+
status_code=status.HTTP_401_UNAUTHORIZED,
153+
detail="Invalid credentials",
154+
)
155+
return CurrentUser(**user.model_dump())
156+
157+
158+
def get_current_user_oauth2(
159+
token: Annotated[str, Depends(oauth2_scheme)],
160+
) -> CurrentUser:
161+
"""
162+
Get current user assuming OAuth2 bearer token.
163+
164+
If there's no current user, raises a 401 Unauthorized exception.
165+
166+
:param token: OAuth2 bearer token dependency from FastAPI
167+
:return: The current user
168+
"""
169+
if token is None:
170+
raise HTTPException(
171+
status_code=status.HTTP_401_UNAUTHORIZED,
172+
detail="Invalid token",
173+
)
174+
user = users.find_one(token=token)
175+
if user is None:
176+
raise HTTPException(
177+
status_code=status.HTTP_401_UNAUTHORIZED,
178+
detail="Invalid token",
179+
)
180+
# We don't want the client to see even the hashed password of the user.
181+
# Note this means that if you just update the user object, it'll set
182+
# the password to an empty string, effectively locking out the user.
183+
return CurrentUser(**user.model_dump())
184+
185+
186+
def oauth2_login_flow(
187+
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
188+
) -> dict:
189+
"""
190+
Implementation of the OAuth2 password flow.
191+
192+
If the user is not found or the password is incorrect, raises a 401
193+
Unauthorized exception.
194+
195+
:param form_data: OAuth2 password request form from FastAPI
196+
:return: OAuth2 access token
197+
"""
198+
user = users.find_one(username=form_data.username)
199+
if user is None or not verify_pwd(form_data.password, user.password_hash):
200+
raise HTTPException(
201+
status_code=status.HTTP_401_UNAUTHORIZED,
202+
detail="Incorrect username or password",
203+
)
204+
user.token = sha512(uuid4().bytes).hexdigest()
205+
users.update(user, username=user.username)
206+
return {"access_token": user.token, "token_type": "bearer"}

0 commit comments

Comments
 (0)