mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
- Add webauthn dependency. - Add PasskeyCredential model and Alembic migration 019_passkey_credentials. - Add PasskeyService for registration/authentication challenge flow. - Add auth API endpoints: - POST /auth/passkeys/options/register - POST /auth/passkeys/register - GET /auth/passkeys - DELETE /auth/passkeys/<id> - POST /auth/passkeys/options/authenticate - POST /auth/passkeys/authenticate - Expose passkey_enabled flag in User.to_dict().
55 lines
1.9 KiB
Python
55 lines
1.9 KiB
Python
import json
|
|
import base64
|
|
from datetime import datetime
|
|
from app import db
|
|
|
|
|
|
def _b64decode_url(value: str) -> bytes:
|
|
"""URL-safe base64 decode with padding fix."""
|
|
if not value:
|
|
return b''
|
|
padding = 4 - len(value) % 4
|
|
if padding != 4:
|
|
value += '=' * padding
|
|
return base64.urlsafe_b64decode(value)
|
|
|
|
|
|
class PasskeyCredential(db.Model):
|
|
"""Stored WebAuthn credential for a user."""
|
|
__tablename__ = 'passkey_credentials'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
|
|
credential_id = db.Column(db.String(500), nullable=False, unique=True, index=True)
|
|
public_key = db.Column(db.Text, nullable=False) # base64url-encoded COSE key
|
|
sign_count = db.Column(db.Integer, default=0)
|
|
transports = db.Column(db.Text, nullable=True) # JSON list
|
|
device_name = db.Column(db.String(200), nullable=True)
|
|
is_active = db.Column(db.Boolean, default=True)
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
last_used_at = db.Column(db.DateTime, nullable=True)
|
|
|
|
user = db.relationship('User', backref=db.backref('passkeys', lazy='dynamic'))
|
|
|
|
def get_transports(self):
|
|
if not self.transports:
|
|
return []
|
|
try:
|
|
return json.loads(self.transports)
|
|
except Exception:
|
|
return []
|
|
|
|
def set_transports(self, transports):
|
|
self.transports = json.dumps(list(transports)) if transports else None
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'id': self.id,
|
|
'credential_id': self.credential_id,
|
|
'device_name': self.device_name,
|
|
'transports': self.get_transports(),
|
|
'is_active': self.is_active,
|
|
'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,
|
|
}
|