Authentication & RBAC
What it is
ManpowerIQ authenticates users with username/password, issues a JWT access token, and authorises every protected action through a permission-centric RBAC model — a global catalog of permission codes, BU-scoped roles that bundle them, and [Authorize("x.y")] attributes turned into live permission checks.
Why it's built this way
Authorisation is on permissions, not role names. A port's roles differ between operating companies, but the underlying capabilities (employee.view, roster.create…) are constant. Checking permissions rather than hard-coded roles lets each business unit shape its own roles while the same capabilities stay consistently enforced (sheet 01 §how-it-works). The permission catalog is global; roles are tenant-scoped (sheet 01 §decisions).
Auth scope was kept deliberately minimal for v1: a single short-lived access token, no refresh-token rotation and no external identity provider. Those are designed-for-later, not built (see Build status).
How it works
Login & token
sequenceDiagram
participant U as User (web)
participant A as AuthService
participant J as JwtTokenMinter
U->>A: username + password
A->>A: verify hash, check lockout
A->>J: MintForUserAsync(user, expiresInHours: 8)
J-->>A: signed JWT (claims + 8h expiry)
A-->>U: access token
U->>U: store token, send as Bearer on every request
AuthServiceverifies the password hash, enforces lockout, then callsJwtTokenMinter.MintForUserAsync(user, expiresInHours: 8)(AuthService.cs:111). The token expires 8 hours after issue (JwtTokenMinter.cs:80,expires: DateTime.UtcNow.AddHours(8)).- The JWT carries the claims that drive both tenancy and authorisation:
business_unit_id,is_super_admin,sub,username,permission,scoped_permissions(sheet 01 §how-it-works). - Account protection — user credentials include
password_hash,password_change_required,failed_login_count, andlockout_until(sheet 01 §entities,User.cs; creds added in MIQ018a). - Dev fallback — in
Developmentwith no token, the system resolves to BU=1 / "dev" (sheet 01 MUST-NOT #6). Not a production path.
Permission enforcement
flowchart LR
REQ["[Authorize(\"employee.view\")]"] --> PPP[PermissionPolicyProvider<br/>name contains a dot → PermissionRequirement]
PPP --> PAH[PermissionAuthorizationHandler]
PAH --> CTP[CurrentTenantProvider.HasPermission]
CTP --> R{super-admin OR<br/>code in permissions OR<br/>code in scoped_permissions}
R -->|yes| ALLOW[allow]
R -->|no| DENY[403]
- Policy auto-creation —
PermissionPolicyProviderbuilds a policy on the fly for any[Authorize("…")]policy name containing a dot; everything else falls through to the default provider (sheet 01 §rules,PermissionPolicyProvider.cs:25-36). - The check —
PermissionAuthorizationHandlercallsICurrentTenantProvider.HasPermission, which issuper-admin OR exact code in permission claims OR in scoped_permissions.HasPermissionForDepartmentadditionally matches adept_idfor department-scoped grants (sheet 01 §rules,CurrentTenantProvider.cs:99-113).
The model
- Permission — a global lookup (no
business_unit_id), a dottedcode(e.g.employee.view), grouped bycategory(sheet 01 §entities,Permission.cs). - Role — tenant-scoped (
TenantEntity), a named bundle, unique(business_unit_id, code)(sheet 01 §entities,Role.cs). - UserRole — a grant of a role to a user, optionally
scope_department_id, witheffective_from/effective_todates (sheet 01 §entities,UserRole.cs).
The verified catalog is 97 permissions across 9 roles (the MIQ-003 baseline was 40 permissions / 7 roles; HR_DIRECTOR and COO were added later). The full canonical matrix is the RBAC matrix reference page, seeded from Sprint/RBAC_RolePermission_Extract.md.
Gotchas / constraints
- No refresh tokens. Login mints one 8-hour access token; there is no refresh/rotation endpoint (verified: grep for
refresh_token/RefreshToken→ no matches). When the token expires the user re-authenticates. - No SSO / Active Directory. Authentication is local username/password only (verified: grep for
AzureAd/OpenIdConnect/SAML→ no matches). SSO was "planned Sprint 1" in the handover but is not built. - Permission is a global table with no BU filter — only
Role/RolePermission/UserRole/Userare tenant-scoped (sheet 01 MUST-NOT #4). - Policy names must contain a dot to be treated as a permission requirement; a name without one falls through to the default provider (e.g. "Authenticated").
- Super-admin bypasses BU scoping by design — a powerful grant, intentionally limited to trusted use (sheet 01 §rules).
Build status
- Available — JWT username/password login, account lockout, the permission catalog, BU-scoped roles, department-scoped + effective-dated grants, and the runtime permission check all ship and are enforced (sheet 01 §build-status).
- Planned — JWT refresh tokens; SSO / Active Directory. Documented as absent, not in use.
Related
- RBAC matrix — the canonical 9×97 reference.
- Roles & permissions — the user-facing view.
- Multi-tenancy — how the JWT claims drive tenant isolation.
- Fact sheet: 01 (foundation).