feat: add user and role view sheets with detailed information display
- Created UserViewSheet component to display user details including username, full name, role, status, and timestamps. - Created RoleViewSheet component to display role details including name, level, status, creation date, and associated permissions. - Introduced permissions configuration in a new permissions.ts file, defining categories and permission codes for better management. - Implemented middleware for authentication, ensuring access control for protected routes.
This commit is contained in:
@@ -1,102 +1,116 @@
|
||||
# Authentication — Own-User Auth via External Identity Service (AuthHex)
|
||||
# Authentication & Authorization Layer
|
||||
|
||||
**Status**: Done
|
||||
**Depends On**: — (cross-cutting infrastructure, not a numbered feature step)
|
||||
## External Auth Service (AuthHex)
|
||||
- Runs on `http://localhost:6703`
|
||||
- Owns credentials — this app never stores passwords
|
||||
- Issues RS256-signed JWTs with claims: `sub`, `role_id`, `role_level`, `username`, `role_code`
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Own-user (staff/API) authentication is delegated to an external identity service ("AuthHex"),
|
||||
which owns credentials and signs RS256 JWTs. This app validates those tokens and maps them onto
|
||||
its own `User`/`Role` tables so the existing permission-policy authorization stays unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Background / Context
|
||||
|
||||
Previously this app verified passwords and minted its own HMAC-signed JWT locally. That's been
|
||||
replaced — AuthHex now owns credentials and signing; this app only holds AuthHex's **public** key
|
||||
to validate tokens, and never sees a plaintext password for this flow.
|
||||
|
||||
Authorization is untouched: permission policies, role levels, scope checks, and the seeded role
|
||||
set all still work exactly as before, because every validated token is mapped back onto a local
|
||||
`User`/`Role` row before any authorization check runs.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Changes
|
||||
|
||||
```jsonc
|
||||
"Jwt": {
|
||||
"PublicKey": "<RSAKeyValue>...</RSAKeyValue>",
|
||||
"Audience": "AuthHexClient",
|
||||
"Issuer": "AuthHex"
|
||||
},
|
||||
"AuthHex": { "BaseUrl": "http://localhost:6102" }
|
||||
```
|
||||
|
||||
`Jwt:Key` (old symmetric secret) is removed. `Jwt:PublicKey` is AuthHex's RSA public key in XML
|
||||
format (not PEM).
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Route | Notes |
|
||||
|---|---|---|
|
||||
| POST | /api/auth/login | Proxies to AuthHex `loginUser` |
|
||||
| POST | /api/auth/register | Proxies to AuthHex `registerUser` |
|
||||
| GET | /api/auth/me | `[Authorize]`, proxies to AuthHex `getUserDetails` |
|
||||
| POST | /api/auth/logout | `[Authorize]`, proxies to AuthHex `LogoutUser` |
|
||||
|
||||
All four just forward to `POST {AuthHex:BaseUrl}/api/user` and pass AuthHex's response back
|
||||
as-is. No cookie — token returned in the response body, sent back as `Authorization: Bearer`.
|
||||
|
||||
---
|
||||
|
||||
## Business Rules
|
||||
|
||||
1. No local password check or token issuance — both delegated to AuthHex.
|
||||
2. `AddJwtBearer` validates issuer/audience/signature/lifetime against AuthHex's public key.
|
||||
3. `OnTokenValidated` maps the token's `sub` claim to a local `User` (`IUserRepository`); fails
|
||||
the token if no matching active user exists.
|
||||
4. On success, `username`/`role_id`/`role_level` claims are attached from the **local** `User`/
|
||||
`Role` row — authorization checks downstream are unaffected by anything AuthHex's own token
|
||||
claims contain.
|
||||
5. Only one validation mechanism runs (`AddJwtBearer`) — two ported alternatives exist but are not
|
||||
registered (see Implementation Notes).
|
||||
6. No refresh tokens — once the AuthHex-issued token expires, the client must log in again.
|
||||
|
||||
---
|
||||
|
||||
## Permissions to Add to Seed
|
||||
|
||||
None — this only changes authentication. Permission policies and role seed data are untouched.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Add a refresh-token flow if AuthHex doesn't offer one and re-login UX proves disruptive.
|
||||
- Keep or delete the ported `AuthMiddleware`/`JwtMiddleware` if never actually needed.
|
||||
- No retry/circuit-breaker if AuthHex is unreachable.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
Features/Auth/ — AuthController, AuthService, AuthDTOs (thin AuthHex proxies)
|
||||
Infra/Authorization/
|
||||
AuthHex/ — IAuthHexClient, AuthHexClient, AuthHexResponse
|
||||
Jwt/RsaXmlKeyReader.cs — portable XML→RSA key parser (RSA.FromXmlString is Windows-only)
|
||||
Middleware/ — AuthMiddleware, JwtMiddleware, JwtOptions, ApiResponse
|
||||
(ported from OSMCore for reference — NOT registered)
|
||||
Program.cs — AddHttpClient<IAuthHexClient>, AddJwtBearer + OnTokenValidated
|
||||
(maps token → local User/Role → role_id/role_level claims)
|
||||
Infra/
|
||||
├── Auth/ External auth service integration
|
||||
│ ├── AuthHexClient.cs HTTP proxy to AuthHex (/api/user)
|
||||
│ ├── IAuthHexClient.cs
|
||||
│ └── AuthHexResponse.cs Raw response shape from AuthHex
|
||||
│
|
||||
└── Authorization/ This app's permission enforcement
|
||||
├── PermissionRequirement.cs ASP.NET Core requirement (wraps permission code string)
|
||||
├── PermissionPolicyProvider.cs Builds policy dynamically for any "x.y" string
|
||||
├── PermissionAuthorizationHandler.cs Evaluates requirement against DB
|
||||
├── AuthorizationController.cs Exposes permission/scope check endpoints
|
||||
├── CurrentUserService.cs Extracts typed claims from HttpContext.User
|
||||
├── ICurrentUserService.cs
|
||||
├── AuthorizationDTOs.cs
|
||||
├── Permission/
|
||||
│ ├── PermissionRepository.cs Queries RolePermissions + Permissions tables
|
||||
│ ├── PermissionService.cs
|
||||
│ ├── IPermissionRepository.cs
|
||||
│ └── IPermissionService.cs
|
||||
└── Scope/
|
||||
├── ScopeRepository.cs Queries UserStoreScopes / UserRouteScopes / UserRegionScopes
|
||||
├── ScopeAccessService.cs Resolves accessible stores via store → route → region hierarchy
|
||||
├── IScopeRepository.cs
|
||||
└── IScopeAccessService.cs
|
||||
```
|
||||
|
||||
**Gotcha:** don't run more than one of `AddJwtBearer` / `AuthMiddleware` /
|
||||
`JwtMiddleware.CustomMiddlewareAttribute` at once — each re-validates the same token
|
||||
independently.
|
||||
---
|
||||
|
||||
## Login Flow
|
||||
|
||||
```
|
||||
POST /api/auth/login
|
||||
→ AuthController
|
||||
→ AuthService.LoginAsync
|
||||
→ AuthHexClient.CallAsync("loginUser", ...)
|
||||
→ POST http://localhost:6703/api/user
|
||||
← RS256-signed JWT returned to client
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request Flow
|
||||
|
||||
```
|
||||
Incoming request with Authorization: Bearer <jwt>
|
||||
│
|
||||
▼
|
||||
AuthMiddleware (Middleware/AuthMiddleware.cs)
|
||||
- Reads Jwt:PublicKey from appsettings.json
|
||||
- RSA.Create().FromXmlString(publicKeyXml)
|
||||
- Validates JWT signature (RS256), issuer, audience, lifetime
|
||||
- Sets HttpContext.User = principal (claims: sub, role_id, role_level, username)
|
||||
│
|
||||
▼
|
||||
app.UseAuthorization()
|
||||
- Global AuthorizeFilter: requires IsAuthenticated on every endpoint
|
||||
- [Authorize("brand.view")] triggers PermissionPolicyProvider
|
||||
→ builds AuthorizationPolicy with PermissionRequirement("brand.view")
|
||||
→ PermissionAuthorizationHandler reads role_id claim
|
||||
→ PermissionService.HasPermissionAsync(roleId, "brand.view")
|
||||
→ DB: RolePermissions JOIN Permissions WHERE code = "brand.view"
|
||||
│
|
||||
▼
|
||||
Controller
|
||||
- ICurrentUserService.UserId / RoleId / RoleLevel extracted from HttpContext.User
|
||||
- Scope guards: HasStoreScopeAsync / HasRouteScopeAsync / HasRegionScopeAsync
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scope System
|
||||
|
||||
Scope controls which **records** a user can access (not just which actions).
|
||||
|
||||
| Assignment table | Access granted to |
|
||||
|---|---|
|
||||
| `UserStoreScopes` | Specific stores directly |
|
||||
| `UserRouteScopes` | All stores on that route |
|
||||
| `UserRegionScopes` | All routes → all stores in that region |
|
||||
|
||||
`ScopeAccessService.GetAccessibleStoreIdsAsync` merges all three levels and deduplicates.
|
||||
|
||||
---
|
||||
|
||||
## Pipeline Order (Program.cs)
|
||||
|
||||
```csharp
|
||||
app.UseCors();
|
||||
app.UseMiddleware<AuthMiddleware>(); // 1. populate HttpContext.User
|
||||
app.UseAuthorization(); // 2. evaluate [Authorize] attributes
|
||||
```
|
||||
|
||||
`UseAuthentication()` is intentionally absent — JWT validation is done manually in `AuthMiddleware`, not via ASP.NET Core's built-in bearer scheme.
|
||||
|
||||
---
|
||||
|
||||
## appsettings.json Keys
|
||||
|
||||
| Key | Purpose |
|
||||
|---|---|
|
||||
| `Jwt:PublicKey` | RSA XML public key from AuthHex (used by AuthMiddleware) |
|
||||
| `Jwt:Issuer` | `AuthHex` |
|
||||
| `Jwt:Audience` | `AuthHexClient` |
|
||||
| `AuthHex:BaseUrl` | `http://localhost:6703` |
|
||||
|
||||
Reference in New Issue
Block a user