9. Authentication & Authorization
9.1 One JWT, issued by Shumoul.Api
Shumoul.MultiTenancyApi has no login endpoint of its own for ERP end-users. Every onboarding call is
authenticated with the exact same JWT Shumoul.Api's normal login endpoint already issues
(TokenService.GetTokenAsync). There is nothing onboarding-specific for a client to obtain or manage.
The token carries (among others):
| Claim type | Meaning |
|---|---|
NameIdentifier | The authenticated user's id — becomes TenantOnboardingApplyLogs.AppliedBy |
Tenant / TenantId | The user's tenant identifier (both set to the same value) |
r (role) | One claim per ERP role assigned to the user, e.g. Admin |
Shumoul.MultiTenancyApi.Host validates this JWT with the same signing key, issuer, and audience list
Shumoul.Api uses (AddJwtAuthentication(), a shared Shumoul.Framework.Infrastructure extension) — the two
hosts must be configured with the same JwtSettings:key for this to work (see
Chapter 12 — Runtime Configuration).
9.2 Why permissions are evaluated from JWT claims, not a database lookup
The ERP's own permission model (Users → UserRoles → RoleClaims) lives entirely inside each tenant's own
ApplicationDbContext, which Shumoul.MultiTenancyApi has no connection to — its central Saas database has
no Users/Roles/RoleClaims table at all. So instead of a database-backed
ICurrentUserPermissionsService (the pattern Shumoul.Api itself uses), Shumoul.MultiTenancyApi uses a
narrow, claims-based implementation: OnboardingCurrentUserPermissionsService.
9.3 Permission evaluation rules
| Permission | Rule |
|---|---|
Permissions.Onboarding.View | Granted to any authenticated user whose token carries a non-empty Tenant/TenantId claim. |
Permissions.Onboarding.Answer | Granted only if the token's r (role) claim includes Admin (or an explicit permission claim, forward-compatible — see below). |
Permissions.Onboarding.Apply | Same rule as Answer. |
Permissions.Onboarding.Skip | Same rule as Answer. |
Any other permission (platform-admin: TenantSubscriptions, BackgroundJobs, ProformaInvoices, etc.) | Always denied. No data source exists in this host for platform-admin permissions — this is a deliberate, safe deny-by-default, not a bug. See Chapter 17. |
9.3.1 What counts as "tenant Admin"
The role name Admin is what Shumoul.Api's tenant-provisioning seeder
(ApplicationDbSeeder.SeedAdminUserAsync) assigns to a new tenant's first/admin user, and it is
immutable — RoleService blocks renaming or deleting any of the platform's reserved role names
(RoleConstants.*), so this signal cannot be spoofed by a tenant renaming a role. No Shumoul.Api
TokenService change was needed to support this — the role claim was already being issued.
OwnerAdminRoleNames also accepts Owner, TenantOwner, TenantAdmin, Administrator, SuperAdmin
case-insensitively, for forward compatibility with a future role-naming convention — none of these are
produced by the current codebase; only Admin is real today.
A permission claim matching the exact permission string is also checked as a defense-in-depth fallback —
also not currently issued by anything.
9.4 Unauthorized vs. forbidden
| Situation | Result |
|---|---|
No Authorization header | 401 |
| Malformed / invalid-signature / expired token | 401 |
| Valid token, missing the required permission | 403 |
A 403 on answers/recommendation/apply/skip for a non-admin tenant user is the system working
correctly, not an error condition to retry or work around.
9.5 Cross-tenant safety
- Tenant resolution for an authenticated request tries the JWT
TenantIdclaim before falling back to anyTenantIdheader — so an authenticated caller cannot override their own tenant by attaching an arbitrary header. OnboardingServiceindependently double-checks ownership on every session-scoped action:session.TenantId != CurrentTenantId(from the JWTTenantclaim) →403,"Session does not belong to the current tenant."This fires even if tenant resolution were somehow bypassed.- A tenant's
TenantIdis never accepted from the request body for authorization purposes — only from the authenticated JWT.
9.6 What this means for Angular
No special onboarding auth handling is needed — attach the normal Authorization: Bearer {jwt} header
exactly as every other ERP API call does. See Chapter 13 for how to
handle the 403 case in the UI.
