Email Verification
Production - POST /api/v1/auth/verify-email, POST /api/v1/auth/resend-verification
Email verification confirms that a user owns the email address they registered with. A verification code is sent via the Notification Service and must be submitted to complete verification.
6.2.7Verification Flow
User registers --> Verification email sent --> User submits code --> Email verified
(6-digit code, 30min TTL) POST /verify-email emailVerified=trueVerify Email
curl -X POST http://localhost:8081/api/v1/auth/verify-email \
-H "Content-Type: application/json" \
-d '{
"email": "newuser@example.com",
"code": "482917"
}'Request Schema
| Field | Type | Required | Description |
|---|---|---|---|
email | String | Yes | The email address to verify |
code | String | Yes | The verification code from the email |
Response (200 OK)
Empty response body on success.
6.2.8Resend Verification
If the user did not receive the verification email or the code expired, they can request a new one:
curl -X POST http://localhost:8081/api/v1/auth/resend-verification \
-H "Content-Type: application/json" \
-d '{
"email": "newuser@example.com"
}'Rate Limiting
Resend requests are rate-limited to one per minute per user. Attempting to resend before the cooldown period returns an error.
6.2.9Implementation Details
Token Creation
When a verification email is sent, an EmailVerificationToken entity is created:
public void sendVerificationEmail(User user) {
// Invalidate any existing tokens
emailVerificationTokenRepository.invalidateAllForUser(user.getId(), Instant.now());
// Create new verification token (30-minute validity)
EmailVerificationToken token = EmailVerificationToken.createForUser(
user, EMAIL_VERIFICATION_VALIDITY_MINUTES);
emailVerificationTokenRepository.save(token);
// Send via Notification Service
notificationServiceClient.sendEmailWithTemplate(
user.getTenantId(),
null,
user.getEmail(),
"email-verification",
Map.of(
"userName", user.getFirstName() != null ? user.getFirstName() : "User",
"verificationCode", token.getToken(),
"validityMinutes", EMAIL_VERIFICATION_VALIDITY_MINUTES,
"verificationUrl", "https://app.matih.ai/verify-email?code=" + token.getToken()
)
);
}Verification Logic
@Transactional
public void verifyEmail(VerifyEmailRequest request) {
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new BusinessException("User not found"));
if (user.isEmailVerified()) {
return; // Already verified - idempotent
}
EmailVerificationToken token = emailVerificationTokenRepository
.findValidTokenByCodeAndEmail(request.getCode(), request.getEmail(), Instant.now())
.orElseThrow(() -> new BusinessException("Invalid or expired verification code"));
if (token.tooManyAttempts()) {
throw new BusinessException(
"Too many failed attempts. Please request a new verification code.");
}
if (!token.getToken().equals(request.getCode())) {
token.incrementAttempts();
emailVerificationTokenRepository.save(token);
throw new BusinessException("Invalid verification code");
}
// Mark token as used
token.markAsUsed();
emailVerificationTokenRepository.save(token);
// Mark user email as verified
user.setEmailVerified(true);
userRepository.save(user);
}Key Behaviors
- Idempotent: Verifying an already-verified email is a no-op (returns success)
- Attempt Tracking: Failed verification attempts are counted. After too many failures, the token is invalidated
- Token Invalidation: When a new verification is requested, all existing tokens for the user are invalidated
- Fallback Logging: If the Notification Service is unavailable, the verification code is logged for development environments
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
BUSINESS_RULE_VIOLATION | 400 | Invalid/expired code, email already verified, rate limited |
RESOURCE_NOT_FOUND | 404 | User not found |