@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,205 @@
|
|
|
1
|
+
# Middleware Reference
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Protect Express routes by verifying Bearer tokens via RS256/JWKS. Supports optional auth, role checks, entitlement checks, and custom error handlers.
|
|
6
|
+
|
|
7
|
+
This is the recommended integration path for platforms using the SDK in Express-style resource servers.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- `@iqauth/sdk` installed
|
|
12
|
+
- Express.js application
|
|
13
|
+
- IQAuthService base URL for JWKS verification
|
|
14
|
+
|
|
15
|
+
## SDK Methods
|
|
16
|
+
|
|
17
|
+
| Export | Description |
|
|
18
|
+
|--------|-------------|
|
|
19
|
+
| `iqAuthMiddleware(client, options?)` | Creates Express middleware |
|
|
20
|
+
|
|
21
|
+
### Options
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
interface ExpressMiddlewareOptions {
|
|
25
|
+
requireAuth?: boolean; // Default: true
|
|
26
|
+
requiredRoles?: string[]; // OR logic
|
|
27
|
+
requiredEntitlements?: string[]; // AND logic
|
|
28
|
+
onUnauthorized?: (res, reason) => void;
|
|
29
|
+
onForbidden?: (res, reason) => void;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Step-by-Step
|
|
34
|
+
|
|
35
|
+
### 1. Basic Protection
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { createServerClient } from "@iqauth/sdk/server";
|
|
39
|
+
|
|
40
|
+
const client = createServerClient({
|
|
41
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.use("/api", client.middleware());
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Access Claims
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
app.get("/api/me", (req, res) => {
|
|
51
|
+
res.json({
|
|
52
|
+
userId: req.auth!.sub,
|
|
53
|
+
email: req.auth!.email,
|
|
54
|
+
tenantId: req.auth!.tenantId,
|
|
55
|
+
roles: req.auth!.roles,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Claims are attached to `req.auth` (not `req.user`). See [DECISION-006](../../DECISIONS.md).
|
|
61
|
+
|
|
62
|
+
### 3. Optional Auth
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
app.use(client.middleware({ requireAuth: false }));
|
|
66
|
+
|
|
67
|
+
app.get("/api/resource", (req, res) => {
|
|
68
|
+
if (req.auth) {
|
|
69
|
+
// Authenticated
|
|
70
|
+
} else {
|
|
71
|
+
// Anonymous
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 4. Role Checks (OR Logic)
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
app.use("/admin", client.middleware({
|
|
80
|
+
requiredRoles: ["tenant_admin", "platform_admin"],
|
|
81
|
+
}));
|
|
82
|
+
// User needs ANY ONE of the listed roles
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 5. Entitlement Checks (AND Logic)
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
app.use("/capture", client.middleware({
|
|
89
|
+
requiredEntitlements: ["iqcapture"],
|
|
90
|
+
}));
|
|
91
|
+
// User needs ALL listed entitlements
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 6. Custom Error Handlers
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
app.use(client.middleware({
|
|
98
|
+
onUnauthorized: (res, reason) => {
|
|
99
|
+
(res as any).status(401).json({ error: "Login required", detail: reason });
|
|
100
|
+
},
|
|
101
|
+
onForbidden: (res, reason) => {
|
|
102
|
+
(res as any).status(403).json({ error: "Access denied", detail: reason });
|
|
103
|
+
},
|
|
104
|
+
}));
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### How It Works
|
|
108
|
+
|
|
109
|
+
1. Extracts token from `Authorization: Bearer <token>` header
|
|
110
|
+
2. Verifies signature using RS256 via JWKS (`/.well-known/jwks.json`)
|
|
111
|
+
3. Validates issuer (`auth.dispositioniq.com`) and audience
|
|
112
|
+
4. Checks expiration
|
|
113
|
+
5. Attaches decoded claims to `req.auth`
|
|
114
|
+
6. Checks `requiredRoles` (OR logic) if specified
|
|
115
|
+
7. Checks `requiredEntitlements` (AND logic) if specified
|
|
116
|
+
|
|
117
|
+
### TypeScript Augmentation
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
declare global {
|
|
121
|
+
namespace Express {
|
|
122
|
+
interface Request {
|
|
123
|
+
auth?: JwtClaims;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Error Handling
|
|
130
|
+
|
|
131
|
+
### Default 401 Response
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"success": false,
|
|
136
|
+
"error": {
|
|
137
|
+
"code": "TOKEN_INVALID",
|
|
138
|
+
"message": "Missing or invalid authorization header"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Default 403 Response
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"success": false,
|
|
148
|
+
"error": {
|
|
149
|
+
"code": "INSUFFICIENT_PERMISSIONS",
|
|
150
|
+
"message": "Missing required role. Required one of: tenant_admin, platform_admin"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Complete Example
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import express from "express";
|
|
159
|
+
import { createServerClient } from "@iqauth/sdk/server";
|
|
160
|
+
|
|
161
|
+
const app = express();
|
|
162
|
+
const client = createServerClient({
|
|
163
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Public routes
|
|
167
|
+
app.get("/health", (req, res) => res.json({ ok: true }));
|
|
168
|
+
|
|
169
|
+
// Any authenticated user
|
|
170
|
+
app.use("/api", client.middleware());
|
|
171
|
+
|
|
172
|
+
app.get("/api/me", (req, res) => {
|
|
173
|
+
res.json({
|
|
174
|
+
userId: req.auth!.sub,
|
|
175
|
+
email: req.auth!.email,
|
|
176
|
+
tenantId: req.auth!.tenantId,
|
|
177
|
+
roles: req.auth!.roles,
|
|
178
|
+
entitlements: req.auth!.entitlements,
|
|
179
|
+
scopeContext: req.auth!.scopeContext,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Admin only
|
|
184
|
+
app.use("/api/admin", client.middleware({
|
|
185
|
+
requiredRoles: ["tenant_admin", "platform_admin"],
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
app.get("/api/admin/users", async (req, res) => {
|
|
189
|
+
const users = await client.users.list({ tenantId: req.auth!.tenantId });
|
|
190
|
+
res.json(users);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Product-gated with custom error
|
|
194
|
+
app.use("/api/capture", client.middleware({
|
|
195
|
+
requiredEntitlements: ["iqcapture"],
|
|
196
|
+
onForbidden: (res, reason) => {
|
|
197
|
+
(res as any).status(403).json({
|
|
198
|
+
error: "IQCapture license required",
|
|
199
|
+
upgrade: "https://dispositioniq.com/pricing",
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
app.listen(3000);
|
|
205
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Mobile / Native Guide
|
|
2
|
+
|
|
3
|
+
Use this guide for iOS, Android, React Native, or Expo-style native applications.
|
|
4
|
+
|
|
5
|
+
## Recommended Model
|
|
6
|
+
|
|
7
|
+
Native apps are public clients.
|
|
8
|
+
|
|
9
|
+
Use:
|
|
10
|
+
|
|
11
|
+
- authorization code flow
|
|
12
|
+
- PKCE
|
|
13
|
+
- system browser authentication
|
|
14
|
+
- secure OS-backed token storage
|
|
15
|
+
|
|
16
|
+
Do not embed a client secret in the app.
|
|
17
|
+
|
|
18
|
+
## Required Security Elements
|
|
19
|
+
|
|
20
|
+
Every mobile login should generate:
|
|
21
|
+
|
|
22
|
+
- `state`
|
|
23
|
+
- `nonce`
|
|
24
|
+
- PKCE code verifier
|
|
25
|
+
- PKCE code challenge with `S256`
|
|
26
|
+
|
|
27
|
+
Validate `state` on return before exchanging the authorization code.
|
|
28
|
+
|
|
29
|
+
## Redirect Handling
|
|
30
|
+
|
|
31
|
+
Supported callback patterns should be:
|
|
32
|
+
|
|
33
|
+
- deep links
|
|
34
|
+
- universal links
|
|
35
|
+
- app links
|
|
36
|
+
|
|
37
|
+
The redirect URI must be registered and validated by IQAuth.
|
|
38
|
+
|
|
39
|
+
## Token Storage
|
|
40
|
+
|
|
41
|
+
Allowed:
|
|
42
|
+
|
|
43
|
+
- iOS Keychain
|
|
44
|
+
- Android Keystore
|
|
45
|
+
- secure-storage wrappers over those primitives
|
|
46
|
+
|
|
47
|
+
Not acceptable:
|
|
48
|
+
|
|
49
|
+
- plaintext async storage
|
|
50
|
+
- browser-style `localStorage` assumptions
|
|
51
|
+
- shipping long-lived credentials in bundled config
|
|
52
|
+
|
|
53
|
+
## Session Lifecycle
|
|
54
|
+
|
|
55
|
+
Typical mobile flow:
|
|
56
|
+
|
|
57
|
+
1. open the authorization URL in the system browser
|
|
58
|
+
2. user authenticates
|
|
59
|
+
3. app receives authorization code
|
|
60
|
+
4. exchange code for tokens
|
|
61
|
+
5. persist tokens in secure storage
|
|
62
|
+
6. attach access token to API requests
|
|
63
|
+
7. refresh when needed
|
|
64
|
+
8. clear secure storage on logout or revocation
|
|
65
|
+
|
|
66
|
+
## SDK Fit
|
|
67
|
+
|
|
68
|
+
The current SDK can be used in mobile only if:
|
|
69
|
+
|
|
70
|
+
- login uses PKCE
|
|
71
|
+
- tokens live in secure storage
|
|
72
|
+
- the app treats refresh tokens as protected credentials
|
|
73
|
+
|
|
74
|
+
Example shape:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const client = new IQAuthClient({
|
|
78
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
79
|
+
accessToken: await secureStore.get("accessToken"),
|
|
80
|
+
refreshToken: await secureStore.get("refreshToken"),
|
|
81
|
+
onTokenRefresh: async (tokens) => {
|
|
82
|
+
await secureStore.set("accessToken", tokens.accessToken);
|
|
83
|
+
await secureStore.set("refreshToken", tokens.refreshToken);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Logout and Revocation
|
|
89
|
+
|
|
90
|
+
On logout:
|
|
91
|
+
|
|
92
|
+
- call the logout endpoint
|
|
93
|
+
- clear secure storage
|
|
94
|
+
- clear in-memory auth state
|
|
95
|
+
|
|
96
|
+
On reuse detection or revocation:
|
|
97
|
+
|
|
98
|
+
- force full re-authentication
|
|
99
|
+
- do not silently continue with cached session assumptions
|
|
100
|
+
|
|
101
|
+
## Summary
|
|
102
|
+
|
|
103
|
+
Mobile should not reuse the browser cookie-session model.
|
|
104
|
+
|
|
105
|
+
The correct model is:
|
|
106
|
+
|
|
107
|
+
- PKCE login
|
|
108
|
+
- secure storage
|
|
109
|
+
- deep link callback
|
|
110
|
+
- explicit logout and revocation handling
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Roles & Permissions
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Check user roles and entitlements from JWT claims (client-side, no network), and manage role definitions and assignments on the server.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- For `permissions` module: authenticated user with tokens set on client
|
|
11
|
+
- For `roles` module: `tenant_admin` or `platform_admin` role
|
|
12
|
+
|
|
13
|
+
## Environment Note
|
|
14
|
+
|
|
15
|
+
Direct token examples in this guide assume a trusted runtime, secure mobile storage, or server-side context.
|
|
16
|
+
|
|
17
|
+
For first-party browser apps, consume roles and permissions through your backend session model.
|
|
18
|
+
|
|
19
|
+
## SDK Methods
|
|
20
|
+
|
|
21
|
+
### PermissionsModule (Client-Side, No Network)
|
|
22
|
+
|
|
23
|
+
| Method | Description |
|
|
24
|
+
|--------|-------------|
|
|
25
|
+
| `getRoles()` | Get roles from JWT → `string[]` |
|
|
26
|
+
| `getEntitlements()` | Get entitlements from JWT → `string[]` |
|
|
27
|
+
| `hasRole(role)` | Check single role → `boolean` |
|
|
28
|
+
| `hasEntitlement(ent)` | Check single entitlement → `boolean` |
|
|
29
|
+
| `hasAllRoles(roles)` | All roles present (AND) → `boolean` |
|
|
30
|
+
| `hasAnyRole(roles)` | Any role present (OR) → `boolean` |
|
|
31
|
+
| `hasAllEntitlements(ents)` | All entitlements (AND) → `boolean` |
|
|
32
|
+
| `hasAnyEntitlement(ents)` | Any entitlement (OR) → `boolean` |
|
|
33
|
+
|
|
34
|
+
### RolesModule (Server-Side API)
|
|
35
|
+
|
|
36
|
+
| Method | Description |
|
|
37
|
+
|--------|-------------|
|
|
38
|
+
| `list(tenantId)` | List roles → `Role[]` |
|
|
39
|
+
| `create(tenantId, data)` | Create role → `Role` |
|
|
40
|
+
| `update(tenantId, roleId, data)` | Update role → `Role` |
|
|
41
|
+
| `delete(tenantId, roleId)` | Delete role |
|
|
42
|
+
| `getUserRoles(tenantId, userId)` | Get user's roles → `Role[]` |
|
|
43
|
+
| `assignRole(tenantId, userId, data)` | Assign role → `UserRoleAssignment` |
|
|
44
|
+
| `removeRole(tenantId, userId, roleId)` | Remove role |
|
|
45
|
+
|
|
46
|
+
## Step-by-Step
|
|
47
|
+
|
|
48
|
+
### 1. Check Roles/Entitlements (Client-Side)
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
client.permissions.getRoles(); // ["tenant_admin", "user"]
|
|
52
|
+
client.permissions.getEntitlements(); // ["iqcapture", "iqreuse"]
|
|
53
|
+
|
|
54
|
+
client.permissions.hasRole("tenant_admin"); // true
|
|
55
|
+
client.permissions.hasEntitlement("iqcapture"); // true
|
|
56
|
+
client.permissions.hasAllRoles(["tenant_admin", "vendor_admin"]); // false
|
|
57
|
+
client.permissions.hasAnyRole(["tenant_admin", "vendor_admin"]); // true
|
|
58
|
+
client.permissions.hasAllEntitlements(["iqcapture", "iqreuse"]); // true
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
These read from the current access token's claims. If no token is set, arrays return `[]` and checks return `false`.
|
|
62
|
+
|
|
63
|
+
### 2. System Roles
|
|
64
|
+
|
|
65
|
+
| Role | Level | Scope |
|
|
66
|
+
|------|-------|-------|
|
|
67
|
+
| `platform_admin` | Global | All tenants, all operations |
|
|
68
|
+
| `tenant_admin` | Tenant | Full control within a tenant |
|
|
69
|
+
| `vendor_admin` | Vendor | Manage vendor and its sources/clients |
|
|
70
|
+
| `user` | User | Standard user operations |
|
|
71
|
+
|
|
72
|
+
### 3. Create Custom Roles
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
const role = await client.roles.create(tenantId, {
|
|
76
|
+
name: "site_operator",
|
|
77
|
+
description: "Operates capture equipment at a site",
|
|
78
|
+
hierarchyLevel: 20,
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 4. Assign Roles
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const assignment = await client.roles.assignRole(tenantId, userId, {
|
|
86
|
+
roleName: "site_operator",
|
|
87
|
+
scopeType: "source", // optional V2 scoping
|
|
88
|
+
scopeId: sourceId, // optional V2 scoping
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 5. Remove Roles
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
await client.roles.removeRole(tenantId, userId, roleId);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Error Handling
|
|
99
|
+
|
|
100
|
+
| Error Code | Meaning | Recovery |
|
|
101
|
+
|------------|---------|----------|
|
|
102
|
+
| `INSUFFICIENT_PERMISSIONS` | Caller lacks admin role | Requires `tenant_admin` |
|
|
103
|
+
| `NOT_FOUND` | Role or user not found | Check IDs |
|
|
104
|
+
| `ALREADY_EXISTS` | Role name exists in tenant | Use different name |
|
|
105
|
+
| `VALIDATION_ERROR` | Invalid role data | Check required fields |
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await client.roles.create(tenantId, { name: "admin", hierarchyLevel: 50 });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.ALREADY_EXISTS) {
|
|
114
|
+
console.log("Role already exists");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 6. Permission Groups (V2)
|
|
120
|
+
|
|
121
|
+
Permission groups provide fine-grained access control beyond flat roles. See also [Scoped Authorization](./scoped-authorization.md).
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// Create a group
|
|
125
|
+
const group = await client.permissionGroups.create(tenantId, "Operators", "Field operators");
|
|
126
|
+
|
|
127
|
+
// Add permission to group
|
|
128
|
+
await client.permissionGroups.addPermission(tenantId, group.id, {
|
|
129
|
+
appKey: "iqcapture",
|
|
130
|
+
nodeKey: "capture.operate",
|
|
131
|
+
effect: "allow",
|
|
132
|
+
weight: 10,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Assign user to group
|
|
136
|
+
await client.permissionGroups.assignUserToGroup(tenantId, userId, group.id);
|
|
137
|
+
|
|
138
|
+
// Check effective permissions
|
|
139
|
+
const effective = await client.permissionGroups.getEffectivePermissions(
|
|
140
|
+
tenantId, userId, { appKey: "iqcapture" },
|
|
141
|
+
);
|
|
142
|
+
console.log("Effective permissions:", effective);
|
|
143
|
+
|
|
144
|
+
// Check a single permission
|
|
145
|
+
const check = await client.permissionGroups.checkPermission(
|
|
146
|
+
tenantId, userId, "iqcapture", "capture.operate",
|
|
147
|
+
);
|
|
148
|
+
console.log("Can operate:", check.allowed);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Complete Example
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { IQAuthClient, IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
155
|
+
|
|
156
|
+
const client = new IQAuthClient({
|
|
157
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
158
|
+
accessToken: adminToken,
|
|
159
|
+
refreshToken: adminRefreshToken,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
async function setupRolesAndPermissions(tenantId: string, operatorUserId: string) {
|
|
163
|
+
// V1: Create and assign flat roles
|
|
164
|
+
const operator = await client.roles.create(tenantId, {
|
|
165
|
+
name: "site_operator",
|
|
166
|
+
description: "Operates equipment at a facility",
|
|
167
|
+
hierarchyLevel: 20,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await client.roles.assignRole(tenantId, operatorUserId, { roleName: "site_operator" });
|
|
171
|
+
const userRoles = await client.roles.getUserRoles(tenantId, operatorUserId);
|
|
172
|
+
console.log("Operator roles:", userRoles.map(r => r.name));
|
|
173
|
+
|
|
174
|
+
// V2: Fine-grained permission groups
|
|
175
|
+
const group = await client.permissionGroups.create(tenantId, "Capture Operators");
|
|
176
|
+
|
|
177
|
+
await client.permissionGroups.addPermission(tenantId, group.id, {
|
|
178
|
+
appKey: "iqcapture",
|
|
179
|
+
nodeKey: "capture.operate",
|
|
180
|
+
effect: "allow",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await client.permissionGroups.assignUserToGroup(tenantId, operatorUserId, group.id);
|
|
184
|
+
|
|
185
|
+
const check = await client.permissionGroups.checkPermission(
|
|
186
|
+
tenantId, operatorUserId, "iqcapture", "capture.operate",
|
|
187
|
+
);
|
|
188
|
+
console.log("Can operate:", check.allowed); // true
|
|
189
|
+
|
|
190
|
+
// Client-side checks (reads from JWT, no network)
|
|
191
|
+
const isAdmin = client.permissions.hasAnyRole(["tenant_admin", "platform_admin"]);
|
|
192
|
+
const hasCapture = client.permissions.hasEntitlement("iqcapture");
|
|
193
|
+
console.log("Is admin:", isAdmin, "Has capture:", hasCapture);
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## API Reference
|
|
198
|
+
|
|
199
|
+
### RolesModule
|
|
200
|
+
|
|
201
|
+
| Method | HTTP | Path |
|
|
202
|
+
|--------|------|------|
|
|
203
|
+
| `list(tenantId)` | GET | `/api/v1/tenants/:tenantId/roles` |
|
|
204
|
+
| `create(tenantId, data)` | POST | `/api/v1/tenants/:tenantId/roles` |
|
|
205
|
+
| `update(tenantId, roleId, data)` | PATCH | `/api/v1/tenants/:tenantId/roles/:roleId` |
|
|
206
|
+
| `delete(tenantId, roleId)` | DELETE | `/api/v1/tenants/:tenantId/roles/:roleId` |
|
|
207
|
+
| `getUserRoles(tenantId, userId)` | GET | `/api/v1/tenants/:tenantId/users/:userId/roles` |
|
|
208
|
+
| `assignRole(tenantId, userId, data)` | POST | `/api/v1/tenants/:tenantId/users/:userId/roles` |
|
|
209
|
+
| `removeRole(tenantId, userId, roleId)` | DELETE | `/api/v1/tenants/:tenantId/users/:userId/roles/:roleId` |
|
|
210
|
+
|
|
211
|
+
### PermissionGroupsModule
|
|
212
|
+
|
|
213
|
+
| Method | HTTP | Path |
|
|
214
|
+
|--------|------|------|
|
|
215
|
+
| `list(tenantId)` | GET | `/api/v1/tenants/:tenantId/permission-groups` |
|
|
216
|
+
| `create(tenantId, name, desc?)` | POST | `/api/v1/tenants/:tenantId/permission-groups` |
|
|
217
|
+
| `addPermission(tenantId, groupId, data)` | POST | `.../:groupId/permissions` |
|
|
218
|
+
| `assignUserToGroup(tenantId, userId, groupId)` | POST | `.../users/:userId/groups` |
|
|
219
|
+
| `getEffectivePermissions(tenantId, userId, params)` | GET | `.../users/:userId/permissions/effective` |
|
|
220
|
+
| `checkPermission(tenantId, userId, appKey, nodeKey)` | POST | `.../users/:userId/permissions/check` |
|