aboutsummaryrefslogtreecommitdiff
path: root/jb/api/magic_token.py
diff options
context:
space:
mode:
Diffstat (limited to 'jb/api/magic_token.py')
-rw-r--r--jb/api/magic_token.py41
1 files changed, 41 insertions, 0 deletions
diff --git a/jb/api/magic_token.py b/jb/api/magic_token.py
new file mode 100644
index 0000000..136e1b4
--- /dev/null
+++ b/jb/api/magic_token.py
@@ -0,0 +1,41 @@
+import hashlib
+import secrets
+
+from fastapi import HTTPException, status
+
+from jb.decorators import REDIS
+
+MAGIC_TOKEN_PREFIX = "auth:magic:"
+MAGIC_TOKEN_TTL: int = 5 * 60 # 5 minutes, in seconds
+
+
+def redis_token_key(token: str) -> str:
+ # Redis never contains a usable credential, even if its keys are exposed.
+ digest = hashlib.sha256(token.encode("utf-8")).hexdigest()
+ return f"{MAGIC_TOKEN_PREFIX}{digest}"
+
+
+def create_magic_token(user_email: str) -> str:
+ """Create a short-lived, single-use token for a user.
+ The raw token can then be sent by email.
+ """
+ if not user_email or not user_email.strip():
+ raise ValueError("user_email must not be empty")
+
+ token = secrets.token_urlsafe(32)
+ REDIS.set(
+ redis_token_key(token),
+ user_email,
+ ex=MAGIC_TOKEN_TTL,
+ )
+ return token
+
+
+def consume_magic_token(token: str) -> str:
+ user_email = REDIS.getdel(redis_token_key(token))
+ if user_email is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid or expired magic token",
+ )
+ return user_email