# Authentication & Authorization Layer ## 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` --- ## Directory Structure ``` 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 ``` --- ## 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 │ ▼ 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(); // 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` |