Appearance
Authentication & JWT
Login Flow
mermaid
sequenceDiagram
participant Client
participant API as POST /api/auth/login
participant Auth as AuthService
participant DB as MySQL
Client->>API: { "email": "admin@example.com", "password": "..." }
API->>Auth: LoginAsync(email, password)
Auth->>DB: SELECT * FROM users WHERE email = ?
DB-->>Auth: user row (with passwordHash)
Auth->>Auth: BCrypt.Verify(password, passwordHash)
Auth->>DB: Resolve permissions for userId
DB-->>Auth: permission rows
Auth->>Auth: Build JWT claims
Auth-->>API: LoginResponseDto
API-->>Client: 200 { token, expiresAt, user }JWT Structure
The token is signed with HS256 using the Jwt:SecretKey from appsettings.json.
Claims
| Claim | Type | Example | Description |
|---|---|---|---|
sub | string | "42" | User ID |
email | string | "admin@rx.com" | User email |
given_name | string | "Alice" | First name |
family_name | string | "Smith" | Last name |
role_id | string | "1" | Assigned role ID |
is_admin | string | "true" or "false" | Admin bypass flag |
perm | string[] | ["Vendor:Read","Invoice:Create"] | Resolved permission list |
exp | int | Unix timestamp | Expiry (default 24h) |
iss | string | "MedRegia" | Issuer |
aud | string | "MedRegiaUsers" | Audience |
Admin vs Regular User
mermaid
graph TD
LOGIN["User logs in"] --> ADMIN{"is_admin?"}
ADMIN -->|"true"| ATOKEN["JWT with is_admin=true<br/>perm = ['*:Create']<br/>Skips ALL permission checks"]
ADMIN -->|"false"| RTOKEN["JWT with resolved perm[] claims<br/>e.g. Vendor:Read, Invoice:Create"]Using the Token
Include the token in every request header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Token Expiry
Tokens expire after 24 hours by default (configurable via Jwt:ExpirationHours).
There is no refresh token mechanism — users must log in again after expiry.
Configuration
json
// appsettings.json
{
"Jwt": {
"SecretKey": "CHANGE_ME_use_a_long_random_secret_key_at_least_32_chars",
"Issuer": "MedRegia",
"Audience": "MedRegiaUsers",
"ExpirationHours": "24"
}
}Production Security
Always replace SecretKey with a randomly generated string of at least 32 characters before deploying to production. Never commit the real secret to source control.
Password Security
- Passwords are hashed with BCrypt before storage — the plain text is never stored
- The
passwordHashfield is never returned by any API endpoint - Password changes go through the UserProfile module (
PUT /api/profile/password)