Identity, Access & Security (IAM)
Module code IAM · Release R0 (core), R1 (OIDC SSO, device approvals), R2 (SAML, API clients) · Packaging Core (SSO on Standard+, SAML on Enterprise) · Requirements
FR-IAM-*,BR-IAM-*,NFR-SEC-*in the PRD and BRD · Related RBAC permission matrix, architecture section 6
1. Overview and scope
IAM answers who is this, and what may they do on whose data. It owns tenant users, authentication, roles and permissions with data scopes, sessions, devices and the audit log. It replaces the reference product's manual credential hand-off and default password with invite activation.
In scope: user accounts linked to employees; invite links (email, SMS, WhatsApp); password policy (typed, per tenant); argon2id hashing and breached-password checks; TOTP MFA (mandatory for admin roles) with recovery codes; OIDC SSO (Google Workspace, Microsoft Entra ID); SAML (Enterprise, R2); role templates, cloning and custom roles; role assignments with scope and optional location or department filters; field-level protection for sensitive data; segregation-of-duties rules; sessions and revocation; mobile device binding and approvals; kiosk device tokens; API clients (R2); the append-only audit log; support-access sessions from PLT.
Out of scope: the reporting-manager relationship (EMP job assignment), platform staff identity (PLT).
2. Learned from the reference product
Source: Spryple analysis, §4, §11, §17 and §21.2.
- [Observed] The admin sets a user ID per employee and the password defaults to "Welcome1!"; the credentials email contains the plaintext password (SP-IAM-01; §21.2).
- [Observed] Credentials are created manually after onboarding, when the admin is emailed (SP-IAM-05).
- [Observed] The "Roles & Permissions" help page duplicates "Users Login", so the permission model is undocumented (SP-IAM-02; defect D1).
- [Observed] HR Policy rules 1–9 cover password complexity (SP-IAM-03); a password reset screen lets admins reset username, password or both (SP-IAM-04).
- [Observed] A new phone needs manager approval in the CRM app (§11, Device Approvals).
- [Observed] Login always needs a Company Code (SP-X-10).
3. Our design decisions
| ID | Decision [Proposed] | Reason |
|---|---|---|
| D-IAM-01 | No default or emailed passwords. A user activates through a single-use invite link (72 h) and sets their own password or signs in with SSO. | Removes the confirmed security weakness. |
| D-IAM-02 | The user is created automatically when an employee reaches Joined (or earlier if HR chooses "invite now"). | Removes the manual hand-off step. |
| D-IAM-03 | Authorisation is (permission, scope). A role is a set of permissions; a role assignment adds the scope and optional filters. The reporting-manager scope (TEAM) is derived from EMP job assignments. | Matches CONVENTIONS §4; one guard for every module. |
| D-IAM-04 | Sensitive fields (salary, bank, Aadhaar, PAN, health, biometrics) need explicit field permissions (emp.sensitive.read, pay.compensation.read) and are masked by default. | DPDP minimisation. |
| D-IAM-05 | MFA is mandatory for org_owner, hr_admin, finance_admin, payroll_manager; step-up MFA for high-risk actions (payroll reversal, role edits, bank detail approval). | High-value targets. |
| D-IAM-06 | Device binding is a tenant policy: off, record only, or approval required (by line manager). | Parity with the reference CRM; configurable for office staff. |
| D-IAM-07 | Every deny and every access to a sensitive resource is written to audit_event, which is append-only and exportable. | Accountability and investigations. |
4. Domain model
iam_permission is global reference data (no tenant_id), seeded from code. iam_device.kind is PERSONAL or KIOSK; kiosk devices hold a location-scoped, punch-only token.
5. State machines
5.1 User account
5.2 Device approval
6. Key business logic
6.1 Authorisation guard
can(user, permission, target):
assignments = active role assignments of user whose role contains permission
for a in assignments:
scope = min(a.scope, rolePermission.max_scope)
if inScope(user, target.employee_id, scope, a.filters): allow and audit if sensitive
deny and audit
inScope(user, employee, scope, filters):
SELF -> employee == user.employee_id
TEAM -> employee in reportingTree(user.employee_id) # direct and indirect
DEPARTMENT -> department(employee) in departments managed or filtered
LOCATION -> location(employee) in filters.locations or user's location
ORG -> true
# Org-hierarchy lookups use EMP job assignments valid today, cached per request.
Self-approval is always blocked: the approval engine never resolves the requester as an approver.
6.2 Login with risk checks
login(identifier, password, device):
resolve tenant from host or company code (never from the body)
user = find by username or email ; constant-time compare argon2id
on failure: increment counter ; lock after policy.max_attempts within window
if device unknown and policy.device_binding == APPROVAL_REQUIRED:
create iam_device Pending ; notify manager ; return limited session (punch disabled)
if mfa required: challenge TOTP
issue access JWT (15 min, claims sub, tid, sid, roles_hash) and rotating refresh token
refresh reuse detected -> revoke the whole session family
7. Configuration
Password and session policy (typed policy replacing HR Policy rules 1–9):
| Setting | Type | Default | Description |
|---|---|---|---|
password.min_length | integer | 10 | Minimum length |
password.max_length | integer | 128 | Maximum length |
password.require_classes | integer | 3 | Of upper, lower, digit, symbol |
password.breach_check | boolean | true | Reject known-breached passwords |
password.history | integer | 5 | Previous passwords that cannot be reused |
password.max_age_days | integer | 0 | 0 means no forced rotation |
login.max_attempts | integer | 5 | Before lock |
login.lock_minutes | integer | 15 | Lock duration |
session.idle_timeout_minutes | integer | 30 | Web idle timeout |
session.mobile_refresh_days | integer | 30 | Mobile refresh lifetime |
invite.expiry_hours | integer | 72 | Invite link validity |
invite.channels | array of string | [EMAIL, SMS] | Allowed channels |
mfa.required_roles | array of string | admin roles | Roles that must enrol |
device_binding | enum | RECORD | OFF, RECORD, APPROVAL_REQUIRED |
sso.enforce | boolean | false | Disable passwords for SSO domains |
sod.rules | array of object | payroll SoD | Conflicting permission pairs |
8. Screens by persona
| Persona | Screens |
|---|---|
| Any user | Activate account, sign in (web subdomain or company code on mobile), MFA enrolment, forgot password, my sessions and devices |
hr_admin | Users (status, resend invite, suspend), password policy, device policy, SSO setup, bulk invite |
org_owner | Roles (clone, edit permissions and max scopes), role assignments, SoD rules, audit log viewer and export, support-access consent |
line_manager | Device approvals for the team |
auditor | Audit log (read-only) |
9. Events
| Event | Consumers |
|---|---|
iam.user.invited | NTF (send invite) |
iam.user.activated | EMP (onboarding checklist item), NTF |
iam.user.locked | NTF (user and admin) |
iam.user.deactivated | MOB (wipe tokens), NTF |
iam.role.changed | Permission cache invalidation, audit |
iam.role_assignment.changed | Permission cache invalidation, audit |
iam.device.approval_requested | NTF (manager task) |
iam.device.approved, iam.device.revoked | MOB |
iam.session.revoked | MOB, web |
Consumed: emp.employee.joined (create user, send invite), emp.job.changed (team-scope cache), ext.lwd.reached (deactivate at 23:59 local time), plt.support_access.granted (temporary session).
10. API surface
| Resource | Main operations |
|---|---|
/v1/iam/auth/login, /refresh, /logout, /mfa/verify | Authentication |
/v1/iam/invites/:token/accept | Activation |
/v1/iam/users | GET, POST /:id/invite, POST /:id/suspend, POST /:id/reset-mfa |
/v1/iam/roles and /v1/iam/roles/:id/permissions | CRUD, POST /:id/clone |
/v1/iam/role-assignments | CRUD |
/v1/iam/permissions | GET catalogue |
/v1/iam/sessions, /v1/iam/devices | GET, DELETE, POST /:id/approve |
/v1/iam/sso-connections | CRUD |
/v1/iam/audit-events | GET, POST /export |
/v1/iam/me/permissions | Effective permissions for navigation |
Detailed in API endpoints.
11. Permissions
| Permission | Scopes | Default roles |
|---|---|---|
iam.user.manage | ORG, LOCATION | hr_admin |
iam.role.manage | ORG | org_owner |
iam.role_assignment.manage | ORG | org_owner, hr_admin |
iam.policy.manage | ORG | hr_admin |
iam.sso.manage | ORG | org_owner |
iam.device.approve | TEAM | line_manager, sales_manager |
iam.audit.read | ORG | org_owner, auditor |
iam.audit.export | ORG | org_owner |
The full role-by-permission grid is in the RBAC permission matrix.
12. Reports
| Report | Contents |
|---|---|
| Users and activation | Invited, active, expired, locked; time to activate |
| MFA coverage | Admin roles without MFA (must be zero) |
| Role assignments | Who holds which role and scope |
| Access review | Quarterly list for owners to confirm |
| Audit log | Filterable, exportable |
| Device register | Approved and pending devices per team |
13. Release and priority
| Capability | Release | MoSCoW |
|---|---|---|
| Invite activation, passwords, MFA, sessions, roles with scopes, audit log | R0 | Must |
| Automatic user creation on join; deactivation at LWD | R1 | Must |
| OIDC SSO | R1 | Should |
| Device binding and approvals | R1 | Must |
| Field-level masking and SoD | R1 | Must |
| Kiosk device tokens | R2 | Should |
| SAML, API clients, access reviews | R2 | Should |
14. Open questions
- Should employees without an email or smartphone (factory staff) log in with mobile OTP only? This is likely, with kiosk punching for attendance.
- Do we allow one person to have users in two tenants (e.g. a consultant)? The default is separate users per tenant.
- Is quarterly access review a Professional feature or Enterprise only?