Enable users to sign up and sign in using passkeys (biometrics/security keys) without needing a manually-created Authentik account. The existing SSO login remains as an alternative. Backend: - Add WebAuthn registration/authentication endpoints via py-webauthn - Issue HS256 JWTs for passkey users, with Redis-backed challenge storage - Dual JWT verification in auth middleware (issuer-based routing: passkey HS256 vs Authentik RS256) - PasskeyCredential model + migration making user.password nullable - UserRepository with full CRUD for users and credentials Frontend: - AuthUser type abstraction unifying OIDC and passkey users - Passkey service using @simplewebauthn/browser for WebAuthn ceremonies - LoginModal redesigned with Sign In / Sign Up tabs - Type migration from oidc-client-ts User to AuthUser across all services and components
62 lines
2.3 KiB
Python
62 lines
2.3 KiB
Python
from models.passkey_credential import PasskeyCredential
|
|
from models.user import User
|
|
from sqlalchemy import Engine
|
|
from sqlmodel import Session, select
|
|
|
|
|
|
class UserRepository:
|
|
engine: Engine
|
|
|
|
def __init__(self, engine: Engine) -> None:
|
|
self.engine = engine
|
|
|
|
def get_user_by_email(self, email: str) -> User | None:
|
|
with Session(self.engine) as session:
|
|
statement = select(User).where(User.email == email)
|
|
return session.exec(statement).first()
|
|
|
|
def get_user_by_id(self, user_id: int) -> User | None:
|
|
with Session(self.engine) as session:
|
|
return session.get(User, user_id)
|
|
|
|
def create_user(self, email: str) -> User:
|
|
with Session(self.engine) as session:
|
|
user = User(email=email) # type: ignore[call-arg]
|
|
session.add(user)
|
|
session.commit()
|
|
session.refresh(user)
|
|
return user
|
|
|
|
def get_credentials_for_user(self, user_id: int) -> list[PasskeyCredential]:
|
|
with Session(self.engine) as session:
|
|
statement = select(PasskeyCredential).where(
|
|
PasskeyCredential.user_id == user_id
|
|
)
|
|
return list(session.exec(statement).all())
|
|
|
|
def get_credential_by_id(self, credential_id: str) -> PasskeyCredential | None:
|
|
with Session(self.engine) as session:
|
|
statement = select(PasskeyCredential).where(
|
|
PasskeyCredential.credential_id == credential_id
|
|
)
|
|
return session.exec(statement).first()
|
|
|
|
def save_credential(self, credential: PasskeyCredential) -> PasskeyCredential:
|
|
with Session(self.engine) as session:
|
|
session.add(credential)
|
|
session.commit()
|
|
session.refresh(credential)
|
|
return credential
|
|
|
|
def update_credential_sign_count(
|
|
self, credential_id: str, new_sign_count: int
|
|
) -> None:
|
|
with Session(self.engine) as session:
|
|
statement = select(PasskeyCredential).where(
|
|
PasskeyCredential.credential_id == credential_id
|
|
)
|
|
credential = session.exec(statement).first()
|
|
if credential:
|
|
credential.sign_count = new_sign_count
|
|
session.add(credential)
|
|
session.commit()
|