@iqauth/sdk 2.0.3 → 2.0.5
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/README.md +384 -181
- package/dist/browser-session.d.mts +1 -1
- package/dist/browser-session.d.ts +1 -1
- package/dist/browser-session.js +4 -1
- package/dist/browser-session.mjs +1 -1
- package/dist/{chunk-JQWYIIIS.mjs → chunk-MDUHPQMM.mjs} +4 -1
- package/dist/{chunk-73R6BEGO.mjs → chunk-ZESHDJDU.mjs} +1 -1
- package/dist/{client-CggvJmmm.d.ts → client-DXbHb2ul.d.ts} +1 -1
- package/dist/{client-C1DXfB8Z.d.mts → client-Dv4v92Mj.d.mts} +1 -1
- package/dist/{express-BKAXB5Nl.d.ts → express-B4o3P8vK.d.ts} +1 -1
- package/dist/{express-CpfyYTmw.d.mts → express-BZmF1llh.d.mts} +1 -1
- package/dist/express.d.mts +3 -3
- package/dist/express.d.ts +3 -3
- package/dist/express.js +4 -1
- package/dist/express.mjs +2 -2
- package/dist/fastify.d.mts +6 -0
- package/dist/fastify.d.ts +6 -0
- package/dist/fastify.js +21 -3
- package/dist/fastify.mjs +18 -3
- package/dist/hono.js +4 -1
- package/dist/hono.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -1
- package/dist/index.mjs +2 -2
- package/dist/mobile.d.mts +1 -1
- package/dist/mobile.d.ts +1 -1
- package/dist/mobile.js +4 -1
- package/dist/mobile.mjs +1 -1
- package/dist/next.js +4 -1
- package/dist/next.mjs +1 -1
- package/dist/react.d.mts +7 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.js +147 -33
- package/dist/react.mjs +147 -33
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +4 -1
- package/dist/server.mjs +2 -2
- package/dist/service.d.mts +1 -1
- package/dist/service.d.ts +1 -1
- package/dist/service.js +4 -1
- package/dist/service.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,287 +1,490 @@
|
|
|
1
1
|
# @iqauth/sdk
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@iqauth/sdk)
|
|
4
|
+
[](https://nodejs.org)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://www.npmjs.com/package/@iqauth/sdk)
|
|
7
|
+
[](#license)
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
The canonical TypeScript SDK for **IQAuthService** — DispositionIQ's multi-tenant identity and authorization platform. One package covers the React client, the four major Node frameworks (Express, Fastify, Hono, Next.js), native mobile, and headless service automation.
|
|
10
|
+
|
|
11
|
+
> **New in 2.0.3** — `SessionManager` gains `serverManagedSession: true` for confidential-client apps, and the cookie-managed `/refresh` helper no longer wipes cookies on transient failures. See [What's new in 2.0.3](#whats-new-in-203).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Table of contents
|
|
16
|
+
|
|
17
|
+
- [Install](#install)
|
|
18
|
+
- [Five-line integration](#five-line-integration)
|
|
19
|
+
- [Pick your environment](#pick-your-environment)
|
|
20
|
+
- [What's new in 2.0.3](#whats-new-in-203)
|
|
21
|
+
- [Browser apps with a backend (recommended)](#browser-apps-with-a-backend-recommended)
|
|
22
|
+
- [Server reference (Express, Fastify, Hono, Next.js)](#server-reference)
|
|
23
|
+
- [Token verification without a framework adapter](#token-verification-without-a-framework-adapter)
|
|
24
|
+
- [Native mobile (PKCE)](#native-mobile-pkce)
|
|
25
|
+
- [Service automation / API keys](#service-automation--api-keys)
|
|
26
|
+
- [Hosted auth pages and branding](#hosted-auth-pages-and-branding)
|
|
27
|
+
- [CLI](#cli)
|
|
28
|
+
- [Error handling](#error-handling)
|
|
29
|
+
- [Troubleshooting](#troubleshooting)
|
|
30
|
+
- [Bundled docs](#bundled-docs)
|
|
31
|
+
- [License](#license)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @iqauth/sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- Node **>= 18** (the SDK uses native `fetch` and Web Crypto).
|
|
42
|
+
- Ships **CJS + ESM + .d.ts** for every entry point.
|
|
43
|
+
- React is an **optional** peer (`>=18`). You only need it if you import from `@iqauth/sdk/react`.
|
|
44
|
+
|
|
45
|
+
You'll need two values from the IQAuth admin dashboard for any app you integrate:
|
|
46
|
+
|
|
47
|
+
- **Publishable key** (`pk_live_…` / `pk_test_…`) — safe to ship to the browser. Self-describes `{iss, appId, tenantId, kid}`, so the SDK auto-discovers your issuer; you almost never need to set it manually.
|
|
48
|
+
- **Secret key** (`sk_live_…` / `sk_test_…`) — server-only. Keep it in env vars, never in client code.
|
|
49
|
+
|
|
50
|
+
Create both in one call from the admin Quickstart wizard, or run `npx iqauth init` (see [CLI](#cli)).
|
|
51
|
+
|
|
52
|
+
---
|
|
9
53
|
|
|
10
54
|
## Five-line integration
|
|
11
55
|
|
|
12
|
-
|
|
56
|
+
### React (browser)
|
|
57
|
+
|
|
13
58
|
```tsx
|
|
14
59
|
import { IQAuthProvider, SignedIn, SignedOut, RedirectToSignIn } from "@iqauth/sdk/react";
|
|
15
60
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
61
|
+
export default function App() {
|
|
62
|
+
return (
|
|
63
|
+
<IQAuthProvider publishableKey={import.meta.env.VITE_IQAUTH_PUBLISHABLE_KEY}>
|
|
64
|
+
<SignedIn><Dashboard /></SignedIn>
|
|
65
|
+
<SignedOut><RedirectToSignIn /></SignedOut>
|
|
66
|
+
</IQAuthProvider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
20
69
|
```
|
|
21
70
|
|
|
22
|
-
|
|
23
|
-
|
|
71
|
+
Available hooks: `useUser()`, `useSession()`, `useAuth()`, `useOrganization()`. Each returns `{ data, isLoading, error }`.
|
|
72
|
+
Drop-in components: `<SignIn/>`, `<SignUp/>`, `<UserButton/>`, `<UserProfile/>`, `<OrganizationSwitcher/>`, `<AuthCallback/>`.
|
|
73
|
+
|
|
74
|
+
### Express
|
|
75
|
+
|
|
76
|
+
```ts
|
|
24
77
|
import express from "express";
|
|
25
|
-
import { iqAuth } from "@iqauth/sdk/express";
|
|
78
|
+
import { iqAuth } from "@iqauth/sdk/express";
|
|
26
79
|
|
|
27
80
|
const app = express();
|
|
28
81
|
app.use(express.json());
|
|
29
|
-
const auth = iqAuth({ publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY, secretKey: process.env.IQAUTH_SECRET_KEY });
|
|
30
|
-
auth.attachHelpers(app); // mounts /api/iqauth/{callback,refresh,signout} (httpOnly cookies)
|
|
31
|
-
app.use(auth); // verifies Bearer or iqauth_at cookie; populates req.auth
|
|
32
|
-
```
|
|
33
82
|
|
|
34
|
-
|
|
83
|
+
const auth = iqAuth({
|
|
84
|
+
publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
|
|
85
|
+
secretKey: process.env.IQAUTH_SECRET_KEY!,
|
|
86
|
+
});
|
|
35
87
|
|
|
36
|
-
|
|
88
|
+
auth.attachHelpers(app); // mounts /api/iqauth/{callback,refresh,signout} (HttpOnly cookies)
|
|
89
|
+
app.use(auth); // verifies Bearer header OR iqauth_at cookie; populates req.auth
|
|
90
|
+
```
|
|
37
91
|
|
|
38
|
-
|
|
92
|
+
### Fastify
|
|
39
93
|
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
94
|
+
```ts
|
|
95
|
+
import Fastify from "fastify";
|
|
96
|
+
import { iqAuth } from "@iqauth/sdk/fastify";
|
|
97
|
+
|
|
98
|
+
const app = Fastify();
|
|
99
|
+
const auth = iqAuth({ publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!, secretKey: process.env.IQAUTH_SECRET_KEY! });
|
|
100
|
+
await app.register(auth.plugin); // mounts helpers + decorates request.auth
|
|
46
101
|
```
|
|
47
102
|
|
|
48
|
-
|
|
103
|
+
### Hono
|
|
49
104
|
|
|
50
|
-
|
|
105
|
+
```ts
|
|
106
|
+
import { Hono } from "hono";
|
|
107
|
+
import { iqAuth } from "@iqauth/sdk/hono";
|
|
51
108
|
|
|
52
|
-
|
|
109
|
+
const app = new Hono();
|
|
110
|
+
const auth = iqAuth({ publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!, secretKey: process.env.IQAUTH_SECRET_KEY! });
|
|
111
|
+
app.route("/api/iqauth", auth.helpers);
|
|
112
|
+
app.use("*", auth.middleware);
|
|
113
|
+
```
|
|
53
114
|
|
|
54
|
-
|
|
55
|
-
|--------|--------|--------|--------|
|
|
56
|
-
| First-party browser app | Backend proxy + `httpOnly` cookies | Backend only | Resource calls, server verification, shared API modules |
|
|
57
|
-
| Native mobile app | Authorization code + PKCE + secure OS storage | Mobile app secure storage | Token-owning client patterns |
|
|
58
|
-
| Server-side app | Direct token handling or request-scoped bearer verification | Server | Strong fit |
|
|
59
|
-
| Service / automation | API key or dedicated service credentials | Service | Strong fit |
|
|
115
|
+
### Next.js (App Router)
|
|
60
116
|
|
|
61
|
-
|
|
117
|
+
```ts
|
|
118
|
+
// app/api/iqauth/[...iqauth]/route.ts
|
|
119
|
+
import { iqAuth } from "@iqauth/sdk/next";
|
|
120
|
+
const auth = iqAuth({ publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!, secretKey: process.env.IQAUTH_SECRET_KEY! });
|
|
121
|
+
export const { GET, POST } = auth.handlers;
|
|
122
|
+
```
|
|
62
123
|
|
|
63
|
-
|
|
124
|
+
```ts
|
|
125
|
+
// In any Server Component / Route Handler / Server Action
|
|
126
|
+
import { getAuth } from "@iqauth/sdk/next";
|
|
127
|
+
const session = await getAuth(); // null when signed-out
|
|
128
|
+
```
|
|
64
129
|
|
|
65
|
-
|
|
130
|
+
The middleware is **cookie-aware** (Bearer header OR `iqauth_at` cookie) and the issuer is **auto-discovered from your publishable key** — no extra config.
|
|
66
131
|
|
|
67
|
-
|
|
132
|
+
---
|
|
68
133
|
|
|
69
|
-
|
|
70
|
-
- store access and refresh tokens in `httpOnly` cookies
|
|
71
|
-
- restore session with a backend endpoint such as `/auth/me`
|
|
72
|
-
- do not store refresh tokens in `localStorage`, `sessionStorage`, or browser-readable memory as your durable session model
|
|
134
|
+
## Pick your environment
|
|
73
135
|
|
|
74
|
-
The
|
|
136
|
+
The SDK is not "one auth model for every environment". The safe pattern depends on where credentials live.
|
|
75
137
|
|
|
76
|
-
|
|
138
|
+
| Environment | Recommended pattern | Refresh-token owner | Entry point |
|
|
139
|
+
|---|---|---|---|
|
|
140
|
+
| First-party browser app **with a backend** | Backend proxy + HttpOnly cookies | Backend only | `@iqauth/sdk/{express,fastify,hono,next}` + `@iqauth/sdk/react` (with `serverManagedSession`) |
|
|
141
|
+
| First-party browser app **without a backend** (SPA only) | Authorization-code + PKCE in-browser, refresh in JS-readable cookie | Browser (in cookie) | `@iqauth/sdk/browser` + `@iqauth/sdk/react` |
|
|
142
|
+
| Native mobile | Authorization-code + PKCE, secure OS storage | Mobile app secure storage | `@iqauth/sdk/mobile` |
|
|
143
|
+
| Server-side / API resource server | Bearer verification via JWKS | Server (per request) | `@iqauth/sdk/server` |
|
|
144
|
+
| Service / automation / cron | API key | Service | `@iqauth/sdk/service` |
|
|
77
145
|
|
|
78
|
-
|
|
146
|
+
> **Rule of thumb:** if your app has its own backend, use a framework adapter and turn on `serverManagedSession: true` in the React provider. Don't store refresh tokens in `localStorage`/`sessionStorage` as your durable session model.
|
|
79
147
|
|
|
80
|
-
|
|
81
|
-
- complete auth in the system browser
|
|
82
|
-
- exchange the authorization code
|
|
83
|
-
- store tokens only in Keychain / Keystore / secure OS-backed storage
|
|
148
|
+
---
|
|
84
149
|
|
|
85
|
-
|
|
150
|
+
## What's new in 2.0.3
|
|
86
151
|
|
|
87
|
-
|
|
152
|
+
### 1. `serverManagedSession: true` for `SessionManager` / `IQAuthProvider`
|
|
88
153
|
|
|
89
|
-
|
|
90
|
-
- Express resource servers
|
|
91
|
-
- admin tooling
|
|
92
|
-
- request-scoped token handling
|
|
93
|
-
- bearer-token verification through JWKS
|
|
154
|
+
For any app whose backend uses one of the framework adapters (`@iqauth/sdk/{express,fastify,hono,next}`), the backend owns the HttpOnly `iqauth_at` + `iqauth_rt` cookies and should be the **sole** authority on token rotation. Opt into that explicitly:
|
|
94
155
|
|
|
95
|
-
|
|
156
|
+
```tsx
|
|
157
|
+
<IQAuthProvider
|
|
158
|
+
publishableKey={import.meta.env.VITE_IQAUTH_PUBLISHABLE_KEY}
|
|
159
|
+
serverManagedSession
|
|
160
|
+
>
|
|
161
|
+
…
|
|
162
|
+
</IQAuthProvider>
|
|
163
|
+
```
|
|
96
164
|
|
|
97
|
-
|
|
165
|
+
What changes when this is on:
|
|
98
166
|
|
|
99
|
-
|
|
167
|
+
- `bootstrap()` learns session state with a single read-only `GET /api/v1/auth/me` (override with `userinfoPath`) instead of POSTing to `/refresh`. **No rotation, no race surface.**
|
|
168
|
+
- The proactive-refresh timer is suppressed — the server middleware refreshes on real navigation, single-flight per request.
|
|
169
|
+
- The browser `tokenStore` defaults to a no-op store, so JS never tries to read the (HttpOnly, invisible) refresh cookie.
|
|
100
170
|
|
|
101
|
-
|
|
171
|
+
This eliminates the multi-tab + React StrictMode + proactive-timer race that previously produced silent forced sign-outs.
|
|
102
172
|
|
|
103
|
-
|
|
173
|
+
### 2. `clearCookiesOnRefreshFailure: "terminal-only" | "always" | "never"`
|
|
104
174
|
|
|
105
|
-
-
|
|
106
|
-
- mobile: acceptable with PKCE + secure storage discipline
|
|
107
|
-
- browser first-party: use only behind a backend session boundary
|
|
108
|
-
- service: recommended with API keys
|
|
175
|
+
The cookie helper `handleRefresh` (used by every framework adapter) now defaults to `"terminal-only"`. Cookies are only cleared when the issuer signals the session is unrecoverable:
|
|
109
176
|
|
|
110
|
-
|
|
177
|
+
- `TOKEN_REVOKED`, `SESSION_REVOKED`, `INVALID_GRANT`
|
|
178
|
+
- `USER_DEACTIVATED`, `USER_DISABLED`, `TENANT_SUSPENDED`
|
|
179
|
+
- HTTP `410 Gone`
|
|
111
180
|
|
|
112
|
-
|
|
113
|
-
|
|
181
|
+
Transient failures — `TOKEN_INVALID` from a rotated-out token, `TOKEN_EXPIRED`, network blips, 5xx — return 401 with cookies intact, so the next legitimate request can either succeed against a still-valid access cookie or be redirected to sign-in cleanly by the middleware.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
iqAuth({
|
|
185
|
+
publishableKey, secretKey,
|
|
186
|
+
clearCookiesOnRefreshFailure: "terminal-only", // default; "always" restores pre-2.0.3 behavior
|
|
187
|
+
});
|
|
114
188
|
```
|
|
115
189
|
|
|
116
|
-
|
|
190
|
+
### 3. Next.js 15+ async cookies
|
|
117
191
|
|
|
118
|
-
|
|
192
|
+
`getAuth()` in `@iqauth/sdk/next` now `await`s `next/headers#cookies()` — fixes the runtime warning and the occasional null-session on the first request after build on Next 15 / 16.
|
|
119
193
|
|
|
120
|
-
|
|
194
|
+
---
|
|
121
195
|
|
|
122
|
-
|
|
123
|
-
import { createBrowserSessionClient } from "@iqauth/sdk/browser-session";
|
|
124
|
-
import { createServerClient, ServerIQAuthClient } from "@iqauth/sdk/server";
|
|
125
|
-
import { createMobileClient } from "@iqauth/sdk/mobile";
|
|
126
|
-
import { createServiceClient } from "@iqauth/sdk/service";
|
|
127
|
-
```
|
|
196
|
+
## Browser apps with a backend (recommended)
|
|
128
197
|
|
|
129
|
-
|
|
198
|
+
This is the integration path you want for any product with its own server. The browser never sees a refresh token; the backend owns rotation; cookies are HttpOnly.
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
// client/src/main.tsx
|
|
202
|
+
import { IQAuthProvider, SignedIn, SignedOut, RedirectToSignIn } from "@iqauth/sdk/react";
|
|
130
203
|
|
|
131
|
-
|
|
204
|
+
createRoot(document.getElementById("root")!).render(
|
|
205
|
+
<IQAuthProvider
|
|
206
|
+
publishableKey={import.meta.env.VITE_IQAUTH_PUBLISHABLE_KEY}
|
|
207
|
+
serverManagedSession
|
|
208
|
+
>
|
|
209
|
+
<SignedIn><App /></SignedIn>
|
|
210
|
+
<SignedOut><RedirectToSignIn /></SignedOut>
|
|
211
|
+
</IQAuthProvider>
|
|
212
|
+
);
|
|
213
|
+
```
|
|
132
214
|
|
|
133
|
-
```
|
|
215
|
+
```ts
|
|
216
|
+
// server/index.ts
|
|
134
217
|
import express from "express";
|
|
135
|
-
import {
|
|
218
|
+
import { iqAuth } from "@iqauth/sdk/express";
|
|
136
219
|
|
|
137
|
-
const
|
|
138
|
-
|
|
220
|
+
const app = express();
|
|
221
|
+
app.use(express.json());
|
|
222
|
+
|
|
223
|
+
const auth = iqAuth({
|
|
224
|
+
publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
|
|
225
|
+
secretKey: process.env.IQAUTH_SECRET_KEY!,
|
|
139
226
|
});
|
|
227
|
+
auth.attachHelpers(app); // mounts /api/iqauth/{callback,refresh,signout}
|
|
228
|
+
app.use(auth); // populates req.auth on every request
|
|
140
229
|
|
|
141
|
-
|
|
230
|
+
app.get("/api/me", (req, res) => res.json(req.auth));
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
That's the whole integration. The hosted sign-in page on `auth.dispositioniq.com` redirects back to `/api/iqauth/callback`, which sets HttpOnly cookies and bounces the user to your `return_to`. Subsequent API calls succeed against the access cookie; on the rare expiry the same cookie endpoint refreshes silently.
|
|
234
|
+
|
|
235
|
+
### Calling your own API from the browser
|
|
236
|
+
|
|
237
|
+
Use `auth.fetch()` from the React provider — it adds credentials and retries once on 401:
|
|
142
238
|
|
|
143
|
-
|
|
239
|
+
```tsx
|
|
240
|
+
import { useAuth } from "@iqauth/sdk/react";
|
|
144
241
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
roles: req.auth!.roles,
|
|
242
|
+
function ProductList() {
|
|
243
|
+
const { fetch } = useAuth();
|
|
244
|
+
const { data } = useQuery({
|
|
245
|
+
queryKey: ["products"],
|
|
246
|
+
queryFn: () => fetch("/api/products").then(r => r.json()),
|
|
151
247
|
});
|
|
152
|
-
|
|
248
|
+
// …
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Calling **another** IQAuth-protected API (cross-origin)
|
|
253
|
+
|
|
254
|
+
Add the calling app's origin to the target app's `app_allowed_origins` in the admin dashboard. The unified CORS allow-list (per-app origins ∪ global `cors_origins` ∪ `CORS_ORIGINS` env) refreshes within 60 seconds.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Server reference
|
|
259
|
+
|
|
260
|
+
### Middleware behavior
|
|
261
|
+
|
|
262
|
+
- Accepts `Authorization: Bearer <jwt>` **or** the `iqauth_at` HttpOnly cookie.
|
|
263
|
+
- Verifies RS256 against the issuer's JWKS (cached with stale-while-revalidate).
|
|
264
|
+
- Populates `req.auth` (Express/Fastify), `c.get("auth")` (Hono), or `getAuth()` return value (Next).
|
|
265
|
+
- The shape of `req.auth` is the verified JWT claims plus normalized `{ sub, email, tenantId, roles, permissions, scopes }`.
|
|
266
|
+
|
|
267
|
+
> The SDK uses `req.auth` (not `req.user`) so it never collides with Passport.
|
|
268
|
+
|
|
269
|
+
### Requiring roles or entitlements
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
app.use("/admin",
|
|
273
|
+
auth.require({
|
|
274
|
+
roles: ["tenant_admin", "platform_admin"],
|
|
275
|
+
entitlements: ["iqcapture"],
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
153
278
|
```
|
|
154
279
|
|
|
155
|
-
|
|
280
|
+
`auth.require()` is composable — chain it on individual routes, route groups, or globally. Failures throw `IQAuthError` with `code: "INSUFFICIENT_PERMISSIONS"` and the middleware returns 403.
|
|
156
281
|
|
|
157
|
-
|
|
282
|
+
### Helper routes mounted by `attachHelpers` / `register` / `auth.handlers`
|
|
158
283
|
|
|
159
|
-
|
|
284
|
+
| Method | Path | Purpose |
|
|
285
|
+
|---|---|---|
|
|
286
|
+
| `GET` | `/api/iqauth/callback` | Receives the OIDC redirect, exchanges the code, sets `iqauth_at` + `iqauth_rt`, redirects to `return_to` |
|
|
287
|
+
| `POST` | `/api/iqauth/refresh` | Reads `iqauth_rt`, rotates, sets new cookies. Honors `clearCookiesOnRefreshFailure` |
|
|
288
|
+
| `POST` | `/api/iqauth/signout` | Revokes the refresh token upstream and clears both cookies |
|
|
160
289
|
|
|
161
|
-
|
|
162
|
-
// frontend
|
|
163
|
-
await fetch("/auth/login", {
|
|
164
|
-
method: "POST",
|
|
165
|
-
credentials: "include",
|
|
166
|
-
headers: { "Content-Type": "application/json" },
|
|
167
|
-
body: JSON.stringify({ email, password }),
|
|
168
|
-
});
|
|
290
|
+
All three set cookies with `HttpOnly; Secure; SameSite=lax; Path=/` by default. Override per-app:
|
|
169
291
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
292
|
+
```ts
|
|
293
|
+
iqAuth({
|
|
294
|
+
publishableKey, secretKey,
|
|
295
|
+
cookieDomain: ".example.com", // for cross-subdomain SSO
|
|
296
|
+
sameSite: "none", // pair with secure: true
|
|
297
|
+
secure: true,
|
|
298
|
+
cookiePath: "/",
|
|
299
|
+
accessCookieName: "iqauth_at",
|
|
300
|
+
refreshCookieName: "iqauth_rt",
|
|
301
|
+
});
|
|
173
302
|
```
|
|
174
303
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Token verification without a framework adapter
|
|
307
|
+
|
|
308
|
+
If you're not using Express/Fastify/Hono/Next (custom Node server, AWS Lambda, Cloudflare Worker, etc.):
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { createServerClient } from "@iqauth/sdk/server";
|
|
312
|
+
|
|
313
|
+
const client = createServerClient({
|
|
314
|
+
publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!, // issuer auto-discovered
|
|
180
315
|
});
|
|
316
|
+
|
|
317
|
+
const claims = await client.tokens.verify(bearerToken);
|
|
318
|
+
// → { sub, email, tenantId, roles, permissions, scopes, iss, aud, exp, … }
|
|
181
319
|
```
|
|
182
320
|
|
|
183
|
-
|
|
321
|
+
`tokens.verify()` enforces RS256, checks `exp` / `nbf` / `iss` / `aud`, and uses the cached JWKS. Throws `IQAuthError` with codes like `TOKEN_EXPIRED`, `TOKEN_INVALID`, `TOKEN_REVOKED`.
|
|
184
322
|
|
|
185
|
-
|
|
323
|
+
---
|
|
186
324
|
|
|
187
|
-
|
|
188
|
-
import { IQAuthClient } from "@iqauth/sdk";
|
|
325
|
+
## Native mobile (PKCE)
|
|
189
326
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
327
|
+
```ts
|
|
328
|
+
import { createMobileClient } from "@iqauth/sdk/mobile";
|
|
329
|
+
|
|
330
|
+
const client = createMobileClient({
|
|
331
|
+
publishableKey: PUBLISHABLE_KEY,
|
|
332
|
+
redirectUri: "myapp://auth/callback",
|
|
333
|
+
storage: {
|
|
334
|
+
get: (k) => SecureStore.getItemAsync(k),
|
|
335
|
+
set: (k, v) => SecureStore.setItemAsync(k, v),
|
|
336
|
+
delete: (k) => SecureStore.deleteItemAsync(k),
|
|
197
337
|
},
|
|
198
338
|
});
|
|
339
|
+
|
|
340
|
+
// 1. begin
|
|
341
|
+
const { url } = await client.signIn.start();
|
|
342
|
+
await WebBrowser.openAuthSessionAsync(url, "myapp://auth/callback");
|
|
343
|
+
|
|
344
|
+
// 2. handle redirect
|
|
345
|
+
await client.signIn.complete(redirectUrl);
|
|
346
|
+
|
|
347
|
+
// 3. use the session
|
|
348
|
+
const me = await client.users.me();
|
|
199
349
|
```
|
|
200
350
|
|
|
201
|
-
|
|
351
|
+
Tokens are written to whichever `storage` adapter you provide — use Keychain on iOS, Keystore / EncryptedSharedPreferences on Android. Never persist them to AsyncStorage / `localStorage`.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Service automation / API keys
|
|
202
356
|
|
|
203
|
-
|
|
357
|
+
For cron jobs, batch scripts, server-to-server calls — anything that isn't acting on behalf of an interactive user:
|
|
204
358
|
|
|
205
|
-
```
|
|
206
|
-
import {
|
|
359
|
+
```ts
|
|
360
|
+
import { createServiceClient } from "@iqauth/sdk/service";
|
|
207
361
|
|
|
208
|
-
const client =
|
|
209
|
-
|
|
210
|
-
apiKey: process.env.IQAUTH_API_KEY
|
|
362
|
+
const client = createServiceClient({
|
|
363
|
+
publishableKey: process.env.IQAUTH_PUBLISHABLE_KEY!,
|
|
364
|
+
apiKey: process.env.IQAUTH_API_KEY!,
|
|
211
365
|
});
|
|
212
366
|
|
|
213
|
-
const
|
|
367
|
+
const users = await client.users.list({ tenantId: "…" });
|
|
368
|
+
const key = await client.apiKeys.create({ name: "nightly-sync", scopes: ["users:read"] });
|
|
214
369
|
```
|
|
215
370
|
|
|
216
|
-
|
|
371
|
+
API-key calls are scoped to the permissions granted at creation time and are subject to per-key rate limits. Rotate with `client.apiKeys.rotate({ id })`; revoke with `client.apiKeys.revoke({ id })`.
|
|
217
372
|
|
|
218
|
-
|
|
219
|
-
|--------|--------|---------|
|
|
220
|
-
| `auth` | `client.auth` | Login, MFA, tenant selection, logout, password reset, OAuth |
|
|
221
|
-
| `tokens` | `client.tokens` | JWT verification (RS256/JWKS), decode, expiry checks |
|
|
222
|
-
| `sessions` | `client.sessions` | List and revoke sessions |
|
|
223
|
-
| `users` | `client.users` | Profile and user management |
|
|
224
|
-
| `permissions` | `client.permissions` | Role and entitlement helpers |
|
|
225
|
-
| `oidc` | `client.oidc` | Discovery, JWKS, authorization helpers, code exchange |
|
|
226
|
-
| `apiKeys` | `client.apiKeys` | API key create/list/revoke/introspect |
|
|
227
|
-
| `webhooks` | `client.webhooks` | Endpoint CRUD, deliveries, test, secret rotation |
|
|
228
|
-
| `mfa` | `client.mfa` | MFA enrollment, backup codes |
|
|
229
|
-
| `branding` | `client.branding` | Branding config CRUD |
|
|
373
|
+
---
|
|
230
374
|
|
|
231
|
-
##
|
|
375
|
+
## Hosted auth pages and branding
|
|
232
376
|
|
|
233
|
-
|
|
234
|
-
import { createServerClient } from "@iqauth/sdk/server";
|
|
377
|
+
Sign-in, sign-up, forgot-password, and account pages are hosted at `auth.dispositioniq.com`. They render with your app's brand once you set CSS variables in the admin dashboard:
|
|
235
378
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
379
|
+
- `--brand-primary`
|
|
380
|
+
- `--brand-accent`
|
|
381
|
+
- `--brand-bg`
|
|
382
|
+
- `--brand-surface`
|
|
383
|
+
- `--brand-text`
|
|
239
384
|
|
|
240
|
-
app.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
);
|
|
385
|
+
Validate the redirect contract from your own page with `GET /api/public/apps/:appKey/sign-in-context` — only `return_to` origins listed in your app's allowlist will be accepted.
|
|
386
|
+
|
|
387
|
+
If you'd rather embed the same components in your own app shell, the React entry point publishes them directly:
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
import { SignIn, SignUp, UserButton, UserProfile, OrganizationSwitcher } from "@iqauth/sdk/react";
|
|
247
391
|
```
|
|
248
392
|
|
|
249
|
-
|
|
393
|
+
---
|
|
250
394
|
|
|
251
|
-
##
|
|
395
|
+
## CLI
|
|
252
396
|
|
|
253
|
-
|
|
397
|
+
```sh
|
|
398
|
+
npx iqauth init # bootstrap a new app + write IQAUTH_* keys to .env
|
|
399
|
+
npx iqauth doctor # check .env, issuer reachability, JWKS, redirect URI
|
|
400
|
+
npx iqauth keys list --app <id>
|
|
401
|
+
npx iqauth keys rotate --app <id> --key-id <id> --yes
|
|
402
|
+
npx iqauth keys revoke --app <id> --key-id <id> --yes
|
|
403
|
+
npx iqauth dev # run the bundled React example with your key
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`init` is the fastest way to provision a new app end-to-end: it creates the OIDC client, manifest, per-app origin allowlist, and a `pk_/sk_` keypair, then writes them to `.env`. Key rotation has a 24-hour grace window — old and new keys both validate during that window.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Error handling
|
|
254
411
|
|
|
255
|
-
|
|
412
|
+
Every API failure throws `IQAuthError` with a stable `code`.
|
|
413
|
+
|
|
414
|
+
```ts
|
|
256
415
|
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
257
416
|
|
|
258
417
|
try {
|
|
259
418
|
await client.auth.login(email, password);
|
|
260
419
|
} catch (err) {
|
|
261
|
-
if (err instanceof IQAuthError
|
|
262
|
-
|
|
420
|
+
if (err instanceof IQAuthError) {
|
|
421
|
+
switch (err.code) {
|
|
422
|
+
case ErrorCodes.ACCOUNT_LOCKED: return showLockoutBanner();
|
|
423
|
+
case ErrorCodes.MFA_REQUIRED: return promptMfa(err.details);
|
|
424
|
+
case ErrorCodes.TENANT_SELECTION_REQUIRED: return promptTenant(err.details.tenants);
|
|
425
|
+
case ErrorCodes.INVALID_CREDENTIALS: return showWrongPassword();
|
|
426
|
+
default: return showGenericError(err.message);
|
|
427
|
+
}
|
|
263
428
|
}
|
|
429
|
+
throw err;
|
|
264
430
|
}
|
|
265
431
|
```
|
|
266
432
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
433
|
+
Common codes you'll handle:
|
|
434
|
+
|
|
435
|
+
| Code | When |
|
|
436
|
+
|---|---|
|
|
437
|
+
| `INVALID_CREDENTIALS` | Wrong email/password |
|
|
438
|
+
| `ACCOUNT_LOCKED` | Too many failures; show cool-off |
|
|
439
|
+
| `MFA_REQUIRED` | Continue to MFA step with `details.mfaToken` |
|
|
440
|
+
| `TENANT_SELECTION_REQUIRED` | User belongs to >1 tenant; pick one |
|
|
441
|
+
| `TOKEN_EXPIRED` | Access token aged out; refresh handled by SDK helpers |
|
|
442
|
+
| `TOKEN_INVALID` | Token malformed or rotated out (transient under multi-tab) |
|
|
443
|
+
| `TOKEN_REVOKED` / `SESSION_REVOKED` | Terminal — sign the user out |
|
|
444
|
+
| `USER_DEACTIVATED` / `USER_DISABLED` / `TENANT_SUSPENDED` | Terminal — sign out |
|
|
445
|
+
| `INSUFFICIENT_PERMISSIONS` | 403 — caller lacks required role/entitlement |
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Troubleshooting
|
|
450
|
+
|
|
451
|
+
**`npx iqauth doctor` is the first stop.** It validates that the `IQAUTH_*` env vars are present and well-formed, the issuer responds, the JWKS endpoint serves keys, and your registered redirect URI matches what the SDK will send.
|
|
452
|
+
|
|
453
|
+
| Symptom | Likely cause |
|
|
454
|
+
|---|---|
|
|
455
|
+
| Silent sign-out within ~60s of login | Pre-2.0.3 cookie-clearing on transient `TOKEN_INVALID`. Upgrade to ≥ 2.0.3, set `serverManagedSession: true` on the React side. |
|
|
456
|
+
| `getAuth()` returns null after first navigation in Next 15+ | Pre-2.0.2 sync-cookies bug. Upgrade to ≥ 2.0.2. |
|
|
457
|
+
| `/api/iqauth/callback` returns 400 "redirect_uri mismatch" | Your registered redirect URI doesn't match the host the browser is on. Add it in the admin app config. |
|
|
458
|
+
| Cross-origin call to a sister IQAuth-protected app gets CORS-blocked | Add the caller's origin to the target app's `app_allowed_origins`. Allow up to 60s for the cache to refresh. |
|
|
459
|
+
| `req.auth` is undefined under Passport | Verify you're reading `req.auth`, not `req.user`. The SDK deliberately uses `req.auth` to avoid Passport collision. |
|
|
460
|
+
| Hosted sign-in page won't accept your `return_to` | The origin isn't in this app's allowlist. Add it in the admin dashboard. |
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Bundled docs
|
|
465
|
+
|
|
466
|
+
Long-form integration guides ship **inside the npm tarball** at `node_modules/@iqauth/sdk/docs/`. List them with:
|
|
467
|
+
|
|
468
|
+
```sh
|
|
469
|
+
ls node_modules/@iqauth/sdk/docs
|
|
470
|
+
ls node_modules/@iqauth/sdk/docs/guides
|
|
471
|
+
ls node_modules/@iqauth/sdk/docs/integration-prompts
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Highlights:
|
|
475
|
+
|
|
476
|
+
- `docs/APP_INTEGRATION_MATRIX.md` — which entry point + pattern for which app archetype
|
|
477
|
+
- `docs/FRESH_IMPLEMENTATION_GUIDE.md` — green-field walkthrough for a new app
|
|
478
|
+
- `docs/BROWSER_SESSION_MIGRATION.md` — moving a browser-token-owning app to cookie-managed sessions (now includes the 2.0.3 `serverManagedSession` recipe)
|
|
479
|
+
- `docs/V1_TO_V2_UPGRADE_GUIDE.md` — upgrading from `1.x`
|
|
480
|
+
- `docs/TARBALL_RELEASE_WORKFLOW.md` — internal: shipping prebuilt tarballs to consumer apps
|
|
481
|
+
- `docs/guides/auth-flows.md`, `session-management.md`, `mfa-enrollment.md`, `roles-and-permissions.md`, `scoped-authorization.md`, `entity-hierarchy.md`, `tenant-management.md`, `user-management.md`, `invitations.md`, `branding.md`, `webhooks.md`, `entitlements.md`, `api-keys.md`, `mobile-native.md`, `server-platform-integration.md`, `service-automation-integration.md`, `token-verification.md`, `middleware-reference.md`, `error-handling.md`, `gdpr-compliance.md`, `app-registration.md`
|
|
482
|
+
- `docs/integration-prompts/{first-party-browser-app,native-mobile-app,server-platform-app,service-automation-app,install-from-tarball,migrate-from-local-packages-source}.md` — drop-in prompts for AI-assisted integration
|
|
483
|
+
|
|
484
|
+
These are also kept on the IQAuth admin dashboard's documentation tab.
|
|
485
|
+
|
|
486
|
+
---
|
|
284
487
|
|
|
285
488
|
## License
|
|
286
489
|
|
|
287
|
-
|
|
490
|
+
Proprietary — DispositionIQ internal use. See the LICENSE/usage terms in the bundled docs.
|