User Authentication Flow - ExpandedUser Authentication Flow - ExpandedHandler LayerInfrastructure LayerRepository / Cross-sliceAPIAPIMediatR pipelineMediatR pipelineMediatR pipelineRegisterUserHandlerLoginHandlerTokenServiceTokenServiceTokenServiceTokenServiceTokenServiceArgon2Argon2JWTJWTJWTJWTJWTEmailTemplateAuthRepositoryAuthRepositoryAuthRepositoryAuthRepositoryAuthRepositoryEmailDispatcherSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerUserAPIMediatR pipelineRegisterUserHandlerLoginHandlerTokenServiceArgon2JWTEmailTemplateAuthRepositoryEmailDispatcherSQL ServerUserUserAPIControllerAPIControllerMediatR pipeline(ValidationBehavior)MediatR pipeline(ValidationBehavior)RegisterUserHandlerRegisterUserHandlerLoginHandlerLoginHandlerTokenServiceTokenServiceArgon2InfrastructureArgon2InfrastructureJWTInfrastructureJWTInfrastructureEmailProviderEmailProviderTemplateProviderTemplateProviderAuthRepositoryAuthRepositoryEmailDispatcher(Features.Emails)EmailDispatcher(Features.Emails)SQL ServerSQL ServerAPIAPIMediatR pipelineMediatR pipelineMediatR pipelineRegisterUserHandlerLoginHandlerTokenServiceTokenServiceTokenServiceTokenServiceTokenServiceArgon2Argon2JWTJWTJWTJWTJWTEmailTemplateAuthRepositoryAuthRepositoryAuthRepositoryAuthRepositoryAuthRepositoryEmailDispatcherSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerRegistration FlowPOST /api/auth/register{username, firstName, lastName,email, dateOfBirth, password}Send(RegisterUserCommand)ValidationBehavior runs RegisterUserValidator(FluentValidation) before the handler:- Username: 3-64 chars, alphanumeric + [._-]- Email: valid format, max 128 chars- Password: min 8 chars, uppercase,\n lowercase, number, special char- DateOfBirth: must be 19+ years oldalt[Validation fails]throwsFluentValidation.ValidationException400 Bad Request{message: "Validation failed",errors: {...}}[Validation succeeds]Handle(command)GetUserByUsernameAsync(username)SELECT UserAccountID,Username, ...FROM dbo.UserAccountWHERE Username = @Usernamenull (user doesn't exist)GetUserByEmailAsync(email)SELECT UserAccountID,Username, ...FROM dbo.UserAccountWHERE Email = @Emailnull (email doesn't exist)alt[Username or email already exists]throw ConflictExceptionpropagate409 Conflict"Username or email alreadyexists"[Username and email are free]Hash(password)Argon2id parameters:- Salt: 16 bytes (128-bit)- Memory: 64MB- Iterations: 4- Parallelism: CPU count- Hash output: 32 bytesGenerate random salt(16 bytes)Hash password withArgon2id algorithm"base64(salt):base64(hash)"RegisterUserAsync(userAccount)BEGIN TRANSACTIONINSERT INTO dbo.UserAccount(Username, FirstName,LastName,DateOfBirth, Email)OUTPUTINSERTED.UserAccountIDUserAccountId (GUID)INSERT INTO dbo.UserCredential(UserAccountId, Hash)COMMIT TRANSACTIONAll three statements run via Dapper'sExecuteScalarAsync/ExecuteAsync againsta single DbTransaction, not a storedprocedure - no manual parameter bindingSELECT UserAccountID,Username, ...FROM dbo.UserAccountWHERE UserAccountID =@UserAccountIdRound trip to load the row theINSERT just createdUserAccount rowUserAccount entityGenerateAccessToken(user)GenerateJwt(userId, username,expiry)JWT Configuration:- Algorithm: HS256- Expires: 1 hour- Claims:* sub: userId* unique_name: username* iat / jtiCreate JWT with claimsSign with secret keyAccess TokenAccess TokenGenerateRefreshToken(user)GenerateJwt(userId, username,expiry)Refresh Token:- Expires: 21 days (504 hours)- Same structure as access token,\n signed with a separate secretRefresh TokenRefresh TokenGenerateConfirmationToken(user)GenerateJwt(userId, username,expiry)Confirmation Token:- Expires: 30 minutes (0.5 hours)- Signed with a third, separate secretConfirmation TokenConfirmation TokenSend(SendRegistrationEmailCommand)(Shared.Application cross-slicecontract)SendRegistrationEmailAsync( firstName, email,confirmationToken)Routed to Features.Emails'SendRegistrationEmailHandler,the only cross-slice call inthe backendRenderUserRegisteredEmailAsync( firstName, confirmationLink)Razor Component:- Header with branding- Welcome message- Confirmation button- FooterRender Razor componentto HTMLHTML email contentSendAsync(email, subject, body)SMTP Configuration (MailKit):- Host/port: from env (SMTP_HOST/SMTP_PORT)- TLS: StartTLS when SMTP_USE_SSL=true- Auth: username/password, if configuredBuild MIME messageConnect, authenticate,send, disconnectSuccess / throwsalt[Email sent successfully]emailSent = true[mediator.Send throws]catch(Exception),log to stderr,emailSent = falseRegistrationPayload( userAccountId, username, refreshToken, accessToken, confirmationEmailSent)RegistrationPayload201 Created{ message: "User registeredsuccessfully.", payload: { userAccountId, username, refreshToken, accessToken, confirmationEmailSent }}Login FlowPOST /api/auth/login{username, password}Send(LoginCommand)ValidationBehavior runs LoginValidator:username and password must both be non-emptyalt[Validation fails]throwsFluentValidation.ValidationException400 Bad Request{message: "Validation failed",errors: {...}}[Validation succeeds]Handle(command)GetUserByUsernameAsync(username)SELECT UserAccountID,Username, ...FROM dbo.UserAccountWHERE Username = @UsernameUserAccount row or nullalt[User not found]throw UnauthorizedException"Invalid username or password."propagate401 Unauthorized[User found]GetActiveCredentialByUserAccountIdAsync(userId)SELECT UserCredentialId,UserAccountId,Hash, CreatedAt, RowVersionFROM dbo.UserCredentialWHERE UserAccountId =@UserAccountId AND IsRevoked = 0UserCredential row or nullalt[No active credential]throw UnauthorizedExceptionpropagate401 Unauthorized[Active credential found]Verify(password, storedHash)1. Split stored hash: "salt:hash"2. Extract salt3. Hash provided password\n with same salt4. Constant-time comparison\n (CryptographicOperations.FixedTimeEquals)Parse salt from stored hashHash provided passwordwith extracted saltFixedTimeEquals( computed, stored)true/falsealt[Password invalid]throw UnauthorizedExceptionpropagate401 Unauthorized[Password valid]GenerateAccessToken(user)GenerateJwt(...)Access TokenAccess TokenGenerateRefreshToken(user)GenerateJwt(...)Refresh TokenRefresh TokenLoginPayload( userAccountId, username, refreshToken, accessToken)LoginPayload200 OK{ message: "Logged insuccessfully.", payload: { userAccountId, username, refreshToken, accessToken }}Error Handling (Global Exception Filter)API.Core's GlobalExceptionFilter catches:- FluentValidation.ValidationException -> 400 Bad Request\n {message, errors} (grouped by property)- ConflictException -> 409 Conflict- NotFoundException -> 404 Not Found- UnauthorizedException -> 401 Unauthorized- ForbiddenException -> 403 Forbidden- Domain.Exceptions.ValidationException -> 400 Bad Request- Microsoft.Data.SqlClient.SqlException -> 503 Service Unavailable- All others -> 500 Internal Server Error