05/18 Enhanced codes and functionalities 5
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
app/models/webauthn_credential.py — WebAuthn (passkey) credential storage.
|
||||
|
||||
Each user can register multiple passkeys (phone, laptop, YubiKey, etc.).
|
||||
Credentials are used for server authentication only — the vault key is
|
||||
still derived from the master password client-side (zero-knowledge preserved).
|
||||
|
||||
Zero-knowledge note:
|
||||
WebAuthn replaces TOTP as a second factor OR replaces the password-based
|
||||
server auth (passwordless flow). In both cases the master password is still
|
||||
required client-side to derive the vault key.
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy.dialects.mysql import INTEGER
|
||||
|
||||
from app import db
|
||||
|
||||
|
||||
class WebAuthnCredential(db.Model):
|
||||
__tablename__ = 'webauthn_credentials'
|
||||
|
||||
id = db.Column(INTEGER(unsigned=True), autoincrement=True, primary_key=True)
|
||||
user_id = db.Column(
|
||||
INTEGER(unsigned=True),
|
||||
db.ForeignKey('users.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
# Base64url-encoded credential ID from the authenticator.
|
||||
credential_id = db.Column(db.String(512), nullable=False, unique=True)
|
||||
# COSE-encoded public key bytes, base64url.
|
||||
public_key = db.Column(db.Text, nullable=False)
|
||||
# Monotonically increasing counter — used to detect cloned authenticators.
|
||||
sign_count = db.Column(db.BigInteger, nullable=False, default=0)
|
||||
# JSON list of transport hints e.g. '["internal", "hybrid"]'
|
||||
transports = db.Column(db.String(255), nullable=True)
|
||||
# Authenticator AAGUID (UUID string) from attestation.
|
||||
aaguid = db.Column(db.String(64), nullable=True)
|
||||
# User-assigned friendly name shown in the UI.
|
||||
name = db.Column(db.String(128), nullable=False, default='Passkey')
|
||||
created_at = db.Column(
|
||||
db.DateTime,
|
||||
nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc).replace(tzinfo=None),
|
||||
)
|
||||
last_used_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
def get_transports(self) -> list:
|
||||
"""Return transport hints as a Python list (empty list if none)."""
|
||||
if not self.transports:
|
||||
return []
|
||||
try:
|
||||
return json.loads(self.transports)
|
||||
except (ValueError, TypeError):
|
||||
return []
|
||||
|
||||
def set_transports(self, transports: list) -> None:
|
||||
self.transports = json.dumps(transports) if transports else None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'id': self.id,
|
||||
'credential_id': self.credential_id,
|
||||
'name': self.name,
|
||||
'aaguid': self.aaguid,
|
||||
'transports': self.get_transports(),
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'last_used_at': self.last_used_at.isoformat() if self.last_used_at else None,
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<WebAuthnCredential user_id={self.user_id} name={self.name!r}>'
|
||||
Reference in New Issue
Block a user