0
# Authentication and Security
1
2
OAuth integration with multiple providers, basic authentication middleware, and session management for secure web applications.
3
4
## Capabilities
5
6
### OAuth Integration
7
8
Complete OAuth authentication system with support for multiple providers including Google, GitHub, Discord, Hugging Face, and Auth0.
9
10
```python { .api }
11
class OAuth:
12
"""
13
Main OAuth handler class.
14
15
Manages OAuth authentication flow including authorization,
16
token exchange, and user information retrieval.
17
"""
18
19
def __init__(self, app, client, redirect_uri: str, logout_uri: str = '/logout'):
20
"""
21
Initialize OAuth handler.
22
23
Args:
24
app: FastHTML application instance
25
client: OAuth client (GoogleAppClient, GitHubAppClient, etc.)
26
redirect_uri: URI to redirect after authentication
27
logout_uri: URI for logout functionality
28
"""
29
30
def login_link(self, info: str = None, **scope) -> str:
31
"""Generate OAuth login URL."""
32
33
def logout_link(self) -> str:
34
"""Generate OAuth logout URL."""
35
```
36
37
### OAuth Provider Clients
38
39
Pre-configured OAuth clients for popular authentication providers.
40
41
```python { .api }
42
class GoogleAppClient:
43
"""
44
Google OAuth integration.
45
46
Provides authentication using Google OAuth 2.0 service.
47
"""
48
49
def __init__(self, client_id: str, client_secret: str, code: str, scope: list, project_id: str = None, **kwargs):
50
"""
51
Initialize Google OAuth client.
52
53
Args:
54
client_id: Google OAuth client ID
55
client_secret: Google OAuth client secret
56
code: Authorization code from OAuth flow
57
scope: OAuth scopes to request
58
project_id: Google Cloud project ID
59
**kwargs: Additional OAuth parameters
60
"""
61
62
@classmethod
63
def from_file(cls, fname: str, code: str, scope: list, **kwargs):
64
"""Create client from credentials file."""
65
66
def consent_url(self, proj: str) -> str:
67
"""Get consent screen URL."""
68
69
def creds(self):
70
"""Create Google credentials object."""
71
72
class GitHubAppClient:
73
"""
74
GitHub OAuth integration.
75
76
Provides authentication using GitHub OAuth service.
77
"""
78
79
def __init__(self, client_id: str, client_secret: str, code: str, scope: list, **kwargs):
80
"""
81
Initialize GitHub OAuth client.
82
83
Args:
84
client_id: GitHub OAuth client ID
85
client_secret: GitHub OAuth client secret
86
code: Authorization code from OAuth flow
87
scope: OAuth scopes to request
88
**kwargs: Additional OAuth parameters
89
"""
90
91
class HuggingFaceClient:
92
"""
93
Hugging Face OAuth integration.
94
95
Provides authentication using Hugging Face OAuth service.
96
"""
97
98
def __init__(self, client_id: str, client_secret: str, code: str, scope: list, state: str, **kwargs):
99
"""
100
Initialize Hugging Face OAuth client.
101
102
Args:
103
client_id: Hugging Face OAuth client ID
104
client_secret: Hugging Face OAuth client secret
105
code: Authorization code from OAuth flow
106
scope: OAuth scopes to request
107
state: State parameter for CSRF protection
108
**kwargs: Additional OAuth parameters
109
"""
110
111
class DiscordAppClient:
112
"""
113
Discord OAuth integration.
114
115
Provides authentication using Discord OAuth service.
116
"""
117
118
def __init__(self, client_id: str, client_secret: str, is_user: bool = False, perms: int = 0, scope: list = None, **kwargs):
119
"""
120
Initialize Discord OAuth client.
121
122
Args:
123
client_id: Discord OAuth client ID
124
client_secret: Discord OAuth client secret
125
is_user: Whether to use user authentication
126
perms: Permission bitfield
127
scope: OAuth scopes to request
128
**kwargs: Additional OAuth parameters
129
"""
130
131
def login_link(self, redirect_uri: str, scope: list = None, state: str = None) -> str:
132
"""Get Discord login URL."""
133
134
def parse_response(self, code: str, redirect_uri: str):
135
"""Parse OAuth callback response."""
136
137
class Auth0AppClient:
138
"""
139
Auth0 OAuth integration.
140
141
Provides authentication using Auth0 service.
142
"""
143
144
def __init__(self, domain: str, client_id: str, client_secret: str, code: str, scope: list, redirect_uri: str = "", **kwargs):
145
"""
146
Initialize Auth0 OAuth client.
147
148
Args:
149
domain: Auth0 domain
150
client_id: Auth0 client ID
151
client_secret: Auth0 client secret
152
code: Authorization code from OAuth flow
153
scope: OAuth scopes to request
154
redirect_uri: OAuth redirect URI
155
**kwargs: Additional OAuth parameters
156
"""
157
158
def login_link(self, req) -> str:
159
"""Get Auth0 login link for request."""
160
```
161
162
### Basic Authentication Middleware
163
164
HTTP Basic authentication middleware for simple username/password authentication.
165
166
```python { .api }
167
class BasicAuthMiddleware:
168
"""
169
HTTP Basic authentication middleware.
170
171
Provides simple username/password authentication using
172
HTTP Basic authentication standard.
173
"""
174
175
def __init__(self, app, verifier, realm: str = 'Secure Area'):
176
"""
177
Initialize basic auth middleware.
178
179
Args:
180
app: ASGI application
181
verifier: Function to verify username/password
182
realm: Authentication realm name
183
"""
184
185
def user_pwd_auth(user: str, pwd: str) -> bool:
186
"""
187
Username/password authentication function.
188
189
Verify user credentials against stored values.
190
191
Args:
192
user: Username to verify
193
pwd: Password to verify
194
195
Returns:
196
bool: True if credentials are valid
197
"""
198
199
def basic_logout() -> str:
200
"""
201
Generate logout URL for basic authentication.
202
203
Returns:
204
str: Logout URL that clears basic auth credentials
205
"""
206
```
207
208
### OAuth Utility Functions
209
210
Helper functions for OAuth flow management and user information handling.
211
212
```python { .api }
213
def login_link(client, redirect_uri: str, **scope) -> str:
214
"""
215
Generate OAuth login URL.
216
217
Args:
218
client: OAuth client instance
219
redirect_uri: Redirect URI after authentication
220
**scope: OAuth scope parameters
221
222
Returns:
223
str: OAuth authorization URL
224
"""
225
226
def get_host(request) -> str:
227
"""
228
Extract host from request.
229
230
Args:
231
request: HTTP request object
232
233
Returns:
234
str: Host name from request headers
235
"""
236
237
def redir_url(request, redirect_uri: str) -> str:
238
"""
239
Build redirect URL from request.
240
241
Args:
242
request: HTTP request object
243
redirect_uri: Base redirect URI
244
245
Returns:
246
str: Complete redirect URL
247
"""
248
249
def parse_response(code: str, state: str, client) -> dict:
250
"""
251
Parse OAuth callback response.
252
253
Args:
254
code: Authorization code from OAuth provider
255
state: State parameter for CSRF protection
256
client: OAuth client instance
257
258
Returns:
259
dict: Parsed response with tokens and user info
260
"""
261
262
def get_info(client, token: str) -> dict:
263
"""
264
Get user info from OAuth provider.
265
266
Args:
267
client: OAuth client instance
268
token: Access token
269
270
Returns:
271
dict: User information from provider
272
"""
273
274
def retr_info(code: str, client) -> dict:
275
"""
276
Retrieve and parse user info from OAuth response.
277
278
Args:
279
code: Authorization code
280
client: OAuth client instance
281
282
Returns:
283
dict: Complete user information
284
"""
285
286
def retr_id(code: str, client) -> str:
287
"""
288
Retrieve user ID from OAuth response.
289
290
Args:
291
code: Authorization code
292
client: OAuth client instance
293
294
Returns:
295
str: User ID from OAuth provider
296
"""
297
298
def url_match(url: str, pattern: str) -> bool:
299
"""
300
Match URL patterns for OAuth callback handling.
301
302
Args:
303
url: URL to match
304
pattern: Pattern to match against
305
306
Returns:
307
bool: True if URL matches pattern
308
"""
309
310
def load_creds(filename: str) -> dict:
311
"""
312
Load saved OAuth credentials from file.
313
314
Args:
315
filename: File containing saved credentials
316
317
Returns:
318
dict: Loaded credentials
319
"""
320
```
321
322
## Usage Examples
323
324
### Google OAuth Authentication
325
326
```python
327
from fasthtml.common import *
328
import os
329
330
# Set up OAuth credentials (use environment variables in production)
331
GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID')
332
GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET')
333
334
app, rt = fast_app(secret_key='your-secret-key')
335
336
# Create Google OAuth client
337
google_client = GoogleAppClient(
338
client_id=GOOGLE_CLIENT_ID,
339
client_secret=GOOGLE_CLIENT_SECRET,
340
scope=['openid', 'email', 'profile']
341
)
342
343
# Set up OAuth handler
344
oauth = OAuth(
345
app=app,
346
client=google_client,
347
redirect_uri='/auth/callback',
348
logout_uri='/logout'
349
)
350
351
@rt('/')
352
def homepage(request):
353
user = request.session.get('user')
354
if user:
355
return Titled("Welcome",
356
Div(
357
H1(f"Welcome, {user.get('name', 'User')}!"),
358
P(f"Email: {user.get('email', 'Not provided')}"),
359
Img(src=user.get('picture', ''), alt="Profile picture", width="100"),
360
A("Logout", href="/logout", cls="button")
361
)
362
)
363
else:
364
return Titled("Login Required",
365
Div(
366
H1("Please log in"),
367
A("Login with Google", href="/auth/login", cls="button")
368
)
369
)
370
371
@rt('/auth/login')
372
def login(request):
373
# Generate login URL and redirect
374
login_url = oauth.login_link()
375
return Redirect(login_url)
376
377
@rt('/auth/callback')
378
def auth_callback(request):
379
# Handle OAuth callback
380
code = request.query_params.get('code')
381
382
if code:
383
try:
384
# Get user information
385
user_info = retr_info(code, google_client)
386
387
# Store user in session
388
request.session['user'] = user_info
389
390
return Redirect('/')
391
except Exception as e:
392
return Div(f"Authentication failed: {str(e)}")
393
else:
394
return Div("Authentication was cancelled")
395
396
@rt('/logout')
397
def logout(request):
398
# Clear session
399
request.session.clear()
400
return Redirect('/')
401
402
serve()
403
```
404
405
### GitHub OAuth with User Permissions
406
407
```python
408
from fasthtml.common import *
409
import os
410
411
app, rt = fast_app(secret_key='your-secret-key')
412
413
# GitHub OAuth client with repository access
414
github_client = GitHubAppClient(
415
client_id=os.getenv('GITHUB_CLIENT_ID'),
416
client_secret=os.getenv('GITHUB_CLIENT_SECRET'),
417
scope=['user:email', 'repo']
418
)
419
420
oauth = OAuth(app, github_client, '/auth/github/callback')
421
422
@rt('/')
423
def homepage(request):
424
user = request.session.get('user')
425
if user:
426
return Titled("GitHub Integration",
427
Div(
428
H1(f"Hello, {user.get('login', 'User')}!"),
429
P(f"GitHub Profile: {user.get('html_url', '')}"),
430
P(f"Repositories: {user.get('public_repos', 0)}"),
431
P(f"Followers: {user.get('followers', 0)}"),
432
A("View Repositories", href="/repos"),
433
Br(),
434
A("Logout", href="/logout")
435
)
436
)
437
else:
438
return Titled("GitHub Login",
439
A("Login with GitHub", href="/auth/github")
440
)
441
442
@rt('/auth/github')
443
def github_login():
444
return Redirect(oauth.login_link())
445
446
@rt('/auth/github/callback')
447
def github_callback(request):
448
code = request.query_params.get('code')
449
if code:
450
user_info = retr_info(code, github_client)
451
request.session['user'] = user_info
452
return Redirect('/')
453
return Redirect('/auth/github')
454
455
@rt('/repos')
456
def view_repos(request):
457
user = request.session.get('user')
458
if not user:
459
return Redirect('/')
460
461
# This would typically fetch repositories using the OAuth token
462
return Titled("Your Repositories",
463
Div(
464
H2("Your GitHub Repositories"),
465
P("Repository list would be displayed here using the GitHub API"),
466
A("Back to Home", href="/")
467
)
468
)
469
```
470
471
### Basic Authentication
472
473
```python
474
from fasthtml.common import *
475
476
# Simple user database (use proper database in production)
477
USERS = {
478
'admin': 'secret123',
479
'user': 'password456'
480
}
481
482
def verify_credentials(username: str, password: str) -> bool:
483
"""Verify username and password"""
484
return USERS.get(username) == password
485
486
app, rt = fast_app()
487
488
# Add basic auth middleware
489
basic_auth = BasicAuthMiddleware(
490
app=app,
491
verifier=verify_credentials,
492
realm='Admin Area'
493
)
494
495
@rt('/')
496
def homepage(request):
497
# This route will require basic authentication
498
user = getattr(request, 'auth_user', 'Unknown')
499
return Titled("Protected Area",
500
Div(
501
H1(f"Welcome, {user}!"),
502
P("This is a protected area requiring authentication."),
503
A("Logout", href=basic_logout())
504
)
505
)
506
507
@rt('/login')
508
def login_form():
509
return Titled("Login",
510
Form(
511
Div(
512
Label("Username:", for_="username"),
513
Input(type="text", name="username", required=True),
514
cls="form-group"
515
),
516
Div(
517
Label("Password:", for_="password"),
518
Input(type="password", name="password", required=True),
519
cls="form-group"
520
),
521
Button("Login", type="submit"),
522
method="post",
523
action="/authenticate"
524
)
525
)
526
527
serve()
528
```
529
530
### Multi-Provider OAuth
531
532
```python
533
from fasthtml.common import *
534
import os
535
536
app, rt = fast_app(secret_key='your-secret-key')
537
538
# Set up multiple OAuth providers
539
providers = {
540
'google': GoogleAppClient(
541
client_id=os.getenv('GOOGLE_CLIENT_ID'),
542
client_secret=os.getenv('GOOGLE_CLIENT_SECRET')
543
),
544
'github': GitHubAppClient(
545
client_id=os.getenv('GITHUB_CLIENT_ID'),
546
client_secret=os.getenv('GITHUB_CLIENT_SECRET')
547
),
548
'discord': DiscordAppClient(
549
client_id=os.getenv('DISCORD_CLIENT_ID'),
550
client_secret=os.getenv('DISCORD_CLIENT_SECRET')
551
)
552
}
553
554
@rt('/')
555
def homepage(request):
556
user = request.session.get('user')
557
provider = request.session.get('provider')
558
559
if user:
560
return Titled("Multi-Provider Login",
561
Div(
562
H1(f"Welcome from {provider.title()}!"),
563
P(f"Name: {user.get('name', user.get('login', 'User'))}"),
564
P(f"Email: {user.get('email', 'Not provided')}"),
565
A("Logout", href="/logout")
566
)
567
)
568
else:
569
return Titled("Choose Login Provider",
570
Div(
571
H1("Login Options"),
572
Div(
573
A("Login with Google", href="/auth/google", cls="btn btn-google"),
574
Br(), Br(),
575
A("Login with GitHub", href="/auth/github", cls="btn btn-github"),
576
Br(), Br(),
577
A("Login with Discord", href="/auth/discord", cls="btn btn-discord"),
578
cls="login-buttons"
579
)
580
)
581
)
582
583
@rt('/auth/{provider}')
584
def provider_login(provider: str):
585
if provider not in providers:
586
return Redirect('/')
587
588
client = providers[provider]
589
oauth = OAuth(app, client, f'/auth/{provider}/callback')
590
return Redirect(oauth.login_link())
591
592
@rt('/auth/{provider}/callback')
593
def provider_callback(provider: str, request):
594
if provider not in providers:
595
return Redirect('/')
596
597
code = request.query_params.get('code')
598
if code:
599
client = providers[provider]
600
user_info = retr_info(code, client)
601
602
request.session['user'] = user_info
603
request.session['provider'] = provider
604
605
return Redirect('/')
606
607
return Redirect('/')
608
609
@rt('/logout')
610
def logout(request):
611
request.session.clear()
612
return Redirect('/')
613
```
614
615
### Custom Authentication with Database
616
617
```python
618
from fasthtml.common import *
619
import hashlib
620
import secrets
621
622
app, rt = fast_app(db='auth.db', secret_key='your-secret-key')
623
624
# Create users table
625
users = app.db.users
626
if not users.exists():
627
users.create({
628
'id': int,
629
'username': str,
630
'email': str,
631
'password_hash': str,
632
'salt': str,
633
'created_at': str
634
}, pk='id')
635
636
def hash_password(password: str, salt: str = None) -> tuple[str, str]:
637
"""Hash password with salt"""
638
if salt is None:
639
salt = secrets.token_hex(16)
640
641
password_hash = hashlib.pbkdf2_hmac(
642
'sha256',
643
password.encode('utf-8'),
644
salt.encode('utf-8'),
645
100000
646
).hex()
647
648
return password_hash, salt
649
650
def verify_password(password: str, password_hash: str, salt: str) -> bool:
651
"""Verify password against hash"""
652
computed_hash, _ = hash_password(password, salt)
653
return computed_hash == password_hash
654
655
@rt('/')
656
def homepage(request):
657
user_id = request.session.get('user_id')
658
if user_id:
659
user = users[user_id]
660
return Titled("Welcome",
661
Div(
662
H1(f"Welcome, {user.username}!"),
663
P(f"Email: {user.email}"),
664
A("Logout", href="/logout")
665
)
666
)
667
else:
668
return Titled("Authentication Demo",
669
Div(
670
H1("Please log in or register"),
671
A("Login", href="/login", cls="button"),
672
" ",
673
A("Register", href="/register", cls="button")
674
)
675
)
676
677
@rt('/register')
678
def register_form():
679
return Titled("Register",
680
Form(
681
Div(
682
Label("Username:", for_="username"),
683
Input(type="text", name="username", required=True),
684
cls="form-group"
685
),
686
Div(
687
Label("Email:", for_="email"),
688
Input(type="email", name="email", required=True),
689
cls="form-group"
690
),
691
Div(
692
Label("Password:", for_="password"),
693
Input(type="password", name="password", required=True),
694
cls="form-group"
695
),
696
Button("Register", type="submit"),
697
method="post",
698
action="/register/submit"
699
)
700
)
701
702
@rt('/register/submit', methods=['POST'])
703
def register_user(username: str, email: str, password: str):
704
# Check if user exists
705
existing = users.select().where('username = ? OR email = ?', username, email).first()
706
if existing:
707
return Div("Username or email already exists", style="color: red;")
708
709
# Hash password and create user
710
password_hash, salt = hash_password(password)
711
712
user_id = users.insert({
713
'username': username,
714
'email': email,
715
'password_hash': password_hash,
716
'salt': salt,
717
'created_at': str(datetime.now())
718
}).last_pk
719
720
# Log user in
721
request.session['user_id'] = user_id
722
723
return Redirect('/')
724
725
@rt('/login')
726
def login_form():
727
return Titled("Login",
728
Form(
729
Div(
730
Label("Username:", for_="username"),
731
Input(type="text", name="username", required=True),
732
cls="form-group"
733
),
734
Div(
735
Label("Password:", for_="password"),
736
Input(type="password", name="password", required=True),
737
cls="form-group"
738
),
739
Button("Login", type="submit"),
740
method="post",
741
action="/login/submit"
742
)
743
)
744
745
@rt('/login/submit', methods=['POST'])
746
def authenticate_user(username: str, password: str, request):
747
user = users.select().where('username = ?', username).first()
748
749
if user and verify_password(password, user.password_hash, user.salt):
750
request.session['user_id'] = user.id
751
return Redirect('/')
752
else:
753
return Div("Invalid username or password", style="color: red;")
754
755
@rt('/logout')
756
def logout(request):
757
request.session.clear()
758
return Redirect('/')
759
```