@iqauth/sdk 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +287 -0
- package/dist/browser-session.d.mts +12 -0
- package/dist/browser-session.d.ts +12 -0
- package/dist/browser-session.js +1812 -0
- package/dist/browser-session.mjs +28 -0
- package/dist/browser.d.mts +46 -0
- package/dist/browser.d.ts +46 -0
- package/dist/browser.js +768 -0
- package/dist/browser.mjs +47 -0
- package/dist/chunk-5HF3OBNO.mjs +189 -0
- package/dist/chunk-5WFR6Y33.mjs +59 -0
- package/dist/chunk-6I6RM4MN.mjs +51 -0
- package/dist/chunk-73R6BEGO.mjs +176 -0
- package/dist/chunk-E46DKOVI.mjs +632 -0
- package/dist/chunk-JQWYIIIS.mjs +1740 -0
- package/dist/chunk-X3K3WOBR.mjs +64 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +581 -0
- package/dist/cli/index.mjs +57 -0
- package/dist/client-C1DXfB8Z.d.mts +911 -0
- package/dist/client-CggvJmmm.d.ts +911 -0
- package/dist/dev-FUTJZSWN.mjs +56 -0
- package/dist/doctor-OHJRZBBT.mjs +89 -0
- package/dist/errors-CDdl24MP.d.mts +52 -0
- package/dist/errors-CDdl24MP.d.ts +52 -0
- package/dist/express-BKAXB5Nl.d.ts +61 -0
- package/dist/express-CpfyYTmw.d.mts +61 -0
- package/dist/express.d.mts +45 -0
- package/dist/express.d.ts +45 -0
- package/dist/express.js +2252 -0
- package/dist/express.mjs +122 -0
- package/dist/fastify.d.mts +23 -0
- package/dist/fastify.d.ts +23 -0
- package/dist/fastify.js +2062 -0
- package/dist/fastify.mjs +118 -0
- package/dist/hono.d.mts +22 -0
- package/dist/hono.d.ts +22 -0
- package/dist/hono.js +2051 -0
- package/dist/hono.mjs +107 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2070 -0
- package/dist/index.mjs +83 -0
- package/dist/init-LLCSQGNL.mjs +198 -0
- package/dist/keys-NLWFAOEM.mjs +63 -0
- package/dist/mobile.d.mts +11 -0
- package/dist/mobile.d.ts +11 -0
- package/dist/mobile.js +1809 -0
- package/dist/mobile.mjs +25 -0
- package/dist/next.d.mts +37 -0
- package/dist/next.d.ts +37 -0
- package/dist/next.js +2078 -0
- package/dist/next.mjs +130 -0
- package/dist/publishableKey-B5DIK81A.d.mts +24 -0
- package/dist/publishableKey-B5DIK81A.d.ts +24 -0
- package/dist/react.d.mts +196 -0
- package/dist/react.d.ts +196 -0
- package/dist/react.js +1457 -0
- package/dist/react.mjs +787 -0
- package/dist/server/handlers.d.mts +96 -0
- package/dist/server/handlers.d.ts +96 -0
- package/dist/server/handlers.js +243 -0
- package/dist/server/handlers.mjs +14 -0
- package/dist/server.d.mts +14 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +2195 -0
- package/dist/server.mjs +47 -0
- package/dist/service.d.mts +11 -0
- package/dist/service.d.ts +11 -0
- package/dist/service.js +1809 -0
- package/dist/service.mjs +25 -0
- package/dist/signIn-C8f6qVjD.d.mts +238 -0
- package/dist/signIn-Cy2lbEXb.d.ts +238 -0
- package/dist/types-Cxl3bQHt.d.mts +900 -0
- package/dist/types-Cxl3bQHt.d.ts +900 -0
- package/docs/APP_INTEGRATION_MATRIX.md +59 -0
- package/docs/BROWSER_SESSION_MIGRATION.md +69 -0
- package/docs/FRESH_IMPLEMENTATION_GUIDE.md +188 -0
- package/docs/TARBALL_RELEASE_WORKFLOW.md +98 -0
- package/docs/V1_TO_V2_UPGRADE_GUIDE.md +318 -0
- package/docs/guides/api-keys.md +130 -0
- package/docs/guides/app-registration.md +149 -0
- package/docs/guides/auth-flows.md +168 -0
- package/docs/guides/branding.md +160 -0
- package/docs/guides/entitlements.md +115 -0
- package/docs/guides/entity-hierarchy.md +200 -0
- package/docs/guides/error-handling.md +251 -0
- package/docs/guides/gdpr-compliance.md +123 -0
- package/docs/guides/invitations.md +143 -0
- package/docs/guides/mfa-enrollment.md +170 -0
- package/docs/guides/middleware-reference.md +205 -0
- package/docs/guides/mobile-native.md +110 -0
- package/docs/guides/roles-and-permissions.md +220 -0
- package/docs/guides/scoped-authorization.md +247 -0
- package/docs/guides/server-platform-integration.md +52 -0
- package/docs/guides/service-automation-integration.md +36 -0
- package/docs/guides/session-management.md +97 -0
- package/docs/guides/tenant-management.md +216 -0
- package/docs/guides/token-verification.md +178 -0
- package/docs/guides/user-management.md +184 -0
- package/docs/guides/webhooks.md +136 -0
- package/docs/integration-prompts/README.md +20 -0
- package/docs/integration-prompts/first-party-browser-app.md +29 -0
- package/docs/integration-prompts/install-from-tarball.md +41 -0
- package/docs/integration-prompts/migrate-from-local-packages-source.md +57 -0
- package/docs/integration-prompts/native-mobile-app.md +24 -0
- package/docs/integration-prompts/server-platform-app.md +20 -0
- package/docs/integration-prompts/service-automation-app.md +20 -0
- package/package.json +115 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Scoped Authorization (V2)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Implement V2 scoped authorization using entity-scoped memberships, scope switching, and permission groups with inheritance. Extends the V1 flat role model.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- Tenant with `enableScopedAuth: true` (set via `client.tenants.update()`)
|
|
11
|
+
- Entity hierarchy created (vendors, sources, clients)
|
|
12
|
+
- `tenant_admin` or `platform_admin` role
|
|
13
|
+
|
|
14
|
+
## Environment Note
|
|
15
|
+
|
|
16
|
+
Scope-switching token examples in this guide assume a trusted runtime or secure mobile storage.
|
|
17
|
+
|
|
18
|
+
For first-party browser apps, perform scope-sensitive operations through your backend session layer.
|
|
19
|
+
|
|
20
|
+
## SDK Methods
|
|
21
|
+
|
|
22
|
+
### MembershipsModule
|
|
23
|
+
|
|
24
|
+
| Method | Description |
|
|
25
|
+
|--------|-------------|
|
|
26
|
+
| `grant(data)` | Grant scoped membership → `Membership` |
|
|
27
|
+
| `listForUser(userId, tenantId)` | List user's memberships |
|
|
28
|
+
| `listForScope(type, id)` | List memberships for a scope |
|
|
29
|
+
| `listForTenant(params?)` | List all tenant memberships |
|
|
30
|
+
| `update(id, data)` | Update membership |
|
|
31
|
+
| `revoke(id)` | Revoke membership |
|
|
32
|
+
|
|
33
|
+
### ScopeModule
|
|
34
|
+
|
|
35
|
+
| Method | Description |
|
|
36
|
+
|--------|-------------|
|
|
37
|
+
| `getAvailable()` | Get available scopes tree |
|
|
38
|
+
| `switchScope(type, id)` | Switch active scope → new access token |
|
|
39
|
+
|
|
40
|
+
### PermissionGroupsModule
|
|
41
|
+
|
|
42
|
+
| Method | Description |
|
|
43
|
+
|--------|-------------|
|
|
44
|
+
| `list(tenantId)` | List permission groups |
|
|
45
|
+
| `create(tenantId, name, desc?)` | Create group |
|
|
46
|
+
| `update(tenantId, groupId, data)` | Update group |
|
|
47
|
+
| `delete(tenantId, groupId)` | Delete group |
|
|
48
|
+
| `getPermissions(tenantId, groupId)` | List group permissions |
|
|
49
|
+
| `addPermission(tenantId, groupId, data)` | Add permission to group |
|
|
50
|
+
| `removePermission(tenantId, groupId, permId)` | Remove permission |
|
|
51
|
+
| `addInheritance(tenantId, groupId, fromId)` | Add group inheritance |
|
|
52
|
+
| `removeInheritance(tenantId, groupId, fromId)` | Remove inheritance |
|
|
53
|
+
| `getUserGroups(tenantId, userId)` | Get user's groups |
|
|
54
|
+
| `assignUserToGroup(tenantId, userId, groupId)` | Assign user to group |
|
|
55
|
+
| `removeUserFromGroup(tenantId, userId, groupId)` | Remove from group |
|
|
56
|
+
| `getUserOverrides(tenantId, userId)` | Get user overrides |
|
|
57
|
+
| `addUserOverride(tenantId, userId, data)` | Add override |
|
|
58
|
+
| `removeUserOverride(tenantId, userId, overrideId)` | Remove override |
|
|
59
|
+
| `getEffectivePermissions(tenantId, userId, params)` | Resolved permissions |
|
|
60
|
+
| `checkPermission(tenantId, userId, appKey, nodeKey)` | Check single permission |
|
|
61
|
+
|
|
62
|
+
## Step-by-Step
|
|
63
|
+
|
|
64
|
+
### 1. Enable Scoped Auth
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
await client.tenants.update(tenantId, { enableScopedAuth: true });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Grant Scoped Memberships
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const membership = await client.memberships.grant({
|
|
74
|
+
userId: "user-uuid",
|
|
75
|
+
roleName: "vendor_admin",
|
|
76
|
+
scopeType: "vendor",
|
|
77
|
+
scopeId: vendorId,
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Valid `scopeType`: `"tenant"`, `"vendor"`, `"source"`, `"client"`.
|
|
82
|
+
|
|
83
|
+
### 3. Query Memberships
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const userResult = await client.memberships.listForUser(userId, tenantId);
|
|
87
|
+
const scopeResult = await client.memberships.listForScope("vendor", vendorId);
|
|
88
|
+
const tenantResult = await client.memberships.listForTenant({ scopeType: "source" });
|
|
89
|
+
// Each result has a .memberships array
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 4. Switch Scope
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const tree = await client.scope.getAvailable();
|
|
96
|
+
// tree.vendors[0].sources[0].clients[0]
|
|
97
|
+
|
|
98
|
+
const { accessToken, scopeContext } = await client.scope.switchScope("vendor", vendorId);
|
|
99
|
+
// Trusted runtime example: preserve the refresh token only in a server or secure mobile context
|
|
100
|
+
client.setTokens({ accessToken, refreshToken: client.getRefreshToken()! });
|
|
101
|
+
// JWT now includes scopeContext: { type, id, role, membershipId }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 5. Set Up Permission Groups
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const group = await client.permissionGroups.create(tenantId, "Operators", "Field operators");
|
|
108
|
+
|
|
109
|
+
await client.permissionGroups.addPermission(tenantId, group.id, {
|
|
110
|
+
appKey: "iqcapture",
|
|
111
|
+
nodeKey: "capture.operate",
|
|
112
|
+
effect: "allow",
|
|
113
|
+
weight: 10,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await client.permissionGroups.assignUserToGroup(tenantId, userId, group.id);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 6. Group Inheritance
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
await client.permissionGroups.addInheritance(tenantId, adminGroupId, baseGroupId);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 7. User Overrides
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
await client.permissionGroups.addUserOverride(tenantId, userId, {
|
|
129
|
+
appKey: "iqcapture",
|
|
130
|
+
nodeKey: "capture.delete",
|
|
131
|
+
effect: "deny",
|
|
132
|
+
weight: 100,
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 8. Check Effective Permissions
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const effective = await client.permissionGroups.getEffectivePermissions(
|
|
140
|
+
tenantId, userId, { appKey: "iqcapture" },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const check = await client.permissionGroups.checkPermission(
|
|
144
|
+
tenantId, userId, "iqcapture", "capture.operate",
|
|
145
|
+
);
|
|
146
|
+
// { allowed: true, appKey: "iqcapture", nodeKey: "capture.operate" }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Error Handling
|
|
150
|
+
|
|
151
|
+
| Error Code | Meaning | Recovery |
|
|
152
|
+
|------------|---------|----------|
|
|
153
|
+
| `NOT_FOUND` | Scope entity or user not found | Check IDs |
|
|
154
|
+
| `ALREADY_EXISTS` | Membership or group already exists | Check existing resources |
|
|
155
|
+
| `INSUFFICIENT_PERMISSIONS` | Caller lacks admin role | Requires `tenant_admin`+ |
|
|
156
|
+
| `VALIDATION_ERROR` | Invalid scope type or data | Check request format |
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await client.memberships.grant({
|
|
163
|
+
userId, roleName: "vendor_admin", scopeType: "vendor", scopeId: vendorId,
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.ALREADY_EXISTS) {
|
|
167
|
+
console.log("Membership already exists");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Complete Example
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { IQAuthClient } from "@iqauth/sdk";
|
|
176
|
+
|
|
177
|
+
const client = new IQAuthClient({
|
|
178
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
179
|
+
accessToken: adminToken,
|
|
180
|
+
refreshToken: adminRefreshToken,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
async function setupScopedAuth(tenantId: string, vendorId: string) {
|
|
184
|
+
await client.tenants.update(tenantId, { enableScopedAuth: true });
|
|
185
|
+
|
|
186
|
+
await client.memberships.grant({
|
|
187
|
+
userId: operatorUserId,
|
|
188
|
+
roleName: "user",
|
|
189
|
+
scopeType: "vendor",
|
|
190
|
+
scopeId: vendorId,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const group = await client.permissionGroups.create(tenantId, "Capture Operators");
|
|
194
|
+
|
|
195
|
+
await client.permissionGroups.addPermission(tenantId, group.id, {
|
|
196
|
+
appKey: "iqcapture",
|
|
197
|
+
nodeKey: "capture.operate",
|
|
198
|
+
effect: "allow",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await client.permissionGroups.assignUserToGroup(tenantId, operatorUserId, group.id);
|
|
202
|
+
|
|
203
|
+
const check = await client.permissionGroups.checkPermission(
|
|
204
|
+
tenantId, operatorUserId, "iqcapture", "capture.operate",
|
|
205
|
+
);
|
|
206
|
+
console.log("Can operate:", check.allowed);
|
|
207
|
+
|
|
208
|
+
const tree = await client.scope.getAvailable();
|
|
209
|
+
console.log("Available scopes:", JSON.stringify(tree, null, 2));
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Note: Scope routes are mounted under `/api/v1/auth`, not `/api/v1/scope`. See [DECISION-014](../../DECISIONS.md).
|
|
214
|
+
|
|
215
|
+
## API Reference
|
|
216
|
+
|
|
217
|
+
### MembershipsModule
|
|
218
|
+
|
|
219
|
+
| Method | HTTP | Path |
|
|
220
|
+
|--------|------|------|
|
|
221
|
+
| `grant(data)` | POST | `/api/v1/memberships` |
|
|
222
|
+
| `listForUser(userId, tenantId)` | GET | `/api/v1/users/:userId/memberships?tenantId=...` |
|
|
223
|
+
| `listForScope(type, id)` | GET | `/api/v1/memberships/scope/:type/:id` |
|
|
224
|
+
| `listForTenant(params?)` | GET | `/api/v1/memberships/tenant` |
|
|
225
|
+
| `update(id, data)` | PATCH | `/api/v1/memberships/:id` |
|
|
226
|
+
| `revoke(id)` | DELETE | `/api/v1/memberships/:id` |
|
|
227
|
+
|
|
228
|
+
### ScopeModule
|
|
229
|
+
|
|
230
|
+
| Method | HTTP | Path |
|
|
231
|
+
|--------|------|------|
|
|
232
|
+
| `getAvailable()` | GET | `/api/v1/auth/available-scopes` |
|
|
233
|
+
| `switchScope(type, id)` | POST | `/api/v1/auth/switch-scope` |
|
|
234
|
+
|
|
235
|
+
### PermissionGroupsModule
|
|
236
|
+
|
|
237
|
+
| Method | HTTP | Path |
|
|
238
|
+
|--------|------|------|
|
|
239
|
+
| `list(tenantId)` | GET | `/api/v1/tenants/:tenantId/permission-groups` |
|
|
240
|
+
| `create(tenantId, name, desc?)` | POST | `/api/v1/tenants/:tenantId/permission-groups` |
|
|
241
|
+
| `getPermissions(tenantId, groupId)` | GET | `.../:groupId/permissions` |
|
|
242
|
+
| `addPermission(tenantId, groupId, data)` | POST | `.../:groupId/permissions` |
|
|
243
|
+
| `removePermission(tenantId, groupId, permId)` | DELETE | `.../:groupId/permissions/:permId` |
|
|
244
|
+
| `addInheritance(tenantId, groupId, fromId)` | POST | `.../:groupId/inherit` |
|
|
245
|
+
| `assignUserToGroup(tenantId, userId, groupId)` | POST | `.../users/:userId/groups` |
|
|
246
|
+
| `getEffectivePermissions(tenantId, userId, params)` | GET | `.../users/:userId/permissions/effective` |
|
|
247
|
+
| `checkPermission(tenantId, userId, appKey, nodeKey)` | POST | `.../users/:userId/permissions/check` |
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Server Platform Integration
|
|
2
|
+
|
|
3
|
+
This guide is for trusted backend platforms that accept IQAuth bearer tokens or need to call IQAuth directly.
|
|
4
|
+
|
|
5
|
+
## Recommended Pattern
|
|
6
|
+
|
|
7
|
+
Use the server entry point and the provided middleware:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import express from "express";
|
|
11
|
+
import { createServerClient } from "@iqauth/sdk/server";
|
|
12
|
+
|
|
13
|
+
const client = createServerClient({
|
|
14
|
+
baseUrl: process.env.IQAUTH_BASE_URL!,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const app = express();
|
|
18
|
+
|
|
19
|
+
app.use("/api", client.middleware());
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Why this is the default
|
|
23
|
+
|
|
24
|
+
- it keeps token verification centralized
|
|
25
|
+
- it prevents each app from re-implementing JWT verification and drift-prone auth checks
|
|
26
|
+
- it gives the platform one canonical middleware contract
|
|
27
|
+
|
|
28
|
+
## Required Practices
|
|
29
|
+
|
|
30
|
+
- use `client.middleware()` unless your framework cannot support Express-style middleware
|
|
31
|
+
- enforce role and entitlement checks on the server, not only in the UI
|
|
32
|
+
- avoid shared multi-user token state
|
|
33
|
+
- keep service tokens and user tokens separate
|
|
34
|
+
|
|
35
|
+
## Typical Integration Shapes
|
|
36
|
+
|
|
37
|
+
### Resource server
|
|
38
|
+
|
|
39
|
+
- accepts incoming bearer tokens
|
|
40
|
+
- uses middleware to verify and attach auth context
|
|
41
|
+
- authorizes routes from verified claims
|
|
42
|
+
|
|
43
|
+
### Backend calling IQAuth APIs directly
|
|
44
|
+
|
|
45
|
+
- uses request-scoped or service-scoped tokens
|
|
46
|
+
- calls `client.users`, `client.sessions`, `client.permissions`, etc.
|
|
47
|
+
|
|
48
|
+
## Anti-Patterns
|
|
49
|
+
|
|
50
|
+
- re-implementing token verification without a concrete reason
|
|
51
|
+
- trusting browser role checks without server enforcement
|
|
52
|
+
- sharing one mutable token cache across unrelated user sessions
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Service Automation Integration
|
|
2
|
+
|
|
3
|
+
This guide is for workers, batch jobs, schedulers, and service-to-service integrations.
|
|
4
|
+
|
|
5
|
+
## Recommended Pattern
|
|
6
|
+
|
|
7
|
+
Prefer API keys.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createServiceClient } from "@iqauth/sdk/service";
|
|
11
|
+
|
|
12
|
+
const client = createServiceClient({
|
|
13
|
+
baseUrl: process.env.IQAUTH_BASE_URL!,
|
|
14
|
+
apiKey: process.env.IQAUTH_API_KEY!,
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Why API keys are preferred
|
|
19
|
+
|
|
20
|
+
- no interactive user lifecycle
|
|
21
|
+
- simpler rotation model
|
|
22
|
+
- lower risk than pretending a headless service is a browser or mobile app
|
|
23
|
+
|
|
24
|
+
## Required Practices
|
|
25
|
+
|
|
26
|
+
- scope service credentials as narrowly as possible
|
|
27
|
+
- document rotation and revocation ownership
|
|
28
|
+
- do not use interactive login flows unless a product requirement explicitly demands it
|
|
29
|
+
|
|
30
|
+
## When not to use this model
|
|
31
|
+
|
|
32
|
+
Do not use service credentials for:
|
|
33
|
+
|
|
34
|
+
- first-party browser apps
|
|
35
|
+
- end-user mobile login flows
|
|
36
|
+
- backend routes that should be enforcing user identity
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Session Management
|
|
2
|
+
|
|
3
|
+
Session handling differs by environment.
|
|
4
|
+
|
|
5
|
+
## Browser First-Party Apps
|
|
6
|
+
|
|
7
|
+
Use backend-managed sessions.
|
|
8
|
+
|
|
9
|
+
### Recommended pattern
|
|
10
|
+
|
|
11
|
+
- backend stores access and refresh tokens in `httpOnly` cookies
|
|
12
|
+
- frontend boots with `/auth/me`
|
|
13
|
+
- frontend calls protected APIs with cookies
|
|
14
|
+
- backend performs refresh
|
|
15
|
+
|
|
16
|
+
### Browser responsibilities
|
|
17
|
+
|
|
18
|
+
- display session state
|
|
19
|
+
- handle session-expired UX
|
|
20
|
+
- handle logout UX
|
|
21
|
+
|
|
22
|
+
### Browser non-goals
|
|
23
|
+
|
|
24
|
+
- do not persist refresh tokens in `localStorage`
|
|
25
|
+
- do not make the browser responsible for durable token lifecycle
|
|
26
|
+
|
|
27
|
+
## Server and Mobile Token-Owning Clients
|
|
28
|
+
|
|
29
|
+
If your runtime can safely own tokens, the SDK session APIs are appropriate.
|
|
30
|
+
|
|
31
|
+
## SDK Methods
|
|
32
|
+
|
|
33
|
+
| Module | Method | Description |
|
|
34
|
+
|--------|--------|-------------|
|
|
35
|
+
| `sessions` | `list()` | List active sessions |
|
|
36
|
+
| `sessions` | `revoke(sessionId)` | Terminate one session |
|
|
37
|
+
| `sessions` | `revokeAll()` | Terminate all sessions |
|
|
38
|
+
| `auth` | `logout()` | End current session |
|
|
39
|
+
| `auth` | `logoutAll()` | End all sessions for the current user |
|
|
40
|
+
|
|
41
|
+
## List Sessions
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const sessions = await client.sessions.list();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Identify Current Session
|
|
48
|
+
|
|
49
|
+
In token-owning environments, the current session ID is available from JWT claims:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const claims = client.tokens.getClaims(client.getAccessToken()!);
|
|
53
|
+
const currentSessionId = claims.sessionId;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Revoke a Session
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
await client.sessions.revoke(sessionId);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Revoke All Sessions
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const { terminatedCount } = await client.sessions.revokeAll();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Mobile Guidance
|
|
69
|
+
|
|
70
|
+
If mobile owns tokens:
|
|
71
|
+
|
|
72
|
+
- keep tokens in secure storage
|
|
73
|
+
- clear secure storage on logout
|
|
74
|
+
- treat `REFRESH_TOKEN_REUSED` or revocation as forced re-authentication
|
|
75
|
+
|
|
76
|
+
## Browser Guidance
|
|
77
|
+
|
|
78
|
+
If the browser is using a backend cookie session:
|
|
79
|
+
|
|
80
|
+
- list/revoke session APIs can still back an "active sessions" UI
|
|
81
|
+
- but logout and refresh should still be backend-driven
|
|
82
|
+
- the browser should not need raw refresh-token access to manage session lifecycle
|
|
83
|
+
|
|
84
|
+
## Error Handling
|
|
85
|
+
|
|
86
|
+
| Error Code | Meaning |
|
|
87
|
+
|--------|--------|
|
|
88
|
+
| `SESSION_INVALID` | Session not found |
|
|
89
|
+
| `SESSION_EXPIRED` | Session already expired |
|
|
90
|
+
| `TOKEN_INVALID` | Caller not authenticated |
|
|
91
|
+
| `TOKEN_REVOKED` | Session or token was revoked |
|
|
92
|
+
|
|
93
|
+
## Summary
|
|
94
|
+
|
|
95
|
+
- browser: session cookies, backend ownership
|
|
96
|
+
- mobile: secure storage
|
|
97
|
+
- server: direct tokens allowed
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Tenant Management
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Create, read, update, and delete tenants. Manage tenant users, password policies, MFA policies, and public branding. Handle tenant promotion to vendor status.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- `tenant_admin` role for tenant user management and policies
|
|
11
|
+
- `platform_admin` role for tenant creation/deletion and promotion
|
|
12
|
+
|
|
13
|
+
## Environment Note
|
|
14
|
+
|
|
15
|
+
Tenant-management examples are intended for trusted administrative runtimes. Keep these operations behind your backend session boundary for first-party web apps.
|
|
16
|
+
|
|
17
|
+
## SDK Methods
|
|
18
|
+
|
|
19
|
+
| Module | Method | Description |
|
|
20
|
+
|--------|--------|-------------|
|
|
21
|
+
| `tenants` | `get(tenantId)` | Get tenant info |
|
|
22
|
+
| `tenants` | `getCurrent(tenantId)` | Get tenant info (alias) |
|
|
23
|
+
| `tenants` | `list(params?)` | List tenants |
|
|
24
|
+
| `tenants` | `create(data)` | Create tenant |
|
|
25
|
+
| `tenants` | `update(tenantId, data)` | Update tenant |
|
|
26
|
+
| `tenants` | `delete(tenantId)` | Delete tenant |
|
|
27
|
+
| `tenants` | `promoteToVendor(tenantId, data)` | Promote to vendor |
|
|
28
|
+
| `tenants` | `getUsers(tenantId)` | List tenant users |
|
|
29
|
+
| `tenants` | `inviteUser(tenantId, data)` | Invite user to tenant |
|
|
30
|
+
| `tenants` | `changeUserRole(tenantId, userId, role)` | Change user role |
|
|
31
|
+
| `tenants` | `migrateUser(tenantId, userId, data)` | Migrate user between tenants |
|
|
32
|
+
| `tenants` | `removeUser(tenantId, userId)` | Remove user from tenant |
|
|
33
|
+
| `tenants` | `getPasswordPolicy(tenantId)` | Get password policy |
|
|
34
|
+
| `tenants` | `updatePasswordPolicy(tenantId, data)` | Update password policy |
|
|
35
|
+
| `tenants` | `getMfaPolicies(tenantId)` | Get MFA policies |
|
|
36
|
+
| `tenants` | `updateMfaPolicy(tenantId, role, data)` | Update MFA policy for role |
|
|
37
|
+
| `tenants` | `getPublicBranding(params?)` | Get public branding (no auth) |
|
|
38
|
+
| `tenants` | `getPublicBrandingBySlug(slug)` | Get branding by vendor slug (no auth) |
|
|
39
|
+
|
|
40
|
+
## Step-by-Step
|
|
41
|
+
|
|
42
|
+
### 1. Get a Tenant
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const tenant = await client.tenants.get(tenantId);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Returns `TenantInfo`:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
interface TenantInfo {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
slug: string;
|
|
55
|
+
plan?: string;
|
|
56
|
+
isActive?: boolean;
|
|
57
|
+
vendorId?: string | null;
|
|
58
|
+
vendorName?: string | null;
|
|
59
|
+
allowedDomains?: string[] | null;
|
|
60
|
+
autoProvisionRole?: string | null;
|
|
61
|
+
enableScopedAuth?: boolean;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Note: `getCurrent(tenantId)` requires explicit `tenantId`. See [DECISION-007](../../DECISIONS.md).
|
|
66
|
+
|
|
67
|
+
### 2. Create a Tenant
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const tenant = await client.tenants.create({
|
|
71
|
+
name: "Acme Corp",
|
|
72
|
+
slug: "acme-corp",
|
|
73
|
+
vendorId: "vendor-uuid",
|
|
74
|
+
allowedDomains: ["acme.com"],
|
|
75
|
+
autoProvisionRole: "user",
|
|
76
|
+
plan: "enterprise",
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Update a Tenant
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
await client.tenants.update(tenantId, {
|
|
84
|
+
name: "Acme Corporation",
|
|
85
|
+
enableScopedAuth: true,
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 4. Manage Users
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const users = await client.tenants.getUsers(tenantId);
|
|
93
|
+
|
|
94
|
+
await client.tenants.inviteUser(tenantId, {
|
|
95
|
+
email: "new@example.com",
|
|
96
|
+
role: "user",
|
|
97
|
+
products: ["iqcapture"],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await client.tenants.changeUserRole(tenantId, userId, "tenant_admin");
|
|
101
|
+
|
|
102
|
+
await client.tenants.migrateUser(tenantId, userId, {
|
|
103
|
+
targetTenantId: "new-tenant-uuid",
|
|
104
|
+
role: "user",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await client.tenants.removeUser(tenantId, userId);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 5. Password and MFA Policies
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
await client.tenants.updatePasswordPolicy(tenantId, {
|
|
114
|
+
minLength: 12,
|
|
115
|
+
maxAgeDays: 90,
|
|
116
|
+
reuseCount: 5,
|
|
117
|
+
maxFailedAttempts: 5,
|
|
118
|
+
lockoutDurationMinutes: 30,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await client.tenants.updateMfaPolicy(tenantId, "tenant_admin", {
|
|
122
|
+
required: true,
|
|
123
|
+
methods: ["totp", "sms"],
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 6. Public Branding (No Auth)
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const vendorBranding = await client.tenants.getPublicBranding({ vendor: "acme" });
|
|
131
|
+
const slugBranding = await client.tenants.getPublicBrandingBySlug("acme-disposal");
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Error Handling
|
|
135
|
+
|
|
136
|
+
| Error Code | Meaning | Recovery |
|
|
137
|
+
|------------|---------|----------|
|
|
138
|
+
| `NOT_FOUND` | Tenant does not exist | Check tenant ID |
|
|
139
|
+
| `ALREADY_EXISTS` | Tenant slug exists | Use different slug |
|
|
140
|
+
| `INSUFFICIENT_PERMISSIONS` | Caller lacks admin role | Requires `tenant_admin` or `platform_admin` |
|
|
141
|
+
| `VALIDATION_ERROR` | Invalid request data | Check required fields |
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await client.tenants.create({ name: "Test", slug: "test" });
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.ALREADY_EXISTS) {
|
|
150
|
+
console.log("Tenant slug already taken");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Complete Example
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { IQAuthClient } from "@iqauth/sdk";
|
|
159
|
+
|
|
160
|
+
const client = new IQAuthClient({
|
|
161
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
162
|
+
accessToken: platformAdminToken,
|
|
163
|
+
refreshToken: platformAdminRefreshToken,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
async function setupNewTenant(name: string, slug: string, adminEmail: string) {
|
|
167
|
+
const tenant = await client.tenants.create({
|
|
168
|
+
name,
|
|
169
|
+
slug,
|
|
170
|
+
plan: "standard",
|
|
171
|
+
allowedDomains: [slug + ".com"],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await client.tenants.updatePasswordPolicy(tenant.id, {
|
|
175
|
+
minLength: 10,
|
|
176
|
+
maxAgeDays: 90,
|
|
177
|
+
maxFailedAttempts: 5,
|
|
178
|
+
lockoutDurationMinutes: 15,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await client.tenants.updateMfaPolicy(tenant.id, "tenant_admin", {
|
|
182
|
+
required: true,
|
|
183
|
+
methods: ["totp"],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await client.tenants.inviteUser(tenant.id, {
|
|
187
|
+
email: adminEmail,
|
|
188
|
+
role: "tenant_admin",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
console.log("Tenant created:", tenant.id, tenant.slug);
|
|
192
|
+
return tenant;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## API Reference
|
|
197
|
+
|
|
198
|
+
| Method | HTTP | Path |
|
|
199
|
+
|--------|------|------|
|
|
200
|
+
| `get(id)` / `getCurrent(id)` | GET | `/api/v1/tenants/:id` |
|
|
201
|
+
| `list(params?)` | GET | `/api/v1/tenants` |
|
|
202
|
+
| `create(data)` | POST | `/api/v1/tenants` |
|
|
203
|
+
| `update(id, data)` | PATCH | `/api/v1/tenants/:id` |
|
|
204
|
+
| `delete(id)` | DELETE | `/api/v1/tenants/:id` |
|
|
205
|
+
| `promoteToVendor(id, data)` | POST | `/api/v1/tenants/:id/promote-to-vendor` |
|
|
206
|
+
| `getUsers(id)` | GET | `/api/v1/tenants/:id/users` |
|
|
207
|
+
| `inviteUser(id, data)` | POST | `/api/v1/tenants/:id/users/invite` |
|
|
208
|
+
| `changeUserRole(id, userId, role)` | PATCH | `/api/v1/tenants/:id/users/:userId/role` |
|
|
209
|
+
| `migrateUser(id, userId, data)` | POST | `/api/v1/tenants/:id/users/:userId/migrate` |
|
|
210
|
+
| `removeUser(id, userId)` | DELETE | `/api/v1/tenants/:id/users/:userId` |
|
|
211
|
+
| `getPasswordPolicy(id)` | GET | `/api/v1/tenants/:id/password-policy` |
|
|
212
|
+
| `updatePasswordPolicy(id, data)` | PATCH | `/api/v1/tenants/:id/password-policy` |
|
|
213
|
+
| `getMfaPolicies(id)` | GET | `/api/v1/tenants/:id/mfa-policies` |
|
|
214
|
+
| `updateMfaPolicy(id, role, data)` | PATCH | `/api/v1/tenants/:id/mfa-policies/:role` |
|
|
215
|
+
| `getPublicBranding(params?)` | GET | `/api/public/branding` |
|
|
216
|
+
| `getPublicBrandingBySlug(slug)` | GET | `/api/public/branding/by-slug/:slug` |
|