Update documentation (#156)

This commit is contained in:
Aaron Po
2026-02-21 05:02:22 -05:00
committed by GitHub
parent c5683df4b6
commit 50c2f5dfda
11 changed files with 3135 additions and 865 deletions

View File

@@ -0,0 +1,298 @@
@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