-
Notifications
You must be signed in to change notification settings - Fork 2
Email verification workflow with rate limiting #460
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
944690d
feat(auth): add email verification workflow with rate limiting
yinanazhou e56019f
chore: rename `**/registration/**` to `**/auth/**`
yinanazhou db7f250
test: update auth tests for registration workflow changes
yinanazhou bc1cb80
fix: validate next parameter to prevent open redirect attacks
yinanazhou 806c686
fix: check for unverified accounts before form validation
yinanazhou e70313f
refactor: reorder rate limit check for email resending
yinanazhou 333d8dc
fix: add redirect authenticated users to custom login
yinanazhou e49d733
test: increase timeout in CI
yinanazhou 8717cec
test: use `html[lang]` instead of cookies in GT E2E test
yinanazhou dbcd878
refactor: make resend verification cooldown atomic with cache.add()
yinanazhou 97e4558
fix: clear stale pending verification session for unverified account
yinanazhou 8f57cff
test: disable GT script loading for most E2E tests to prevent 429
yinanazhou 99eb613
fix: set cooldown on expiration to prevent race condition
yinanazhou d3742b3
fix: verify password for existing unverified accounts when register
yinanazhou File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| """Rate limiting utilities for email operations.""" | ||
|
|
||
| import hashlib | ||
| import time | ||
| from django.conf import settings | ||
| from django.core.cache import cache | ||
|
|
||
|
|
||
| def get_email_cooldown_remaining(email): | ||
| """ | ||
| Return remaining cooldown seconds for an email (0 if none). | ||
|
|
||
| Args: | ||
| email: Email address to check | ||
|
|
||
| Returns: | ||
| int: Remaining seconds (0 if no cooldown active) | ||
| """ | ||
| email_hash = hashlib.sha256(email.lower().encode()).hexdigest() | ||
| cache_key = f"email_resend_cooldown:{email_hash}" | ||
| expiration_time = cache.get(cache_key) | ||
|
|
||
| if expiration_time: | ||
| remaining = int(expiration_time - time.time()) | ||
| return max(0, remaining) | ||
| return 0 | ||
|
|
||
|
|
||
| def check_email_resend_cooldown(email): | ||
| """ | ||
| Check if email resend is allowed and return remaining cooldown time. | ||
|
|
||
| Uses atomic cache.add() (set-if-not-exists) to prevent TOCTOU races. | ||
|
|
||
| Args: | ||
| email: Email address to check cooldown for | ||
|
|
||
| Returns: | ||
| tuple: (allowed: bool, remaining_seconds: int) | ||
| """ | ||
| email_hash = hashlib.sha256(email.lower().encode()).hexdigest() | ||
| cache_key = f"email_resend_cooldown:{email_hash}" | ||
| current_time = time.time() | ||
| expiration_time = current_time + settings.RESEND_EMAIL_COOLDOWN | ||
|
|
||
| # Atomically start cooldown only if not already set. | ||
| added = cache.add( | ||
| cache_key, expiration_time, timeout=settings.RESEND_EMAIL_COOLDOWN | ||
| ) | ||
|
|
||
| if added: | ||
| return True, 0 | ||
|
|
||
| # Cooldown already active; compute remaining time. | ||
| existing_expiration = cache.get(cache_key) | ||
| if existing_expiration: | ||
| remaining = int(existing_expiration - current_time) | ||
| if remaining > 0: | ||
| return False, remaining | ||
| # Cooldown expired but key still present (backend quirk). | ||
| # Set new cooldown to prevent race condition with concurrent requests. | ||
| cache.set(cache_key, expiration_time, timeout=settings.RESEND_EMAIL_COOLDOWN) | ||
| return True, 0 | ||
|
|
||
| # Key disappeared between add() and get(). | ||
| # Set new cooldown to prevent race condition with concurrent requests. | ||
| cache.set(cache_key, expiration_time, timeout=settings.RESEND_EMAIL_COOLDOWN) | ||
| return True, 0 | ||
|
cursor[bot] marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| """Session utilities for user verification workflow.""" | ||
|
|
||
| from django.utils import timezone | ||
| from django.conf import settings | ||
|
|
||
|
|
||
| def set_pending_verification_email(request, email): | ||
| """Store email in session with timestamp for expiry tracking.""" | ||
| request.session["pending_verification_email"] = email | ||
| request.session["pending_verification_timestamp"] = timezone.now().timestamp() | ||
|
|
||
|
|
||
| def clear_pending_verification_email(request): | ||
| """Clear pending verification email and timestamp from session.""" | ||
| request.session.pop("pending_verification_email", None) | ||
| request.session.pop("pending_verification_timestamp", None) | ||
|
|
||
|
|
||
| def get_pending_verification_email(request): | ||
| """ | ||
| Get pending verification email if not expired. | ||
|
|
||
| Returns: | ||
| str or None: Email address if valid and not expired, None otherwise | ||
| """ | ||
| email = request.session.get("pending_verification_email") | ||
| timestamp = request.session.get("pending_verification_timestamp", 0) | ||
|
|
||
| # Check if email exists and is not expired | ||
| if ( | ||
| not email | ||
| or timezone.now().timestamp() - timestamp | ||
| > settings.PENDING_EMAIL_SESSION_EXPIRY | ||
| ): | ||
| return None | ||
|
|
||
| return email |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.