functional totp
This commit is contained in:
@@ -19,10 +19,10 @@ class AuthenticationMethod(BaseModel):
|
||||
provider_user_id = db.Column(db.String(255), nullable=True)
|
||||
provider_data = db.Column(db.JSON, nullable=True)
|
||||
|
||||
# # For TOTP authentication
|
||||
# totp_secret = db.Column(db.String(32), nullable=True)
|
||||
# totp_backup_codes = db.Column(db.JSON, nullable=True)
|
||||
# totp_verified_at = db.Column(db.DateTime, nullable=True)
|
||||
# For TOTP authentication
|
||||
totp_secret = db.Column(db.String(32), nullable=True)
|
||||
totp_backup_codes = db.Column(db.JSON, nullable=True)
|
||||
totp_verified_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Metadata
|
||||
is_primary = db.Column(db.Boolean, default=False, nullable=False)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""OIDC JWKS Key model for persisting signing keys."""
|
||||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class OidcJwksKey(BaseModel):
|
||||
"""
|
||||
OIDC JWKS Key model for persisting JSON Web Key Set signing keys.
|
||||
|
||||
This model stores RSA/ECDSA key pairs used for signing OIDC tokens.
|
||||
Multiple keys can be stored to support key rotation scenarios.
|
||||
|
||||
Attributes:
|
||||
id: Integer primary key
|
||||
kid: Unique key ID used in JWT "kid" header
|
||||
key_type: Type of key (e.g., "RSA", "EC")
|
||||
private_key: PEM-encoded private key
|
||||
public_key: PEM-encoded public key
|
||||
algorithm: Signing algorithm (e.g., "RS256", "ES256")
|
||||
created_at: When the key was created
|
||||
is_active: Whether this key is currently active for signing
|
||||
is_primary: Whether this is the primary signing key
|
||||
expires_at: ...
|
||||
"""
|
||||
|
||||
__tablename__ = "oidc_jwks_keys"
|
||||
|
||||
# Override the default UUID id with integer primary key
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
expires_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Key identification and type
|
||||
kid = db.Column(db.String(255), unique=True, nullable=False, index=True)
|
||||
key_type = db.Column(db.String(50), nullable=False) # e.g., "RSA", "EC"
|
||||
algorithm = db.Column(db.String(50), nullable=False) # e.g., "RS256", "ES256"
|
||||
|
||||
# Key material (PEM-encoded)
|
||||
private_key = db.Column(db.Text, nullable=False)
|
||||
public_key = db.Column(db.Text, nullable=False)
|
||||
|
||||
# Key status
|
||||
is_active = db.Column(db.Boolean, default=True, nullable=False)
|
||||
is_primary = db.Column(db.Boolean, default=False, nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of OidcJwksKey."""
|
||||
return f"<OidcJwksKey kid={self.kid} key_type={self.key_type} algorithm={self.algorithm}>"
|
||||
|
||||
def to_dict(self, exclude_private_key=True):
|
||||
"""
|
||||
Convert model to dictionary.
|
||||
|
||||
Args:
|
||||
exclude_private_key: If True, excludes the private key from output
|
||||
|
||||
Returns:
|
||||
Dictionary representation of the model
|
||||
"""
|
||||
exclude = ["private_key"] if exclude_private_key else []
|
||||
return super().to_dict(exclude=exclude)
|
||||
|
||||
@classmethod
|
||||
def get_active_keys(cls):
|
||||
"""Get all active keys for signing operations."""
|
||||
return cls.query.filter(cls.is_active == True).all()
|
||||
|
||||
@classmethod
|
||||
def get_primary_key(cls):
|
||||
"""Get the primary signing key."""
|
||||
return cls.query.filter(cls.is_primary == True).first()
|
||||
|
||||
@classmethod
|
||||
def get_key_by_kid(cls, kid):
|
||||
"""Get a key by its key ID."""
|
||||
return cls.query.filter(cls.kid == kid, cls.is_active == True).first()
|
||||
+12
-3
@@ -21,7 +21,7 @@ class Session(BaseModel):
|
||||
|
||||
# Timing
|
||||
expires_at = db.Column(db.DateTime, nullable=False)
|
||||
last_activity_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
last_activity_at = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
revoked_at = db.Column(db.DateTime, nullable=True)
|
||||
revoked_reason = db.Column(db.String(255), nullable=True)
|
||||
|
||||
@@ -35,15 +35,24 @@ class Session(BaseModel):
|
||||
def is_active(self):
|
||||
"""Check if session is currently active."""
|
||||
now = datetime.now(timezone.utc)
|
||||
# Make expires_at timezone-aware if it's naive
|
||||
expires_at = self.expires_at
|
||||
if expires_at.tzinfo is None:
|
||||
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
||||
return (
|
||||
self.status == SessionStatus.ACTIVE
|
||||
and self.expires_at > now
|
||||
and expires_at > now
|
||||
and self.deleted_at is None
|
||||
)
|
||||
|
||||
def is_expired(self):
|
||||
"""Check if session has expired."""
|
||||
return datetime.now(timezone.utc) > self.expires_at
|
||||
now = datetime.now(timezone.utc)
|
||||
# Make expires_at timezone-aware if it's naive
|
||||
expires_at = self.expires_at
|
||||
if expires_at.tzinfo is None:
|
||||
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
||||
return now > expires_at
|
||||
|
||||
def refresh(self, duration_seconds=86400):
|
||||
"""
|
||||
|
||||
+5
-1
@@ -84,10 +84,14 @@ class User(BaseModel):
|
||||
|
||||
Returns:
|
||||
The AuthenticationMethod instance for TOTP or None if not found.
|
||||
|
||||
Note:
|
||||
Returns the most recently created TOTP method to handle cases where
|
||||
multiple enrollment attempts may exist.
|
||||
"""
|
||||
from app.models.authentication_method import AuthenticationMethod
|
||||
from app.utils.constants import AuthMethodType
|
||||
|
||||
return AuthenticationMethod.query.filter_by(
|
||||
user_id=self.id, method_type=AuthMethodType.TOTP, deleted_at=None
|
||||
).first()
|
||||
).order_by(AuthenticationMethod.created_at.desc()).first()
|
||||
|
||||
Reference in New Issue
Block a user