User Authentication Flow - ExpandedUser Authentication Flow - ExpandedHandler LayerInfrastructure LayerRepository / Cross-sliceAPIAPIAPIAPIAPIAPIAPIAPIAPIAPIMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineRegisterUserHandlerLoginHandlerRefreshTokenHandlerConfirmUserHandlerResendConfirmationEmailHandlerUpdateUsernameHandlerUpdateEmailHandlerUpdatePasswordHandlerUpdateProfileHandlerDeleteAccountHandlerUserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.TokenServiceTokenServiceTokenServiceTokenServiceTokenServiceTokenServiceTokenServiceTokenServiceArgon2PasswordHasherArgon2PasswordHasherArgon2PasswordHasherArgon2PasswordHasherJWTJWTJWTJWTJWTJWTJWTEmailTemplateDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreEmailDispatcherEmailDispatcherSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerUserAPIMediatR pipelineRegisterUserHandlerLoginHandlerRefreshTokenHandlerConfirmUserHandlerResendConfirmationEmailHandlerUpdateUsernameHandlerUpdateEmailHandlerUpdatePasswordHandlerUpdateProfileHandlerDeleteAccountHandlerUserManager.ApplicationUser.TokenServiceArgon2PasswordHasherJWTEmailTemplateDapperUserStoreEmailDispatcherSQL ServerUserUserAPIControllerAPIControllerMediatR pipeline(ValidationBehavior)MediatR pipeline(ValidationBehavior)RegisterUserHandlerRegisterUserHandlerLoginHandlerLoginHandlerRefreshTokenHandlerRefreshTokenHandlerConfirmUserHandlerConfirmUserHandlerResendConfirmationEmailHandlerResendConfirmationEmailHandlerUpdateUsernameHandlerUpdateUsernameHandlerUpdateEmailHandlerUpdateEmailHandlerUpdatePasswordHandlerUpdatePasswordHandlerUpdateProfileHandlerUpdateProfileHandlerDeleteAccountHandlerDeleteAccountHandlerUserManager<ApplicationUser>UserManager<ApplicationUser>TokenServiceTokenServiceArgon2PasswordHasherArgon2PasswordHasherJWTInfrastructureJWTInfrastructureEmailProviderEmailProviderTemplateProviderTemplateProviderDapperUserStore(IUserStore/IUserPasswordStore/IUserEmailStore)DapperUserStore(IUserStore/IUserPasswordStore/IUserEmailStore)EmailDispatcher(Features.Emails)EmailDispatcher(Features.Emails)SQL ServerSQL ServerAPIAPIAPIAPIAPIAPIAPIAPIAPIAPIMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineMediatR pipelineRegisterUserHandlerLoginHandlerRefreshTokenHandlerConfirmUserHandlerResendConfirmationEmailHandlerUpdateUsernameHandlerUpdateEmailHandlerUpdatePasswordHandlerUpdateProfileHandlerDeleteAccountHandlerUserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.UserManager.ApplicationUser.TokenServiceTokenServiceTokenServiceTokenServiceTokenServiceTokenServiceTokenServiceTokenServiceArgon2PasswordHasherArgon2PasswordHasherArgon2PasswordHasherArgon2PasswordHasherJWTJWTJWTJWTJWTJWTJWTEmailTemplateDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreDapperUserStoreEmailDispatcherEmailDispatcherSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerSQL ServerASP.NET Core Identity's UserManager<TUser>replaces the deleted AuthRepository: handlersno longer issue SQL or call Argon2 directly,they delegate to UserManager, which in turncalls DapperUserStore (persistence) andArgon2PasswordHasher (IPasswordHasher<TUser>).Registration 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)user = new UserAccount{...}.ToApplicationUser()CreateAsync(user, password)HashPassword(user, password)Delegates to IPasswordInfrastructure.Hash:Argon2id parameters:- Salt: 16 bytes (128-bit)- Memory: 64MB- Iterations: 4- Parallelism: CPU count- Hash output: 32 bytes"base64(salt):base64(hash)"SetPasswordHashAsync(user,hash)user.Id is still empty at this point -only sets the in-memory hash. CreateAsyncwrites it to dbo.UserCredential itself.FindByNameAsync(username)SELECT UserAccountID,Username, ...FROM dbo.UserAccountWHERE Username =@Usernamenull (user doesn't exist)FindByEmailAsync(email)SELECT UserAccountID,Username, ...FROM dbo.UserAccountWHERE Email = @Emailnull (email doesn't exist)Default UserManager validators checkuniqueness before CreateAsync is called;DapperUserStore.CreateAsync also guardsagainst a genuine race via a caughtduplicate-key SqlException (2601/2627).CreateAsync(user)BEGIN TRANSACTIONINSERT INTO dbo.UserAccount(Username, FirstName,LastName,DateOfBirth, Email)OUTPUTINSERTED.UserAccountIDUserAccountId (GUID)INSERT INTOdbo.UserCredential(UserAccountId, Hash)COMMIT TRANSACTIONBoth statements run via Dapper'sExecuteScalarAsync/ExecuteAsync againsta single DbTransaction, not a storedprocedure - no manual parameter bindingIdentityResult.Success(user.Id set)IdentityResultalt[!result.Succeeded]throw ConflictException(DuplicateUserName/DuplicateEmailcodes ->"Username or email alreadyexists";otherwise joined errordescriptions)propagate409 Conflict"Username or email alreadyexists"[result.Succeeded]GenerateAccessToken(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)FindByNameAsync(username)FindByNameAsync(username)SELECT UserAccountID,Username, ...FROM dbo.UserAccountWHERE Username =@UsernameUserAccount row or nullHydrateAsync(load activeUserCredential.Hash,UserVerification ->EmailConfirmed)ApplicationUser or nullApplicationUser or nullalt[User not found]throw UnauthorizedException"Invalid username or password."propagate401 Unauthorized[User found]CheckPasswordAsync(user,password)HasPasswordAsync short-circuits to false(no active UserCredential was hydrated)without calling the hasher; otherwisedelegates to Argon2PasswordHasher.VerifyHashedPassword( user, user.PasswordHash,password)Delegates to IPasswordInfrastructure.Verify:1. Split stored hash: "salt:hash"2. Hash provided password with the same salt3. Constant-time comparison\n (CryptographicOperations.FixedTimeEquals)Success/Failedtrue/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 }}Refresh Token FlowPOST /api/auth/refreshHeader: X-Refresh-Token:<token>Send(RefreshTokenCommand(refreshToken))ValidationBehavior runs RefreshTokenValidator:refreshToken must be non-empty. Anonymousendpoint - no Bearer token required.alt[Validation fails]throwsFluentValidation.ValidationException400 Bad Request{message: "Validation failed",errors: {...}}[Validation succeeds]Handle(command)RefreshTokenAsync(refreshToken)ValidateRefreshTokenAsync(refreshToken)Validates signature/expiry against therefresh token secret, extracts userId/username claims.ValidatedToken or throwsalt[Token invalid or expired]throw UnauthorizedExceptionpropagatepropagate401 Unauthorized[Token valid]FindByIdAsync(validated.UserId)FindByIdAsync(userId)SELECT UserAccountID,Username, ...FROM dbo.UserAccountWHERE UserAccountID = @Idrow or nullApplicationUser or nullApplicationUser or nullalt[User not found]throw UnauthorizedException"User account not found"propagatepropagate401 Unauthorized[User found]GenerateAccessToken(user)GenerateRefreshToken(user)new access + refresh tokensRefreshTokenResultLoginPayload( userId, username, refreshToken, accessToken)LoginPayload200 OK{message: "Token refreshedsuccessfully.",payload: {...}}Confirm User FlowPOST/api/auth/confirm?token=<token>(requires Bearer access token)Send(ConfirmUserCommand(token))Handle(command)ValidateConfirmationTokenAsync(token)Validate against confirmationtoken secret, extract userIdValidatedToken or throwsValidatedTokenalt[Token invalid or expired]throw UnauthorizedExceptionpropagate401 Unauthorized[Token valid]FindByIdAsync(validatedToken.UserId)FindByIdAsync(userId)SELECT ...FROM dbo.UserAccountWHERE UserAccountID = @Idrow or nullApplicationUser or nullApplicationUser or nullalt[User not found]throw UnauthorizedException"User account not found"propagate401 Unauthorized[User found]SetEmailConfirmedAsync(user,true)IUserEmailStore<ApplicationUser> is injecteddirectly into the handler (not via UserManager),setting the confirmed flag in-memory here.UpdateAsync(user)UpdateAsync(user)UPDATE dbo.UserAccount SET ...INSERT INTOdbo.UserVerification(UserAccountId,VerificationDateTime)Idempotent: skipped (duplicate-keyexception swallowed) if a verificationrow already exists.IdentityResult.SuccessIdentityResultConfirmationPayload( user.Id, DateTime.UtcNow)ConfirmationPayload200 OK{message: "User with ID {id} isconfirmed.",payload: {...}}Resend Confirmation Email FlowPOST/api/auth/confirm/resend?userId=<id>(requires Bearer access token)Send(ResendConfirmationEmailCommand(userId))Handle(command)FindByIdAsync(userId)FindByIdAsync(userId)SELECT ...FROM dbo.UserAccountWHERE UserAccountID = @Idrow or nullApplicationUser or nullApplicationUser or nullalt[User not found]return silently(prevents user enumeration)[User found]IsEmailConfirmedAsync(user)boolalt[Already confirmed]return silently (no-op)[Not yet confirmed]GenerateConfirmationToken(user)confirmation tokenSend(SendResendConfirmationEmailCommand)SendResendConfirmationEmailAsync( firstName, email,confirmationToken)Render email(same Razor pipeline asregistration)SendAsync(email, subject, body)(void)(void)200 OK{message: "confirmation emailhas been resent"}Always 200 OK regardless of outcome above -same user-enumeration precaution as a not-founduser or an already-confirmed account.Update Username FlowPATCH /api/auth/username{newUsername}(requires Bearer access token)Send(UpdateUsernameCommand( GetAuthenticatedUserId(),newUsername))ValidationBehavior runs UpdateUsernameValidator(same format rules as registration)alt[Validation fails]throwsFluentValidation.ValidationException400 Bad Request[Validation succeeds]Handle(command)FindByIdAsync(userAccountId)FindByIdAsync(userAccountId)SELECT ...FROM dbo.UserAccountWHERE UserAccountID = @Idrow or nullApplicationUser or nullApplicationUser or nullalt[Account not found]throw NotFoundExceptionpropagate404 Not Found[Account found]SetUserNameAsync(user,newUsername)FindByNameAsync(newUsername)Uniqueness check before the UPDATE.UpdateAsync(user)UPDATE dbo.UserAccountSET Username = @UserName, ...IdentityResultIdentityResultalt[!result.Succeeded]throw ConflictException(DuplicateUserName code ->"Username already exists")propagate409 Conflict[result.Succeeded]UpdateUsernamePayload( user.Id, user.UserName)UpdateUsernamePayload200 OK{message: "Username updatedsuccessfully.",payload: {...}}Update Email FlowPATCH /api/auth/email{newEmail}(requires Bearer access token)Send(UpdateEmailCommand( GetAuthenticatedUserId(),newEmail))ValidationBehavior runs UpdateEmailValidator(same format rules as registration)alt[Validation fails]throwsFluentValidation.ValidationException400 Bad Request[Validation succeeds]Handle(command)FindByIdAsync(userAccountId)FindByIdAsync(userAccountId)SELECT ...ApplicationUser or nullApplicationUser or nullalt[Account not found]throw NotFoundExceptionpropagate404 Not Found[Account found]SetEmailAsync(user, newEmail)FindByEmailAsync(newEmail)Uniqueness check. Also resetsuser.EmailConfirmed = false -the caller must re-confirm viaPOST /api/auth/confirm/resend.UpdateAsync(user)UPDATE dbo.UserAccountSET Email = @Email, ...DELETE FROMdbo.UserVerificationWHERE UserAccountID = @IdIdentityResultIdentityResultalt[!result.Succeeded]throw ConflictException(DuplicateEmail code ->"Email address already exists")propagate409 Conflict[result.Succeeded]UpdateEmailPayload( user.Id, user.Email, user.EmailConfirmed = false)UpdateEmailPayload200 OK{message: "Email updatedsuccessfully.",payload: {...}}Update Password FlowPATCH /api/auth/password{currentPassword,newPassword}(requires Bearer access token)Send(UpdatePasswordCommand( GetAuthenticatedUserId(), currentPassword,newPassword))ValidationBehavior runs UpdatePasswordValidator(same password strength rules as registration)alt[Validation fails]throwsFluentValidation.ValidationException400 Bad Request[Validation succeeds]Handle(command)FindByIdAsync(userAccountId)FindByIdAsync(userAccountId)ApplicationUser or nullApplicationUser or nullalt[Account not found]throw NotFoundExceptionpropagate404 Not Found[Account found]ChangePasswordAsync( user, currentPassword,newPassword)VerifyHashedPassword( user, user.PasswordHash,currentPassword)Success/Failedalt[Current password mismatch]IdentityResult.Failed(PasswordMismatch)[Current password matches]HashPassword(user,newPassword)new hashSetPasswordHashAsync(user,newHash)UPDATE dbo.UserCredentialSET IsRevoked = 1, RevokedAt =GETDATE()WHERE UserAccountId = @IdAND IsRevoked = 0INSERT INTOdbo.UserCredential(UserAccountId, Hash)Revoke-then-insert runs inside oneDbTransaction (credential rotation).(void)IdentityResult.Successalt[Current password is incorrect]throw UnauthorizedException"Current password is incorrect"propagate401 Unauthorized[!result.Succeeded (other failure)]throw ConflictExceptionpropagate409 Conflict[result.Succeeded]UpdatePasswordPayload( user.Id, DateTime.UtcNow)UpdatePasswordPayload200 OK{message: "Password updatedsuccessfully.",payload: {...}}Update Profile FlowPATCH /api/auth/profile{firstName, lastName,dateOfBirth}(requires Bearer access token)Send(UpdateProfileCommand( GetAuthenticatedUserId(), firstName, lastName,dateOfBirth))ValidationBehavior runs UpdateProfileValidator(same name/DateOfBirth rules as registration)alt[Validation fails]throwsFluentValidation.ValidationException400 Bad Request[Validation succeeds]Handle(command)FindByIdAsync(userAccountId)FindByIdAsync(userAccountId)ApplicationUser or nullApplicationUser or nullalt[Account not found]throw NotFoundExceptionpropagate404 Not Found[Account found]user.FirstName = ...user.LastName = ...user.DateOfBirth = ...First/last name and date of birth aren'ttracked by any Identity store interface -mutated directly on the ApplicationUser,then saved via UserManager.UpdateAsync.UpdateAsync(user)UpdateAsync(user)UPDATE dbo.UserAccountSET FirstName = @FirstName, LastName = @LastName, DateOfBirth = @DateOfBirth,...IdentityResultIdentityResultalt[!result.Succeeded]throw ConflictExceptionpropagate409 Conflict[result.Succeeded]UpdateProfilePayload( user.Id, firstName, lastName, dateOfBirth)UpdateProfilePayload200 OK{message: "Profile updatedsuccessfully.",payload: {...}}Delete Account FlowDELETE /api/auth/account(requires Bearer access token)Send(DeleteAccountCommand( GetAuthenticatedUserId()))Handle(command)FindByIdAsync(userAccountId)FindByIdAsync(userAccountId)ApplicationUser or nullApplicationUser or nullalt[Account not found]throw NotFoundExceptionpropagate404 Not Found[Account found]DeleteAsync(user)DeleteAsync(user)DELETE FROM dbo.UserAccountWHERE UserAccountID = @IdUserCredential/UserVerification/UserAvatarcascade on delete. Posts, comments, photos,and follows use ON DELETE NO ACTION - if anyreference this account, the DELETE throws aforeign key violation (SQL error 547).alt[Foreign key violation (dependent records exist)]IdentityResult.Failed(AccountHasDependentRecords)[Delete succeeds]IdentityResult.SuccessIdentityResultalt[Account has dependent records]throw ConflictException"Account cannot be deletedwhile it stillhas associated posts,comments, photos,or follows."propagate409 Conflict[!result.Succeeded (other failure)]throw ConflictExceptionpropagate409 Conflict[result.Succeeded](void)(void)200 OK{message: "Account deletedsuccessfully."}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