@cleverclouds/auth-client 1.0.1 → 1.0.2
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/AGENTS.md +379 -379
- package/README.md +451 -451
- package/dist/express/index.d.ts.map +1 -1
- package/dist/express/index.js +35 -7
- package/dist/express/index.js.map +1 -1
- package/dist/nextjs/index.d.ts.map +1 -1
- package/dist/nextjs/index.js +11 -4
- package/dist/nextjs/index.js.map +1 -1
- package/package.json +88 -88
package/AGENTS.md
CHANGED
|
@@ -1,379 +1,379 @@
|
|
|
1
|
-
# @cleverclouds/auth-client — AI Agent & LLM Implementation Guide
|
|
2
|
-
|
|
3
|
-
> **Note for AI Coding Assistants (Claude, Cursor, Copilot, Antigravity, Devin, Codex):**
|
|
4
|
-
> Use this document as the authoritative specification when implementing or refactoring authentication and authorization in downstream applications using `@cleverclouds/auth-client`.
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## 1. Core Mental Model & Architectural Invariants
|
|
9
|
-
|
|
10
|
-
CleverClouds Identity is a centralized SSO and Identity Management service. Downstream applications (e.g. `projects`, `hr`, `finance`, `ops`) delegate authentication and role/permission assignment to Identity.
|
|
11
|
-
|
|
12
|
-
### Identity Boundary Rules
|
|
13
|
-
1. **Zero Direct DB Access**: Downstream applications must **never** connect directly to the Identity MongoDB cluster. All communication flows over HTTPS backchannel calls or cryptographic cookies.
|
|
14
|
-
2. **Identity vs. Domain Authorization**:
|
|
15
|
-
- Identity tells downstream apps: *Who is the user?* (`userId`, `email`, `name`, `status`), *What roles do they hold in THIS app?* (`roles: ["PROJECT_MANAGER"]`), and *What permissions do they have?* (`permissions: ["projects.*"]`).
|
|
16
|
-
- The downstream application remains **100% responsible** for resource-level authorization (e.g., whether user A owns project #42).
|
|
17
|
-
3. **Mandatory HMAC Cookie Signing**: Downstream session cookies MUST be cryptographically signed using HMAC-SHA256 via a configured `sessionSecret`. The client strictly prohibits unsigned or unverified cookie consumption.
|
|
18
|
-
4. **Single-Use Tickets**: SSO tickets (`cc_ticket_...`) are valid for **60 seconds** and are burned upon first exchange. Never reuse or cache tickets.
|
|
19
|
-
5. **Open Redirect Defense**: When redirecting users post-callback, downstream handlers MUST validate `returnTo` URLs to ensure they are relative paths (starting with `/`, not `//` or `/\`) or match a strict whitelist.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## 2. Package Entrypoints & Subpath Imports
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
// 1. Core client, types, error classes, and standalone RBAC evaluators
|
|
27
|
-
import {
|
|
28
|
-
CleverCloudsAuthClient,
|
|
29
|
-
hasPermission,
|
|
30
|
-
hasAnyPermission,
|
|
31
|
-
hasAllPermissions,
|
|
32
|
-
hasRole,
|
|
33
|
-
hasAnyRole,
|
|
34
|
-
CleverCloudsAuthError,
|
|
35
|
-
AppAccessDeniedError,
|
|
36
|
-
InvalidTicketError,
|
|
37
|
-
InvalidCredentialsError,
|
|
38
|
-
} from '@cleverclouds/auth-client';
|
|
39
|
-
|
|
40
|
-
// 2. Express.js middleware and route helpers
|
|
41
|
-
import { createAuthMiddleware } from '@cleverclouds/auth-client/express';
|
|
42
|
-
|
|
43
|
-
// 3. Next.js App Router helpers (Server Components, Route Handlers, Server Actions)
|
|
44
|
-
import { getServerSession, protectRoute } from '@cleverclouds/auth-client/nextjs';
|
|
45
|
-
|
|
46
|
-
// 4. Low-level HMAC-SHA256 cookie signer & verifier
|
|
47
|
-
import { signCookie, verifyCookie } from '@cleverclouds/auth-client/cookies';
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## 3. Required Environment Variables
|
|
53
|
-
|
|
54
|
-
Downstream applications must define these environment variables:
|
|
55
|
-
|
|
56
|
-
```env
|
|
57
|
-
# URL where CleverClouds Identity is hosted
|
|
58
|
-
CC_IDENTITY_URL=https://auth.cleverclouds.in
|
|
59
|
-
|
|
60
|
-
# Application identifier registered in CleverClouds Identity
|
|
61
|
-
CC_APP_SLUG=projects
|
|
62
|
-
|
|
63
|
-
# Application API key generated in CleverClouds Identity Admin Console
|
|
64
|
-
CC_APP_KEY=cc_app_projects_xxxxxxxxxxxxxxxx
|
|
65
|
-
|
|
66
|
-
# Callback URL registered in application's redirectUrls
|
|
67
|
-
CC_REDIRECT_URI=https://projects.cleverclouds.in/api/auth/callback
|
|
68
|
-
|
|
69
|
-
# Strong secret for signing downstream session cookies (min 32 characters)
|
|
70
|
-
SESSION_SECRET=super_strong_random_secret_at_least_32_chars_long
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## 4. Initializing the Client Singleton
|
|
76
|
-
|
|
77
|
-
Create a shared singleton instance (e.g. `src/lib/auth.ts` or `src/auth.ts`):
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
import { CleverCloudsAuthClient } from '@cleverclouds/auth-client';
|
|
81
|
-
|
|
82
|
-
export const auth = new CleverCloudsAuthClient({
|
|
83
|
-
identityBaseUrl: process.env.CC_IDENTITY_URL || 'http://localhost:3000',
|
|
84
|
-
applicationSlug: process.env.CC_APP_SLUG || 'projects',
|
|
85
|
-
applicationApiKey: process.env.CC_APP_KEY!,
|
|
86
|
-
redirectUri: process.env.CC_REDIRECT_URI || 'http://localhost:3001/api/auth/callback',
|
|
87
|
-
sessionSecret: process.env.SESSION_SECRET!, // Mandatory for session cookie signing
|
|
88
|
-
});
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
## 5. Implementation Recipes
|
|
94
|
-
|
|
95
|
-
### Pattern A: Express.js (API & SSR)
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
import express from 'express';
|
|
99
|
-
import cookieParser from 'cookie-parser';
|
|
100
|
-
import { createAuthMiddleware } from '@cleverclouds/auth-client/express';
|
|
101
|
-
import { auth } from './lib/auth.js';
|
|
102
|
-
|
|
103
|
-
const app = express();
|
|
104
|
-
app.use(express.json());
|
|
105
|
-
app.use(cookieParser()); // REQUIRED: cookie-parser middleware must precede auth middleware
|
|
106
|
-
|
|
107
|
-
// Initialize middleware adapter
|
|
108
|
-
const {
|
|
109
|
-
requireAuth,
|
|
110
|
-
requirePermission,
|
|
111
|
-
requireRole,
|
|
112
|
-
handleCallback,
|
|
113
|
-
handleLogout,
|
|
114
|
-
} = createAuthMiddleware(auth, {
|
|
115
|
-
cookieName: 'projects_session',
|
|
116
|
-
cookieOptions: {
|
|
117
|
-
httpOnly: true,
|
|
118
|
-
secure: process.env.NODE_ENV === 'production',
|
|
119
|
-
sameSite: 'lax',
|
|
120
|
-
path: '/',
|
|
121
|
-
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// 1. SSO Callback Route — Exchanges ticket and sets signed session cookie
|
|
126
|
-
app.get('/api/auth/callback', handleCallback({
|
|
127
|
-
defaultReturnTo: '/dashboard',
|
|
128
|
-
allowedRedirectHosts: ['projects.cleverclouds.in', 'localhost:3001'],
|
|
129
|
-
}));
|
|
130
|
-
|
|
131
|
-
// 2. Logout Route — Clears session cookie and redirects
|
|
132
|
-
app.get('/logout', handleLogout({
|
|
133
|
-
redirectTo: '/login',
|
|
134
|
-
}));
|
|
135
|
-
|
|
136
|
-
// 3. Authenticated Route — Attaches req.auth: VerificationResult
|
|
137
|
-
app.get('/api/me', requireAuth(), (req, res) => {
|
|
138
|
-
res.json({
|
|
139
|
-
user: req.auth.user,
|
|
140
|
-
roles: req.auth.roles,
|
|
141
|
-
permissions: req.auth.permissions,
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// 4. Permission-Gated Route (supports wildcards e.g. 'projects.*')
|
|
146
|
-
app.post('/api/projects', requirePermission('projects.create'), async (req, res) => {
|
|
147
|
-
// Safe: user is guaranteed to have 'projects.create' or 'projects.*' or '*.*'
|
|
148
|
-
res.json({ message: 'Project created' });
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// 5. Role-Gated Route (case-insensitive role check)
|
|
152
|
-
app.delete('/api/projects/:id', requireRole('project_admin'), async (req, res) => {
|
|
153
|
-
res.json({ message: 'Project deleted' });
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Optional: Custom Login Trigger (redirects browser to Identity)
|
|
157
|
-
app.get('/login', (req, res) => {
|
|
158
|
-
const returnTo = typeof req.query.returnTo === 'string' ? req.query.returnTo : '/dashboard';
|
|
159
|
-
const loginUrl = auth.getAuthorizationUrl({ returnTo });
|
|
160
|
-
res.redirect(loginUrl);
|
|
161
|
-
});
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
### Pattern B: Next.js App Router (14/15)
|
|
167
|
-
|
|
168
|
-
#### 1. Server Components & Route Handlers (`getServerSession`)
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
// app/dashboard/page.tsx (Server Component)
|
|
172
|
-
import { redirect } from 'next/navigation';
|
|
173
|
-
import { getServerSession } from '@cleverclouds/auth-client/nextjs';
|
|
174
|
-
import { auth } from '@/lib/auth';
|
|
175
|
-
|
|
176
|
-
export default async function DashboardPage() {
|
|
177
|
-
const session = await getServerSession(auth, { cookieName: 'projects_session' });
|
|
178
|
-
|
|
179
|
-
if (!session) {
|
|
180
|
-
redirect(auth.getAuthorizationUrl({ returnTo: '/dashboard' }));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return (
|
|
184
|
-
<main>
|
|
185
|
-
<h1>Welcome, {session.user.name}</h1>
|
|
186
|
-
<p>Email: {session.user.email}</p>
|
|
187
|
-
<p>Roles: {session.roles.join(', ')}</p>
|
|
188
|
-
</main>
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
#### 2. Route Guard Helper (`protectRoute`)
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
// app/api/projects/route.ts (Route Handler)
|
|
197
|
-
import { NextResponse } from 'next/server';
|
|
198
|
-
import { protectRoute } from '@cleverclouds/auth-client/nextjs';
|
|
199
|
-
import { auth } from '@/lib/auth';
|
|
200
|
-
|
|
201
|
-
export async function POST(req: Request) {
|
|
202
|
-
const { authorized, session, response } = await protectRoute(auth, {
|
|
203
|
-
request: req,
|
|
204
|
-
permission: 'projects.create',
|
|
205
|
-
cookieName: 'projects_session',
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
if (!authorized) {
|
|
209
|
-
return response!; // Returns 401 Unauthorized or 403 Forbidden with JSON error
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const body = await req.json();
|
|
213
|
-
return NextResponse.json({
|
|
214
|
-
success: true,
|
|
215
|
-
createdBy: session!.user.email,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
#### 3. SSO Callback Route Handler
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
// app/api/auth/callback/route.ts
|
|
224
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
225
|
-
import { auth } from '@/lib/auth';
|
|
226
|
-
|
|
227
|
-
export async function GET(req: NextRequest) {
|
|
228
|
-
const ticket = req.nextUrl.searchParams.get('ticket');
|
|
229
|
-
const returnTo = req.nextUrl.searchParams.get('returnTo') || '/dashboard';
|
|
230
|
-
|
|
231
|
-
if (!ticket) {
|
|
232
|
-
return NextResponse.json({ error: 'Missing ticket' }, { status: 400 });
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const exchange = await auth.exchangeTicket(ticket);
|
|
236
|
-
if (!exchange.success) {
|
|
237
|
-
return NextResponse.json({ error: exchange.error.message }, { status: 401 });
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Cryptographically sign session payload
|
|
241
|
-
const signedCookie = auth.signSessionCookie(exchange.data, {
|
|
242
|
-
expiresInSeconds: 7 * 24 * 60 * 60,
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// Enforce relative redirect (anti-open-redirect)
|
|
246
|
-
const safePath = returnTo.startsWith('/') && !returnTo.startsWith('//') ? returnTo : '/dashboard';
|
|
247
|
-
const response = NextResponse.redirect(new URL(safePath, req.url));
|
|
248
|
-
|
|
249
|
-
response.cookies.set('projects_session', signedCookie, {
|
|
250
|
-
httpOnly: true,
|
|
251
|
-
secure: process.env.NODE_ENV === 'production',
|
|
252
|
-
sameSite: 'lax',
|
|
253
|
-
path: '/',
|
|
254
|
-
maxAge: 7 * 24 * 60 * 60,
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
return response;
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
---
|
|
262
|
-
|
|
263
|
-
### Pattern C: Fastify / Raw Node HTTP
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
import Fastify from 'fastify';
|
|
267
|
-
import fastifyCookie from '@fastify/cookie';
|
|
268
|
-
import { auth } from './lib/auth.js';
|
|
269
|
-
|
|
270
|
-
const fastify = Fastify();
|
|
271
|
-
fastify.register(fastifyCookie);
|
|
272
|
-
|
|
273
|
-
// Authentication Hook
|
|
274
|
-
fastify.addHook('preHandler', async (req, reply) => {
|
|
275
|
-
const cookieValue = req.cookies.projects_session;
|
|
276
|
-
if (!cookieValue) return;
|
|
277
|
-
|
|
278
|
-
const session = auth.verifySessionCookie(cookieValue);
|
|
279
|
-
if (session) {
|
|
280
|
-
(req as any).auth = session;
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Guard helper
|
|
285
|
-
function requirePermission(permission: string) {
|
|
286
|
-
return async (req: any, reply: any) => {
|
|
287
|
-
if (!req.auth) {
|
|
288
|
-
return reply.status(401).send({ error: 'Unauthorized' });
|
|
289
|
-
}
|
|
290
|
-
if (!auth.hasPermission(req.auth.permissions, permission)) {
|
|
291
|
-
return reply.status(403).send({ error: 'Forbidden', required: permission });
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
---
|
|
298
|
-
|
|
299
|
-
## 6. Role & Permission Evaluator Rules
|
|
300
|
-
|
|
301
|
-
The library provides zero-dependency pure evaluation functions:
|
|
302
|
-
|
|
303
|
-
```typescript
|
|
304
|
-
import {
|
|
305
|
-
hasPermission,
|
|
306
|
-
hasAnyPermission,
|
|
307
|
-
hasAllPermissions,
|
|
308
|
-
hasRole,
|
|
309
|
-
hasAnyRole,
|
|
310
|
-
} from '@cleverclouds/auth-client';
|
|
311
|
-
|
|
312
|
-
// 1. Exact match
|
|
313
|
-
hasPermission(['projects.read', 'projects.create'], 'projects.create'); // true
|
|
314
|
-
|
|
315
|
-
// 2. Resource Wildcard matching (matches any action under the resource namespace)
|
|
316
|
-
hasPermission(['projects.*'], 'projects.delete'); // true
|
|
317
|
-
hasPermission(['projects.*'], 'finance.read'); // false
|
|
318
|
-
|
|
319
|
-
// 3. Platform Wildcard matching
|
|
320
|
-
hasPermission(['*.*'], 'anything.action'); // true
|
|
321
|
-
hasPermission(['*'], 'anything.action'); // true
|
|
322
|
-
|
|
323
|
-
// 4. Multi-permission helpers
|
|
324
|
-
hasAnyPermission(['projects.read'], ['projects.write', 'projects.read']); // true
|
|
325
|
-
hasAllPermissions(['projects.read'], ['projects.write', 'projects.read']); // false
|
|
326
|
-
|
|
327
|
-
// 5. Role matching (case-insensitive)
|
|
328
|
-
hasRole(['PROJECT_MANAGER'], 'project_manager'); // true
|
|
329
|
-
hasAnyRole(['DEVELOPER'], ['ADMIN', 'DEVELOPER']); // true
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
---
|
|
333
|
-
|
|
334
|
-
## 7. TypeScript Type Definitions
|
|
335
|
-
|
|
336
|
-
```typescript
|
|
337
|
-
export interface AuthUser {
|
|
338
|
-
id: string;
|
|
339
|
-
email: string;
|
|
340
|
-
name: string;
|
|
341
|
-
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
|
342
|
-
platformRole?: string;
|
|
343
|
-
avatarUrl?: string;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
export interface ApplicationMetadata {
|
|
347
|
-
id: string;
|
|
348
|
-
slug: string;
|
|
349
|
-
name: string;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export interface VerificationResult {
|
|
353
|
-
valid: boolean;
|
|
354
|
-
user: AuthUser;
|
|
355
|
-
application: ApplicationMetadata;
|
|
356
|
-
roles: string[];
|
|
357
|
-
permissions: string[];
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export interface ClientConfig {
|
|
361
|
-
identityBaseUrl?: string; // Default: 'http://localhost:3000'
|
|
362
|
-
applicationSlug: string; // e.g. 'projects'
|
|
363
|
-
applicationApiKey: string; // 'cc_app_...'
|
|
364
|
-
redirectUri: string; // e.g. 'http://localhost:3001/api/auth/callback'
|
|
365
|
-
sessionSecret?: string; // Mandatory for cookie signing/verifying
|
|
366
|
-
}
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
---
|
|
370
|
-
|
|
371
|
-
## 8. Critical Checklist for AI Agents
|
|
372
|
-
|
|
373
|
-
When writing or refactoring code that uses `@cleverclouds/auth-client`, verify that:
|
|
374
|
-
- [ ] `cookie-parser` is registered before `createAuthMiddleware` in Express apps.
|
|
375
|
-
- [ ] `SESSION_SECRET` is passed in `ClientConfig` whenever cookies are used.
|
|
376
|
-
- [ ] All redirect destinations in callbacks are checked for open redirects (`safePath.startsWith('/') && !safePath.startsWith('//')`).
|
|
377
|
-
- [ ] `ticket` is validated and rejected with HTTP 400 if missing from callback queries.
|
|
378
|
-
- [ ] Cookies are configured with `httpOnly: true`, `secure: true` (in production), and `sameSite: 'lax'`.
|
|
379
|
-
- [ ] Domain authorization checks (e.g. database entity ownership) remain enforced downstream; do not assume identity permissions imply row-level database ownership.
|
|
1
|
+
# @cleverclouds/auth-client — AI Agent & LLM Implementation Guide
|
|
2
|
+
|
|
3
|
+
> **Note for AI Coding Assistants (Claude, Cursor, Copilot, Antigravity, Devin, Codex):**
|
|
4
|
+
> Use this document as the authoritative specification when implementing or refactoring authentication and authorization in downstream applications using `@cleverclouds/auth-client`.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Core Mental Model & Architectural Invariants
|
|
9
|
+
|
|
10
|
+
CleverClouds Identity is a centralized SSO and Identity Management service. Downstream applications (e.g. `projects`, `hr`, `finance`, `ops`) delegate authentication and role/permission assignment to Identity.
|
|
11
|
+
|
|
12
|
+
### Identity Boundary Rules
|
|
13
|
+
1. **Zero Direct DB Access**: Downstream applications must **never** connect directly to the Identity MongoDB cluster. All communication flows over HTTPS backchannel calls or cryptographic cookies.
|
|
14
|
+
2. **Identity vs. Domain Authorization**:
|
|
15
|
+
- Identity tells downstream apps: *Who is the user?* (`userId`, `email`, `name`, `status`), *What roles do they hold in THIS app?* (`roles: ["PROJECT_MANAGER"]`), and *What permissions do they have?* (`permissions: ["projects.*"]`).
|
|
16
|
+
- The downstream application remains **100% responsible** for resource-level authorization (e.g., whether user A owns project #42).
|
|
17
|
+
3. **Mandatory HMAC Cookie Signing**: Downstream session cookies MUST be cryptographically signed using HMAC-SHA256 via a configured `sessionSecret`. The client strictly prohibits unsigned or unverified cookie consumption.
|
|
18
|
+
4. **Single-Use Tickets**: SSO tickets (`cc_ticket_...`) are valid for **60 seconds** and are burned upon first exchange. Never reuse or cache tickets.
|
|
19
|
+
5. **Open Redirect Defense**: When redirecting users post-callback, downstream handlers MUST validate `returnTo` URLs to ensure they are relative paths (starting with `/`, not `//` or `/\`) or match a strict whitelist.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. Package Entrypoints & Subpath Imports
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// 1. Core client, types, error classes, and standalone RBAC evaluators
|
|
27
|
+
import {
|
|
28
|
+
CleverCloudsAuthClient,
|
|
29
|
+
hasPermission,
|
|
30
|
+
hasAnyPermission,
|
|
31
|
+
hasAllPermissions,
|
|
32
|
+
hasRole,
|
|
33
|
+
hasAnyRole,
|
|
34
|
+
CleverCloudsAuthError,
|
|
35
|
+
AppAccessDeniedError,
|
|
36
|
+
InvalidTicketError,
|
|
37
|
+
InvalidCredentialsError,
|
|
38
|
+
} from '@cleverclouds/auth-client';
|
|
39
|
+
|
|
40
|
+
// 2. Express.js middleware and route helpers
|
|
41
|
+
import { createAuthMiddleware } from '@cleverclouds/auth-client/express';
|
|
42
|
+
|
|
43
|
+
// 3. Next.js App Router helpers (Server Components, Route Handlers, Server Actions)
|
|
44
|
+
import { getServerSession, protectRoute } from '@cleverclouds/auth-client/nextjs';
|
|
45
|
+
|
|
46
|
+
// 4. Low-level HMAC-SHA256 cookie signer & verifier
|
|
47
|
+
import { signCookie, verifyCookie } from '@cleverclouds/auth-client/cookies';
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 3. Required Environment Variables
|
|
53
|
+
|
|
54
|
+
Downstream applications must define these environment variables:
|
|
55
|
+
|
|
56
|
+
```env
|
|
57
|
+
# URL where CleverClouds Identity is hosted
|
|
58
|
+
CC_IDENTITY_URL=https://auth.cleverclouds.in
|
|
59
|
+
|
|
60
|
+
# Application identifier registered in CleverClouds Identity
|
|
61
|
+
CC_APP_SLUG=projects
|
|
62
|
+
|
|
63
|
+
# Application API key generated in CleverClouds Identity Admin Console
|
|
64
|
+
CC_APP_KEY=cc_app_projects_xxxxxxxxxxxxxxxx
|
|
65
|
+
|
|
66
|
+
# Callback URL registered in application's redirectUrls
|
|
67
|
+
CC_REDIRECT_URI=https://projects.cleverclouds.in/api/auth/callback
|
|
68
|
+
|
|
69
|
+
# Strong secret for signing downstream session cookies (min 32 characters)
|
|
70
|
+
SESSION_SECRET=super_strong_random_secret_at_least_32_chars_long
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 4. Initializing the Client Singleton
|
|
76
|
+
|
|
77
|
+
Create a shared singleton instance (e.g. `src/lib/auth.ts` or `src/auth.ts`):
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { CleverCloudsAuthClient } from '@cleverclouds/auth-client';
|
|
81
|
+
|
|
82
|
+
export const auth = new CleverCloudsAuthClient({
|
|
83
|
+
identityBaseUrl: process.env.CC_IDENTITY_URL || 'http://localhost:3000',
|
|
84
|
+
applicationSlug: process.env.CC_APP_SLUG || 'projects',
|
|
85
|
+
applicationApiKey: process.env.CC_APP_KEY!,
|
|
86
|
+
redirectUri: process.env.CC_REDIRECT_URI || 'http://localhost:3001/api/auth/callback',
|
|
87
|
+
sessionSecret: process.env.SESSION_SECRET!, // Mandatory for session cookie signing
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 5. Implementation Recipes
|
|
94
|
+
|
|
95
|
+
### Pattern A: Express.js (API & SSR)
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import express from 'express';
|
|
99
|
+
import cookieParser from 'cookie-parser';
|
|
100
|
+
import { createAuthMiddleware } from '@cleverclouds/auth-client/express';
|
|
101
|
+
import { auth } from './lib/auth.js';
|
|
102
|
+
|
|
103
|
+
const app = express();
|
|
104
|
+
app.use(express.json());
|
|
105
|
+
app.use(cookieParser()); // REQUIRED: cookie-parser middleware must precede auth middleware
|
|
106
|
+
|
|
107
|
+
// Initialize middleware adapter
|
|
108
|
+
const {
|
|
109
|
+
requireAuth,
|
|
110
|
+
requirePermission,
|
|
111
|
+
requireRole,
|
|
112
|
+
handleCallback,
|
|
113
|
+
handleLogout,
|
|
114
|
+
} = createAuthMiddleware(auth, {
|
|
115
|
+
cookieName: 'projects_session',
|
|
116
|
+
cookieOptions: {
|
|
117
|
+
httpOnly: true,
|
|
118
|
+
secure: process.env.NODE_ENV === 'production',
|
|
119
|
+
sameSite: 'lax',
|
|
120
|
+
path: '/',
|
|
121
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 1. SSO Callback Route — Exchanges ticket and sets signed session cookie
|
|
126
|
+
app.get('/api/auth/callback', handleCallback({
|
|
127
|
+
defaultReturnTo: '/dashboard',
|
|
128
|
+
allowedRedirectHosts: ['projects.cleverclouds.in', 'localhost:3001'],
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
// 2. Logout Route — Clears session cookie and redirects
|
|
132
|
+
app.get('/logout', handleLogout({
|
|
133
|
+
redirectTo: '/login',
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
// 3. Authenticated Route — Attaches req.auth: VerificationResult
|
|
137
|
+
app.get('/api/me', requireAuth(), (req, res) => {
|
|
138
|
+
res.json({
|
|
139
|
+
user: req.auth.user,
|
|
140
|
+
roles: req.auth.roles,
|
|
141
|
+
permissions: req.auth.permissions,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// 4. Permission-Gated Route (supports wildcards e.g. 'projects.*')
|
|
146
|
+
app.post('/api/projects', requirePermission('projects.create'), async (req, res) => {
|
|
147
|
+
// Safe: user is guaranteed to have 'projects.create' or 'projects.*' or '*.*'
|
|
148
|
+
res.json({ message: 'Project created' });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// 5. Role-Gated Route (case-insensitive role check)
|
|
152
|
+
app.delete('/api/projects/:id', requireRole('project_admin'), async (req, res) => {
|
|
153
|
+
res.json({ message: 'Project deleted' });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Optional: Custom Login Trigger (redirects browser to Identity)
|
|
157
|
+
app.get('/login', (req, res) => {
|
|
158
|
+
const returnTo = typeof req.query.returnTo === 'string' ? req.query.returnTo : '/dashboard';
|
|
159
|
+
const loginUrl = auth.getAuthorizationUrl({ returnTo });
|
|
160
|
+
res.redirect(loginUrl);
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### Pattern B: Next.js App Router (14/15)
|
|
167
|
+
|
|
168
|
+
#### 1. Server Components & Route Handlers (`getServerSession`)
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// app/dashboard/page.tsx (Server Component)
|
|
172
|
+
import { redirect } from 'next/navigation';
|
|
173
|
+
import { getServerSession } from '@cleverclouds/auth-client/nextjs';
|
|
174
|
+
import { auth } from '@/lib/auth';
|
|
175
|
+
|
|
176
|
+
export default async function DashboardPage() {
|
|
177
|
+
const session = await getServerSession(auth, { cookieName: 'projects_session' });
|
|
178
|
+
|
|
179
|
+
if (!session) {
|
|
180
|
+
redirect(auth.getAuthorizationUrl({ returnTo: '/dashboard' }));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<main>
|
|
185
|
+
<h1>Welcome, {session.user.name}</h1>
|
|
186
|
+
<p>Email: {session.user.email}</p>
|
|
187
|
+
<p>Roles: {session.roles.join(', ')}</p>
|
|
188
|
+
</main>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### 2. Route Guard Helper (`protectRoute`)
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// app/api/projects/route.ts (Route Handler)
|
|
197
|
+
import { NextResponse } from 'next/server';
|
|
198
|
+
import { protectRoute } from '@cleverclouds/auth-client/nextjs';
|
|
199
|
+
import { auth } from '@/lib/auth';
|
|
200
|
+
|
|
201
|
+
export async function POST(req: Request) {
|
|
202
|
+
const { authorized, session, response } = await protectRoute(auth, {
|
|
203
|
+
request: req,
|
|
204
|
+
permission: 'projects.create',
|
|
205
|
+
cookieName: 'projects_session',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!authorized) {
|
|
209
|
+
return response!; // Returns 401 Unauthorized or 403 Forbidden with JSON error
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const body = await req.json();
|
|
213
|
+
return NextResponse.json({
|
|
214
|
+
success: true,
|
|
215
|
+
createdBy: session!.user.email,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### 3. SSO Callback Route Handler
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// app/api/auth/callback/route.ts
|
|
224
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
225
|
+
import { auth } from '@/lib/auth';
|
|
226
|
+
|
|
227
|
+
export async function GET(req: NextRequest) {
|
|
228
|
+
const ticket = req.nextUrl.searchParams.get('ticket');
|
|
229
|
+
const returnTo = req.nextUrl.searchParams.get('returnTo') || '/dashboard';
|
|
230
|
+
|
|
231
|
+
if (!ticket) {
|
|
232
|
+
return NextResponse.json({ error: 'Missing ticket' }, { status: 400 });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const exchange = await auth.exchangeTicket(ticket);
|
|
236
|
+
if (!exchange.success) {
|
|
237
|
+
return NextResponse.json({ error: exchange.error.message }, { status: 401 });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Cryptographically sign session payload
|
|
241
|
+
const signedCookie = auth.signSessionCookie(exchange.data, {
|
|
242
|
+
expiresInSeconds: 7 * 24 * 60 * 60,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Enforce relative redirect (anti-open-redirect)
|
|
246
|
+
const safePath = returnTo.startsWith('/') && !returnTo.startsWith('//') ? returnTo : '/dashboard';
|
|
247
|
+
const response = NextResponse.redirect(new URL(safePath, req.url));
|
|
248
|
+
|
|
249
|
+
response.cookies.set('projects_session', signedCookie, {
|
|
250
|
+
httpOnly: true,
|
|
251
|
+
secure: process.env.NODE_ENV === 'production',
|
|
252
|
+
sameSite: 'lax',
|
|
253
|
+
path: '/',
|
|
254
|
+
maxAge: 7 * 24 * 60 * 60,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return response;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### Pattern C: Fastify / Raw Node HTTP
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import Fastify from 'fastify';
|
|
267
|
+
import fastifyCookie from '@fastify/cookie';
|
|
268
|
+
import { auth } from './lib/auth.js';
|
|
269
|
+
|
|
270
|
+
const fastify = Fastify();
|
|
271
|
+
fastify.register(fastifyCookie);
|
|
272
|
+
|
|
273
|
+
// Authentication Hook
|
|
274
|
+
fastify.addHook('preHandler', async (req, reply) => {
|
|
275
|
+
const cookieValue = req.cookies.projects_session;
|
|
276
|
+
if (!cookieValue) return;
|
|
277
|
+
|
|
278
|
+
const session = auth.verifySessionCookie(cookieValue);
|
|
279
|
+
if (session) {
|
|
280
|
+
(req as any).auth = session;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Guard helper
|
|
285
|
+
function requirePermission(permission: string) {
|
|
286
|
+
return async (req: any, reply: any) => {
|
|
287
|
+
if (!req.auth) {
|
|
288
|
+
return reply.status(401).send({ error: 'Unauthorized' });
|
|
289
|
+
}
|
|
290
|
+
if (!auth.hasPermission(req.auth.permissions, permission)) {
|
|
291
|
+
return reply.status(403).send({ error: 'Forbidden', required: permission });
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 6. Role & Permission Evaluator Rules
|
|
300
|
+
|
|
301
|
+
The library provides zero-dependency pure evaluation functions:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import {
|
|
305
|
+
hasPermission,
|
|
306
|
+
hasAnyPermission,
|
|
307
|
+
hasAllPermissions,
|
|
308
|
+
hasRole,
|
|
309
|
+
hasAnyRole,
|
|
310
|
+
} from '@cleverclouds/auth-client';
|
|
311
|
+
|
|
312
|
+
// 1. Exact match
|
|
313
|
+
hasPermission(['projects.read', 'projects.create'], 'projects.create'); // true
|
|
314
|
+
|
|
315
|
+
// 2. Resource Wildcard matching (matches any action under the resource namespace)
|
|
316
|
+
hasPermission(['projects.*'], 'projects.delete'); // true
|
|
317
|
+
hasPermission(['projects.*'], 'finance.read'); // false
|
|
318
|
+
|
|
319
|
+
// 3. Platform Wildcard matching
|
|
320
|
+
hasPermission(['*.*'], 'anything.action'); // true
|
|
321
|
+
hasPermission(['*'], 'anything.action'); // true
|
|
322
|
+
|
|
323
|
+
// 4. Multi-permission helpers
|
|
324
|
+
hasAnyPermission(['projects.read'], ['projects.write', 'projects.read']); // true
|
|
325
|
+
hasAllPermissions(['projects.read'], ['projects.write', 'projects.read']); // false
|
|
326
|
+
|
|
327
|
+
// 5. Role matching (case-insensitive)
|
|
328
|
+
hasRole(['PROJECT_MANAGER'], 'project_manager'); // true
|
|
329
|
+
hasAnyRole(['DEVELOPER'], ['ADMIN', 'DEVELOPER']); // true
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 7. TypeScript Type Definitions
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
export interface AuthUser {
|
|
338
|
+
id: string;
|
|
339
|
+
email: string;
|
|
340
|
+
name: string;
|
|
341
|
+
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
|
342
|
+
platformRole?: string;
|
|
343
|
+
avatarUrl?: string;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface ApplicationMetadata {
|
|
347
|
+
id: string;
|
|
348
|
+
slug: string;
|
|
349
|
+
name: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface VerificationResult {
|
|
353
|
+
valid: boolean;
|
|
354
|
+
user: AuthUser;
|
|
355
|
+
application: ApplicationMetadata;
|
|
356
|
+
roles: string[];
|
|
357
|
+
permissions: string[];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface ClientConfig {
|
|
361
|
+
identityBaseUrl?: string; // Default: 'http://localhost:3000'
|
|
362
|
+
applicationSlug: string; // e.g. 'projects'
|
|
363
|
+
applicationApiKey: string; // 'cc_app_...'
|
|
364
|
+
redirectUri: string; // e.g. 'http://localhost:3001/api/auth/callback'
|
|
365
|
+
sessionSecret?: string; // Mandatory for cookie signing/verifying
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 8. Critical Checklist for AI Agents
|
|
372
|
+
|
|
373
|
+
When writing or refactoring code that uses `@cleverclouds/auth-client`, verify that:
|
|
374
|
+
- [ ] `cookie-parser` is registered before `createAuthMiddleware` in Express apps.
|
|
375
|
+
- [ ] `SESSION_SECRET` is passed in `ClientConfig` whenever cookies are used.
|
|
376
|
+
- [ ] All redirect destinations in callbacks are checked for open redirects (`safePath.startsWith('/') && !safePath.startsWith('//')`).
|
|
377
|
+
- [ ] `ticket` is validated and rejected with HTTP 400 if missing from callback queries.
|
|
378
|
+
- [ ] Cookies are configured with `httpOnly: true`, `secure: true` (in production), and `sameSite: 'lax'`.
|
|
379
|
+
- [ ] Domain authorization checks (e.g. database entity ownership) remain enforced downstream; do not assume identity permissions imply row-level database ownership.
|