@startuml authentication-flow !theme plain skinparam backgroundColor #FFFFFF skinparam defaultFontName Arial skinparam sequenceMessageAlign center skinparam maxMessageSize 200 title User Authentication Flow - Expanded actor User participant "API\nController" as API box "Service Layer" #LightBlue participant "RegisterService" as RegSvc participant "LoginService" as LoginSvc participant "TokenService" as TokenSvc participant "EmailService" as EmailSvc end box box "Infrastructure Layer" #LightGreen participant "Argon2\nInfrastructure" as Argon2 participant "JWT\nInfrastructure" as JWT participant "Email\nProvider" as SMTP participant "Template\nProvider" as Template end box box "Repository Layer" #LightYellow participant "AuthRepository" as AuthRepo participant "UserAccount\nRepository" as UserRepo end box database "SQL Server\nStored Procedures" as DB == Registration Flow == User -> API: POST /api/auth/register\n{username, firstName, lastName,\nemail, dateOfBirth, password} activate API note right of API FluentValidation runs: - 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 old end note API -> API: Validate request\n(FluentValidation) alt Validation fails API -> User: 400 Bad Request\n{errors: {...}} else Validation succeeds API -> RegSvc: RegisterAsync(userAccount, password) activate RegSvc RegSvc -> AuthRepo: GetUserByUsernameAsync(username) activate AuthRepo AuthRepo -> DB: EXEC usp_GetUserAccountByUsername activate DB DB --> AuthRepo: null (user doesn't exist) deactivate DB deactivate AuthRepo RegSvc -> AuthRepo: GetUserByEmailAsync(email) activate AuthRepo AuthRepo -> DB: EXEC usp_GetUserAccountByEmail activate DB DB --> AuthRepo: null (email doesn't exist) deactivate DB deactivate AuthRepo alt User/Email already exists RegSvc -> API: throw ConflictException API -> User: 409 Conflict\n"Username or email already exists" else User doesn't exist RegSvc -> Argon2: Hash(password) activate Argon2 note right of Argon2 Argon2id parameters: - Salt: 16 bytes (128-bit) - Memory: 64MB - Iterations: 4 - Parallelism: CPU count - Hash output: 32 bytes end note Argon2 -> Argon2: Generate random salt\n(16 bytes) Argon2 -> Argon2: Hash password with\nArgon2id algorithm Argon2 --> RegSvc: "base64(salt):base64(hash)" deactivate Argon2 RegSvc -> AuthRepo: RegisterUserAsync(\n username, firstName, lastName,\n email, dateOfBirth, hash) activate AuthRepo AuthRepo -> DB: EXEC USP_RegisterUser activate DB note right of DB Transaction begins: 1. INSERT UserAccount 2. INSERT UserCredential (with hashed password) Transaction commits end note DB -> DB: BEGIN TRANSACTION DB -> DB: INSERT INTO UserAccount\n(Username, FirstName, LastName,\nEmail, DateOfBirth) DB -> DB: OUTPUT INSERTED.UserAccountID DB -> DB: INSERT INTO UserCredential\n(UserAccountId, Hash) DB -> DB: COMMIT TRANSACTION DB --> AuthRepo: UserAccountId (GUID) deactivate DB AuthRepo --> RegSvc: UserAccount entity deactivate AuthRepo RegSvc -> TokenSvc: GenerateAccessToken(userAccount) activate TokenSvc TokenSvc -> JWT: GenerateJwt(userId, username, expiry) activate JWT note right of JWT JWT Configuration: - Algorithm: HS256 - Expires: 1 hour - Claims: * sub: userId * unique_name: username * jti: unique token ID end note JWT -> JWT: Create JWT with claims JWT -> JWT: Sign with secret key JWT --> TokenSvc: Access Token deactivate JWT TokenSvc --> RegSvc: Access Token deactivate TokenSvc RegSvc -> TokenSvc: GenerateRefreshToken(userAccount) activate TokenSvc TokenSvc -> JWT: GenerateJwt(userId, username, expiry) activate JWT note right of JWT Refresh Token: - Expires: 21 days - Same structure as access token end note JWT --> TokenSvc: Refresh Token deactivate JWT TokenSvc --> RegSvc: Refresh Token deactivate TokenSvc RegSvc -> EmailSvc: SendRegistrationEmailAsync(\n createdUser, confirmationToken) activate EmailSvc EmailSvc -> Template: RenderUserRegisteredEmailAsync(\n firstName, confirmationLink) activate Template note right of Template Razor Component: - Header with branding - Welcome message - Confirmation button - Footer end note Template -> Template: Render Razor component\nto HTML Template --> EmailSvc: HTML email content deactivate Template EmailSvc -> SMTP: SendAsync(email, subject, body) activate SMTP note right of SMTP SMTP Configuration: - Host: from env (SMTP_HOST) - Port: from env (SMTP_PORT) - TLS: StartTLS - Auth: username/password end note SMTP -> SMTP: Create MIME message SMTP -> SMTP: Connect to SMTP server SMTP -> SMTP: Authenticate SMTP -> SMTP: Send email SMTP -> SMTP: Disconnect SMTP --> EmailSvc: Success / Failure deactivate SMTP alt Email sent successfully EmailSvc --> RegSvc: emailSent = true else Email failed EmailSvc --> RegSvc: emailSent = false\n(error suppressed) end deactivate EmailSvc RegSvc --> API: RegisterServiceReturn(\n userAccount, accessToken,\n refreshToken, emailSent) deactivate RegSvc API -> API: Create response body API -> User: 201 Created\n{\n message: "User registered successfully",\n payload: {\n userAccountId, username,\n accessToken, refreshToken,\n confirmationEmailSent\n }\n} end end deactivate API == Login Flow == User -> API: POST /api/auth/login\n{username, password} activate API API -> API: Validate request\n(FluentValidation) alt Validation fails API -> User: 400 Bad Request\n{errors: {...}} else Validation succeeds API -> LoginSvc: LoginAsync(username, password) activate LoginSvc LoginSvc -> AuthRepo: GetUserByUsernameAsync(username) activate AuthRepo AuthRepo -> DB: EXEC usp_GetUserAccountByUsername activate DB DB -> DB: SELECT FROM UserAccount\nWHERE Username = @Username DB --> AuthRepo: UserAccount entity deactivate DB deactivate AuthRepo alt User not found LoginSvc -> API: throw UnauthorizedException\n"Invalid username or password" API -> User: 401 Unauthorized else User found LoginSvc -> AuthRepo: GetActiveCredentialByUserAccountIdAsync(userId) activate AuthRepo AuthRepo -> DB: EXEC USP_GetActiveUserCredentialByUserAccountId activate DB note right of DB SELECT FROM UserCredential WHERE UserAccountId = @UserAccountId AND IsRevoked = 0 end note DB --> AuthRepo: UserCredential entity deactivate DB deactivate AuthRepo alt No active credential LoginSvc -> API: throw UnauthorizedException API -> User: 401 Unauthorized else Active credential found LoginSvc -> Argon2: Verify(password, storedHash) activate Argon2 note right of Argon2 1. Split stored hash: "salt:hash" 2. Extract salt 3. Hash provided password\n with same salt 4. Constant-time comparison end note Argon2 -> Argon2: Parse salt from stored hash Argon2 -> Argon2: Hash provided password\nwith extracted salt Argon2 -> Argon2: FixedTimeEquals(\n computed, stored) Argon2 --> LoginSvc: true/false deactivate Argon2 alt Password invalid LoginSvc -> API: throw UnauthorizedException API -> User: 401 Unauthorized else Password valid LoginSvc -> TokenSvc: GenerateAccessToken(user) activate TokenSvc TokenSvc -> JWT: GenerateJwt(...) activate JWT JWT --> TokenSvc: Access Token deactivate JWT TokenSvc --> LoginSvc: Access Token deactivate TokenSvc LoginSvc -> TokenSvc: GenerateRefreshToken(user) activate TokenSvc TokenSvc -> JWT: GenerateJwt(...) activate JWT JWT --> TokenSvc: Refresh Token deactivate JWT TokenSvc --> LoginSvc: Refresh Token deactivate TokenSvc LoginSvc --> API: LoginServiceReturn(\n userAccount, accessToken,\n refreshToken) deactivate LoginSvc API -> User: 200 OK\n{\n message: "Logged in successfully",\n payload: {\n userAccountId, username,\n accessToken, refreshToken\n }\n} end end end end deactivate API == Error Handling (Global Exception Filter) == note over API GlobalExceptionFilter catches: - ValidationException → 400 Bad Request - ConflictException → 409 Conflict - NotFoundException → 404 Not Found - UnauthorizedException → 401 Unauthorized - ForbiddenException → 403 Forbidden - All others → 500 Internal Server Error end note @enduml