Add in a mechanism for email confirmation
This is some of the way there but it is still missing at least: - An actual page to view to confirm the email - An expiry time on the minted JWT (and other JWT issues like "sub")
This commit is contained in:
parent
ecb98dac53
commit
782e0b0ad6
8 changed files with 133 additions and 20 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import abc
|
||||
from src.config.email import EmailAddress
|
||||
from src.config.parse import assert_key_of_type, ParseError
|
||||
from typing import Any
|
||||
from typing import Any, TypeVar
|
||||
from dataclasses import dataclass, asdict
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
import jwt
|
||||
|
|
@ -9,19 +11,53 @@ from src.config.auth import Auth as AuthConfig
|
|||
|
||||
JWT = str
|
||||
|
||||
T = TypeVar('T', bound='Claim')
|
||||
|
||||
|
||||
# TODO: Do these claims need explicit exp attributes? I think the ID fields should actually be sub now that I think
|
||||
# about it...
|
||||
@dataclass
|
||||
class Claim(abc.ABC):
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def from_dict(cls, claims: dict[str, Any]) -> T:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def from_user(cls, user: User) -> T:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Claims:
|
||||
class UserClaims(Claim):
|
||||
id: int
|
||||
|
||||
@classmethod
|
||||
def from_user(cls, user: User) -> 'Claims':
|
||||
return Claims(id=user.id)
|
||||
def from_user(cls, user: User) -> 'UserClaims':
|
||||
return UserClaims(id=user.id)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, claims: dict[str, Any]) -> 'Claims':
|
||||
def from_dict(cls, claims: dict[str, Any]) -> 'UserClaims':
|
||||
assert_key_of_type(claims, 'id', int)
|
||||
return Claims(id=claims['id'])
|
||||
return UserClaims(id=claims['id'])
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmailConfirmationClaim(Claim):
|
||||
id: int
|
||||
email: EmailAddress
|
||||
|
||||
@classmethod
|
||||
def from_user(cls, user: User) -> 'EmailConfirmationClaim':
|
||||
return EmailConfirmationClaim(id=user.id, email=user.email)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, claims: dict[str, Any]) -> 'EmailConfirmationClaim':
|
||||
assert_key_of_type(claims, 'id', int)
|
||||
assert_key_of_type(claims, 'email', str)
|
||||
|
||||
return EmailConfirmationClaim(id=claims['id'], email=claims['email'])
|
||||
|
||||
|
||||
class AuthService:
|
||||
|
|
@ -29,17 +65,17 @@ class AuthService:
|
|||
self._private_key = Ed25519PrivateKey.from_private_bytes(config.ed25519_private_key.expose_secret())
|
||||
self._public_key = self._private_key.public_key()
|
||||
|
||||
def mint_jwt(self, user: User) -> JWT:
|
||||
claims = Claims.from_user(user)
|
||||
def mint_claim_from_user(self, claim: type[Claim], user: User) -> JWT:
|
||||
claims = claim.from_user(user)
|
||||
return jwt.encode(asdict(claims), self._private_key, algorithm='EdDSA')
|
||||
|
||||
def validate_token(self, token: JWT) -> Claims | None:
|
||||
def validate_token[T: Claim](self, claim: type[T], token: JWT) -> T | None:
|
||||
try:
|
||||
claims = jwt.decode(token, key=self._public_key, algorithms=['EdDSA'])
|
||||
except jwt.InvalidTokenError:
|
||||
return None
|
||||
|
||||
try:
|
||||
return Claims.from_dict(claims)
|
||||
return claim.from_dict(claims)
|
||||
except ParseError:
|
||||
return None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue