0
# WebAuthn Support
1
2
WebAuthn/FIDO2 authentication for passwordless and phishing-resistant authentication using hardware security keys, biometrics, and platform authenticators in Flask applications.
3
4
## Capabilities
5
6
### WebAuthn Forms
7
8
Forms for managing WebAuthn credential registration, signin, and administration.
9
10
```python { .api }
11
class WebAuthnRegisterForm(Form):
12
"""
13
WebAuthn credential registration initiation form.
14
15
Fields:
16
- usage: Usage type for credential ('first', 'secondary')
17
- name: Human-readable name for the credential
18
"""
19
usage: SelectField
20
name: StringField
21
22
class WebAuthnRegisterResponseForm(Form):
23
"""
24
WebAuthn credential registration completion form.
25
26
Fields:
27
- credential: JSON credential response from authenticator
28
- name: Human-readable name for the credential
29
"""
30
credential: HiddenField
31
name: StringField
32
33
class WebAuthnSigninForm(Form):
34
"""
35
WebAuthn signin initiation form.
36
37
Fields:
38
- identity: User identity (email/username) for credential lookup
39
"""
40
identity: StringField
41
42
class WebAuthnSigninResponseForm(Form):
43
"""
44
WebAuthn signin completion form.
45
46
Fields:
47
- credential: JSON credential assertion from authenticator
48
"""
49
credential: HiddenField
50
51
class WebAuthnDeleteForm(Form):
52
"""
53
WebAuthn credential deletion form.
54
55
Fields:
56
- credential_id: ID of credential to delete
57
"""
58
credential_id: HiddenField
59
60
class WebAuthnVerifyForm(Form):
61
"""
62
WebAuthn verification form for sensitive operations.
63
64
Fields:
65
- credential: JSON credential assertion for verification
66
"""
67
credential: HiddenField
68
```
69
70
### WebAuthn User Mixin
71
72
Mixin providing WebAuthn credential management methods for user models.
73
74
```python { .api }
75
class WebAuthnMixin:
76
"""
77
Mixin for user models providing WebAuthn credential interface.
78
"""
79
80
@property
81
def webauthn_credentials(self):
82
"""
83
Return list of WebAuthn credentials for user.
84
85
Returns:
86
List of WebAuthnCredential objects associated with user
87
"""
88
89
def get_webauthn_credential_by_id(self, credential_id):
90
"""
91
Get WebAuthn credential by credential ID.
92
93
Parameters:
94
- credential_id: Base64-encoded credential ID to search for
95
96
Returns:
97
WebAuthnCredential object if found, None otherwise
98
"""
99
100
def get_webauthn_credential_options(self):
101
"""
102
Get options for WebAuthn credential creation.
103
104
Returns:
105
Dictionary containing credential creation options including:
106
- rp: Relying party information
107
- user: User information for credential
108
- challenge: Random challenge bytes
109
- pubKeyCredParams: Supported algorithms
110
- excludeCredentials: Existing credentials to exclude
111
"""
112
```
113
114
### WebAuthn Utility Class
115
116
Utility class for WebAuthn operations and credential management.
117
118
```python { .api }
119
class WebauthnUtil:
120
"""
121
WebAuthn operations and credential management utility class.
122
"""
123
124
def __init__(self, app=None):
125
"""
126
Initialize WebAuthn utility.
127
128
Parameters:
129
- app: Flask application instance (optional)
130
"""
131
132
def init_app(self, app):
133
"""
134
Initialize WebAuthn utility with Flask app.
135
136
Parameters:
137
- app: Flask application instance
138
"""
139
140
def generate_challenge(self):
141
"""
142
Generate cryptographic challenge for WebAuthn operations.
143
144
Returns:
145
Base64-encoded challenge bytes
146
"""
147
148
def get_credential_creation_options(self, user):
149
"""
150
Get credential creation options for user registration.
151
152
Parameters:
153
- user: User object for credential creation
154
155
Returns:
156
Dictionary with credential creation options
157
"""
158
159
def get_credential_request_options(self, user=None):
160
"""
161
Get credential request options for authentication.
162
163
Parameters:
164
- user: User object (optional, for usernameless authentication)
165
166
Returns:
167
Dictionary with credential request options
168
"""
169
170
def register_credential(self, user, credential_response, name=None):
171
"""
172
Register new WebAuthn credential for user.
173
174
Parameters:
175
- user: User object to associate credential with
176
- credential_response: Credential response from authenticator
177
- name: Human-readable name for credential (optional)
178
179
Returns:
180
WebAuthnCredential object if registration successful
181
182
Raises:
183
ValidationError if credential registration fails
184
"""
185
186
def authenticate_credential(self, credential_response, user=None):
187
"""
188
Authenticate using WebAuthn credential assertion.
189
190
Parameters:
191
- credential_response: Credential assertion from authenticator
192
- user: User object for verification (optional)
193
194
Returns:
195
Tuple of (user, credential) if authentication successful,
196
(None, None) otherwise
197
"""
198
199
def delete_credential(self, user, credential_id):
200
"""
201
Delete WebAuthn credential for user.
202
203
Parameters:
204
- user: User object owning the credential
205
- credential_id: ID of credential to delete
206
207
Returns:
208
True if credential deleted successfully, False otherwise
209
"""
210
```
211
212
### WebAuthn Plugin for Two-Factor Authentication
213
214
Plugin integrating WebAuthn with Flask-Security's two-factor authentication system.
215
216
```python { .api }
217
class WebAuthnTfPlugin(TfPluginBase):
218
"""
219
WebAuthn two-factor authentication plugin.
220
"""
221
222
def get_setup_form(self):
223
"""
224
Get form for WebAuthn 2FA setup.
225
226
Returns:
227
WebAuthnRegisterForm class for credential registration
228
"""
229
230
def get_verify_form(self):
231
"""
232
Get form for WebAuthn 2FA verification.
233
234
Returns:
235
WebAuthnVerifyForm class for credential verification
236
"""
237
238
def setup_validate(self, form):
239
"""
240
Validate WebAuthn 2FA setup.
241
242
Parameters:
243
- form: WebAuthnRegisterResponseForm with credential data
244
245
Returns:
246
True if setup validation successful, False otherwise
247
"""
248
249
def verify(self, form):
250
"""
251
Verify WebAuthn 2FA credential.
252
253
Parameters:
254
- form: WebAuthnVerifyForm with credential assertion
255
256
Returns:
257
True if verification successful, False otherwise
258
"""
259
260
def delete(self, totp_secret):
261
"""
262
Delete WebAuthn 2FA credential.
263
264
Parameters:
265
- totp_secret: Credential identifier for deletion
266
267
Returns:
268
True if deletion successful, False otherwise
269
"""
270
```
271
272
## Configuration
273
274
WebAuthn support is configured through Flask-Security configuration variables:
275
276
```python
277
# Enable WebAuthn support
278
app.config['SECURITY_WEBAUTHN'] = True
279
280
# WebAuthn relying party configuration
281
app.config['SECURITY_WEBAUTHN_RP_NAME'] = 'Your App Name'
282
app.config['SECURITY_WEBAUTHN_RP_ID'] = 'example.com'
283
284
# Challenge configuration
285
app.config['SECURITY_WEBAUTHN_CHALLENGE_TTL'] = 300 # 5 minutes
286
287
# Credential algorithms (default includes ES256, PS256, RS256)
288
app.config['SECURITY_WEBAUTHN_ALGORITHMS'] = ['ES256', 'PS256', 'RS256']
289
290
# Authenticator attachment preference
291
app.config['SECURITY_WEBAUTHN_AUTHENTICATOR_ATTACHMENT'] = 'cross-platform'
292
293
# User verification requirement
294
app.config['SECURITY_WEBAUTHN_USER_VERIFICATION'] = 'preferred'
295
```
296
297
## Usage Examples
298
299
### Basic WebAuthn Setup
300
301
```python
302
from flask import Flask
303
from flask_security import Security, WebAuthnMixin, UserMixin
304
from flask_sqlalchemy import SQLAlchemy
305
306
app = Flask(__name__)
307
app.config['SECURITY_WEBAUTHN'] = True
308
app.config['SECURITY_WEBAUTHN_RP_NAME'] = 'My Secure App'
309
app.config['SECURITY_WEBAUTHN_RP_ID'] = 'myapp.com'
310
311
db = SQLAlchemy(app)
312
313
class User(db.Model, UserMixin, WebAuthnMixin):
314
id = db.Column(db.Integer, primary_key=True)
315
email = db.Column(db.String(255), unique=True)
316
password = db.Column(db.String(255))
317
active = db.Column(db.Boolean(), default=True)
318
319
class WebAuthnCredential(db.Model):
320
id = db.Column(db.Integer, primary_key=True)
321
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
322
credential_id = db.Column(db.Text, unique=True)
323
public_key = db.Column(db.Text)
324
name = db.Column(db.String(100))
325
usage = db.Column(db.String(20))
326
created_at = db.Column(db.DateTime)
327
328
security = Security(app, user_datastore)
329
```
330
331
### Credential Registration
332
333
```python
334
from flask_security import current_user, login_required
335
336
@app.route('/register-webauthn')
337
@login_required
338
def register_webauthn():
339
# Display WebAuthn registration form
340
return render_template('webauthn_register.html')
341
342
@app.route('/webauthn/register/begin', methods=['POST'])
343
@login_required
344
def webauthn_register_begin():
345
# Get credential creation options
346
options = current_user.get_webauthn_credential_options()
347
session['webauthn_challenge'] = options['challenge']
348
return jsonify(options)
349
350
@app.route('/webauthn/register/complete', methods=['POST'])
351
@login_required
352
def webauthn_register_complete():
353
credential_response = request.json
354
challenge = session.get('webauthn_challenge')
355
356
try:
357
# Register the credential
358
webauthn_util = current_app.extensions['security'].webauthn_util
359
credential = webauthn_util.register_credential(
360
current_user,
361
credential_response,
362
name=request.form.get('name', 'Security Key')
363
)
364
365
return jsonify({'success': True, 'credential_id': credential.id})
366
except ValidationError as e:
367
return jsonify({'success': False, 'error': str(e)}), 400
368
```
369
370
### WebAuthn Authentication
371
372
```python
373
@app.route('/webauthn-signin')
374
def webauthn_signin():
375
return render_template('webauthn_signin.html')
376
377
@app.route('/webauthn/authenticate/begin', methods=['POST'])
378
def webauthn_authenticate_begin():
379
identity = request.form.get('identity')
380
user = lookup_identity(identity) if identity else None
381
382
# Get authentication options
383
webauthn_util = current_app.extensions['security'].webauthn_util
384
options = webauthn_util.get_credential_request_options(user)
385
session['webauthn_challenge'] = options['challenge']
386
387
return jsonify(options)
388
389
@app.route('/webauthn/authenticate/complete', methods=['POST'])
390
def webauthn_authenticate_complete():
391
credential_response = request.json
392
challenge = session.get('webauthn_challenge')
393
394
try:
395
webauthn_util = current_app.extensions['security'].webauthn_util
396
user, credential = webauthn_util.authenticate_credential(credential_response)
397
398
if user:
399
login_user(user)
400
return jsonify({'success': True, 'redirect': '/dashboard'})
401
else:
402
return jsonify({'success': False, 'error': 'Authentication failed'}), 401
403
404
except ValidationError as e:
405
return jsonify({'success': False, 'error': str(e)}), 400
406
```
407
408
### WebAuthn as Two-Factor Authentication
409
410
```python
411
from flask_security import TwoFactorSetupForm
412
413
# Enable WebAuthn 2FA plugin
414
app.config['SECURITY_TWO_FACTOR_ENABLED_METHODS'] = ['authenticator', 'webauthn']
415
416
@app.route('/setup-2fa-webauthn')
417
@login_required
418
def setup_2fa_webauthn():
419
"""Setup WebAuthn as second factor."""
420
if not current_user.tf_totp_secret:
421
# User needs to complete initial 2FA setup first
422
return redirect(url_for_security('two_factor_setup'))
423
424
return render_template('setup_webauthn_2fa.html')
425
426
@app.route('/verify-2fa-webauthn')
427
@login_required
428
def verify_2fa_webauthn():
429
"""Verify WebAuthn credential for 2FA."""
430
return render_template('verify_webauthn_2fa.html')
431
```
432
433
### Credential Management
434
435
```python
436
@app.route('/manage-webauthn')
437
@login_required
438
def manage_webauthn_credentials():
439
"""Display user's WebAuthn credentials."""
440
credentials = current_user.webauthn_credentials
441
return render_template('manage_webauthn.html', credentials=credentials)
442
443
@app.route('/delete-webauthn/<credential_id>', methods=['POST'])
444
@login_required
445
def delete_webauthn_credential(credential_id):
446
"""Delete a WebAuthn credential."""
447
webauthn_util = current_app.extensions['security'].webauthn_util
448
449
if webauthn_util.delete_credential(current_user, credential_id):
450
flash('Security key removed successfully')
451
else:
452
flash('Failed to remove security key')
453
454
return redirect(url_for('manage_webauthn_credentials'))
455
```
456
457
### JavaScript Integration
458
459
```javascript
460
// WebAuthn credential registration
461
async function registerWebAuthnCredential() {
462
try {
463
// Get registration options from server
464
const optionsResponse = await fetch('/webauthn/register/begin', {
465
method: 'POST',
466
headers: {'Content-Type': 'application/json'},
467
});
468
const options = await optionsResponse.json();
469
470
// Convert base64 strings to ArrayBuffers
471
options.challenge = base64ToArrayBuffer(options.challenge);
472
options.user.id = base64ToArrayBuffer(options.user.id);
473
474
// Create credential
475
const credential = await navigator.credentials.create({
476
publicKey: options
477
});
478
479
// Send credential to server
480
const registrationResponse = await fetch('/webauthn/register/complete', {
481
method: 'POST',
482
headers: {'Content-Type': 'application/json'},
483
body: JSON.stringify({
484
id: credential.id,
485
rawId: arrayBufferToBase64(credential.rawId),
486
response: {
487
clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
488
attestationObject: arrayBufferToBase64(credential.response.attestationObject)
489
}
490
})
491
});
492
493
const result = await registrationResponse.json();
494
if (result.success) {
495
alert('Security key registered successfully!');
496
} else {
497
alert('Registration failed: ' + result.error);
498
}
499
500
} catch (error) {
501
console.error('WebAuthn registration error:', error);
502
alert('WebAuthn registration failed');
503
}
504
}
505
506
// WebAuthn authentication
507
async function authenticateWithWebAuthn() {
508
try {
509
// Get authentication options
510
const optionsResponse = await fetch('/webauthn/authenticate/begin', {
511
method: 'POST',
512
headers: {'Content-Type': 'application/json'},
513
});
514
const options = await optionsResponse.json();
515
516
// Convert challenge to ArrayBuffer
517
options.challenge = base64ToArrayBuffer(options.challenge);
518
519
// Get assertion
520
const assertion = await navigator.credentials.get({
521
publicKey: options
522
});
523
524
// Send assertion to server
525
const authResponse = await fetch('/webauthn/authenticate/complete', {
526
method: 'POST',
527
headers: {'Content-Type': 'application/json'},
528
body: JSON.stringify({
529
id: assertion.id,
530
rawId: arrayBufferToBase64(assertion.rawId),
531
response: {
532
clientDataJSON: arrayBufferToBase64(assertion.response.clientDataJSON),
533
authenticatorData: arrayBufferToBase64(assertion.response.authenticatorData),
534
signature: arrayBufferToBase64(assertion.response.signature),
535
userHandle: assertion.response.userHandle ?
536
arrayBufferToBase64(assertion.response.userHandle) : null
537
}
538
})
539
});
540
541
const result = await authResponse.json();
542
if (result.success) {
543
window.location.href = result.redirect || '/dashboard';
544
} else {
545
alert('Authentication failed: ' + result.error);
546
}
547
548
} catch (error) {
549
console.error('WebAuthn authentication error:', error);
550
alert('WebAuthn authentication failed');
551
}
552
}
553
554
// Utility functions for base64/ArrayBuffer conversion
555
function base64ToArrayBuffer(base64) {
556
const binaryString = atob(base64);
557
const bytes = new Uint8Array(binaryString.length);
558
for (let i = 0; i < binaryString.length; i++) {
559
bytes[i] = binaryString.charCodeAt(i);
560
}
561
return bytes.buffer;
562
}
563
564
function arrayBufferToBase64(buffer) {
565
const bytes = new Uint8Array(buffer);
566
let binary = '';
567
for (let i = 0; i < bytes.byteLength; i++) {
568
binary += String.fromCharCode(bytes[i]);
569
}
570
return btoa(binary);
571
}
572
```
573
574
## Security Considerations
575
576
### Relying Party Configuration
577
- Set `SECURITY_WEBAUTHN_RP_ID` to your domain name for proper origin validation
578
- Use HTTPS in production - WebAuthn requires secure contexts
579
- Configure appropriate `SECURITY_WEBAUTHN_RP_NAME` for user recognition
580
581
### Credential Storage
582
- Store credential public keys securely in your database
583
- Never store private keys - they remain on the authenticator device
584
- Implement proper credential ID uniqueness constraints
585
586
### Challenge Management
587
- Use cryptographically secure random challenges
588
- Implement appropriate challenge timeouts (default: 5 minutes)
589
- Store challenges server-side to prevent replay attacks
590
591
### User Verification
592
- Configure appropriate user verification requirements based on security needs
593
- Consider authenticator attachment preferences for your use case
594
- Implement fallback authentication methods for accessibility
595
596
## Browser Support
597
598
WebAuthn is supported in modern browsers:
599
- Chrome 67+
600
- Firefox 60+
601
- Safari 14+
602
- Edge 18+
603
604
Consider implementing feature detection and fallback authentication methods for broader compatibility.