Skip to content

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
  • AuthService verifies the password hash, enforces lockout, then calls JwtTokenMinter.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, and lockout_until (sheet 01 §entities, User.cs; creds added in MIQ018a).
  • Dev fallback — in Development with 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-creationPermissionPolicyProvider builds 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 checkPermissionAuthorizationHandler calls ICurrentTenantProvider.HasPermission, which is super-admin OR exact code in permission claims OR in scoped_permissions. HasPermissionForDepartment additionally matches a dept_id for department-scoped grants (sheet 01 §rules, CurrentTenantProvider.cs:99-113).

The model

  • Permission — a global lookup (no business_unit_id), a dotted code (e.g. employee.view), grouped by category (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, with effective_from / effective_to dates (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/User are 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.