@bluealba/platform-cli 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/dist/index.js +277 -9
- package/docs/404.mdx +5 -0
- package/docs/architecture/api-explorer.mdx +478 -0
- package/docs/architecture/architecture-diagrams.mdx +12 -0
- package/docs/architecture/authentication-system.mdx +903 -0
- package/docs/architecture/authorization-system.mdx +886 -0
- package/docs/architecture/bootstrap.mdx +1442 -0
- package/docs/architecture/gateway-architecture.mdx +845 -0
- package/docs/architecture/multi-tenancy.mdx +1150 -0
- package/docs/architecture/overview.mdx +776 -0
- package/docs/architecture/scheduler.mdx +818 -0
- package/docs/architecture/shell.mdx +885 -0
- package/docs/architecture/ui-extension-points.mdx +781 -0
- package/docs/architecture/user-states.mdx +794 -0
- package/docs/development/overview.mdx +21 -0
- package/docs/development/workflow.mdx +914 -0
- package/docs/getting-started/core-concepts.mdx +892 -0
- package/docs/getting-started/installation.mdx +780 -0
- package/docs/getting-started/overview.mdx +83 -0
- package/docs/getting-started/quick-start.mdx +940 -0
- package/docs/guides/adding-documentation-sites.mdx +1367 -0
- package/docs/guides/creating-services.mdx +1736 -0
- package/docs/guides/creating-ui-modules.mdx +1860 -0
- package/docs/guides/identity-providers.mdx +1007 -0
- package/docs/guides/mermaid-diagrams.mdx +212 -0
- package/docs/guides/using-feature-flags.mdx +1059 -0
- package/docs/guides/working-with-rooms.mdx +566 -0
- package/docs/index.mdx +57 -0
- package/docs/platform-cli/commands.mdx +604 -0
- package/docs/platform-cli/overview.mdx +195 -0
- package/package.json +5 -2
- package/skills/ba-platform/platform-cli.skill.md +26 -0
- package/skills/ba-platform/platform.skill.md +35 -0
- package/templates/application-monorepo-template/gitignore +95 -0
- package/templates/bootstrap-service-template/gitignore +57 -0
- package/templates/bootstrap-service-template/src/main.ts +6 -16
- package/templates/customization-ui-module-template/gitignore +73 -0
- package/templates/nestjs-service-module-template/gitignore +56 -0
- package/templates/platform-init-template/{{platformName}}-core/gitignore +97 -0
- package/templates/react-ui-module-template/Dockerfile +1 -1
- package/templates/react-ui-module-template/caddy/Caddyfile +1 -1
- package/templates/react-ui-module-template/gitignore +72 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Authentication System
|
|
3
|
+
description: Deep dive into the Blue Alba Platform authentication architecture - JWT, multi-IDP, OAuth 2.0, API keys, service auth, and impersonation
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import { Card, CardGrid, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
7
|
+
|
|
8
|
+
The Blue Alba Platform implements a comprehensive authentication system that supports multiple identity providers, authentication methods, and advanced features like user impersonation.
|
|
9
|
+
|
|
10
|
+
## Authentication Architecture Overview
|
|
11
|
+
|
|
12
|
+
The platform supports multiple authentication mechanisms:
|
|
13
|
+
|
|
14
|
+
<CardGrid stagger>
|
|
15
|
+
<Card title="JWT-Based Sessions" icon="approve-check">
|
|
16
|
+
Primary authentication using JWT tokens stored in HTTP-only cookies for browser-based access
|
|
17
|
+
</Card>
|
|
18
|
+
|
|
19
|
+
<Card title="Multi-IDP Support" icon="star">
|
|
20
|
+
Integration with multiple identity providers: Okta, EntraId, OneLogin, Github, AWS Cognito
|
|
21
|
+
</Card>
|
|
22
|
+
|
|
23
|
+
<Card title="API Key Authentication" icon="seti:lock">
|
|
24
|
+
Long-lived API keys for programmatic access and integrations
|
|
25
|
+
</Card>
|
|
26
|
+
|
|
27
|
+
<Card title="Service-to-Service Auth" icon="seti:config">
|
|
28
|
+
Secure service authentication using shared secrets for internal communication
|
|
29
|
+
</Card>
|
|
30
|
+
|
|
31
|
+
<Card title="User Impersonation" icon="random">
|
|
32
|
+
Admin capability to impersonate users for support and debugging
|
|
33
|
+
</Card>
|
|
34
|
+
|
|
35
|
+
<Card title="OAuth 2.0 / OIDC" icon="seti:graphql">
|
|
36
|
+
Standard OAuth 2.0 Authorization Code flow with OpenID Connect
|
|
37
|
+
</Card>
|
|
38
|
+
</CardGrid>
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## JWT-Based Authentication
|
|
43
|
+
|
|
44
|
+
### JWT Token Structure
|
|
45
|
+
|
|
46
|
+
The platform uses JWT (JSON Web Tokens) as the primary session mechanism.
|
|
47
|
+
|
|
48
|
+
**JWT Payload**:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
interface JWTPayload {
|
|
52
|
+
// User identity
|
|
53
|
+
id: string; // User ID
|
|
54
|
+
username: string; // Email or username
|
|
55
|
+
displayName: string; // Full name
|
|
56
|
+
authProviderName: string; // IDP name (okta, entraid, etc.)
|
|
57
|
+
|
|
58
|
+
// Authorization
|
|
59
|
+
groups: string[]; // User's groups
|
|
60
|
+
applications: string[]; // Allowed application IDs
|
|
61
|
+
|
|
62
|
+
// Optional fields
|
|
63
|
+
orig?: any; // Original user object from IDP
|
|
64
|
+
impersonatedBy?: { // If user is being impersonated
|
|
65
|
+
username: string;
|
|
66
|
+
displayName: string;
|
|
67
|
+
orig?: any;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Standard JWT claims
|
|
71
|
+
iat: number; // Issued at
|
|
72
|
+
exp: number; // Expiration
|
|
73
|
+
iss: string; // Issuer
|
|
74
|
+
aud: string; // Audience
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Example JWT**:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"id": "user-abc-123",
|
|
83
|
+
"username": "john.doe@acme.com",
|
|
84
|
+
"displayName": "John Doe",
|
|
85
|
+
"authProviderName": "okta",
|
|
86
|
+
"groups": ["admins", "users"],
|
|
87
|
+
"applications": ["app-1", "app-2", "app-3"],
|
|
88
|
+
"iat": 1704067200,
|
|
89
|
+
"exp": 1704153600,
|
|
90
|
+
"iss": "https://platform.acme.com",
|
|
91
|
+
"aud": "platform-users"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Cookie-Based Storage
|
|
96
|
+
|
|
97
|
+
JWT tokens are stored in HTTP-only cookies for security.
|
|
98
|
+
|
|
99
|
+
**Cookie Configuration**:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// Set authentication cookie
|
|
103
|
+
response.cookie(AUTH_COOKIE_NAME, jwtToken, {
|
|
104
|
+
httpOnly: true, // Not accessible via JavaScript
|
|
105
|
+
secure: true, // Only sent over HTTPS
|
|
106
|
+
sameSite: 'lax', // CSRF protection
|
|
107
|
+
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
108
|
+
path: '/', // Available for all paths
|
|
109
|
+
domain: '.platform.com' // Available for all subdomains
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
<Aside type="tip" title="Security Benefits">
|
|
114
|
+
HTTP-only cookies prevent XSS attacks since JavaScript cannot access the token. The `secure` flag ensures tokens are only sent over HTTPS. The `sameSite` attribute provides CSRF protection.
|
|
115
|
+
</Aside>
|
|
116
|
+
|
|
117
|
+
### Token Validation Flow
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
┌──────────┐
|
|
121
|
+
│ Browser │
|
|
122
|
+
└────┬─────┘
|
|
123
|
+
│ GET /api/data
|
|
124
|
+
│ Cookie: auth_token=eyJhbGc...
|
|
125
|
+
▼
|
|
126
|
+
┌─────────────────────────────────┐
|
|
127
|
+
│ Gateway Authentication Guard │
|
|
128
|
+
│ │
|
|
129
|
+
│ 1. Extract token from cookie │
|
|
130
|
+
│ ↓ │
|
|
131
|
+
│ 2. Verify JWT signature │
|
|
132
|
+
│ ↓ │
|
|
133
|
+
│ 3. Check expiration │
|
|
134
|
+
│ ↓ │
|
|
135
|
+
│ 4. Decode payload │
|
|
136
|
+
│ ↓ │
|
|
137
|
+
│ 5. Create AuthUser object │
|
|
138
|
+
└────┬────────────────────────────┘
|
|
139
|
+
│ AuthUser { username, groups, ... }
|
|
140
|
+
▼
|
|
141
|
+
┌─────────────────────────────────┐
|
|
142
|
+
│ Store in Platform Context │
|
|
143
|
+
└─────────────────────────────────┘
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Implementation**:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
@Injectable()
|
|
150
|
+
export class AuthenticationService {
|
|
151
|
+
async validateToken(token: string): Promise<AuthUserWithApplications> {
|
|
152
|
+
// Get JWT secret (can be stored in parameters table)
|
|
153
|
+
const secret = await this.parametersService.getParameterOrDefault(
|
|
154
|
+
'AUTHENTICATION_SESSION_JWT_SECRET',
|
|
155
|
+
this.configService.get<string>('jwtSecret')
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Verify and decode JWT
|
|
159
|
+
const payload = this.jwtService.verify(token, { secret });
|
|
160
|
+
|
|
161
|
+
// Map to AuthUser
|
|
162
|
+
return {
|
|
163
|
+
id: payload.id,
|
|
164
|
+
username: payload.username,
|
|
165
|
+
displayName: payload.displayName,
|
|
166
|
+
authProviderName: payload.authProviderName,
|
|
167
|
+
groups: payload.groups || [],
|
|
168
|
+
applications: payload.applications || [],
|
|
169
|
+
impersonatedBy: payload.impersonatedBy,
|
|
170
|
+
orig: payload.orig
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Multi-IDP Architecture
|
|
179
|
+
|
|
180
|
+
The platform supports multiple identity providers through a pluggable architecture.
|
|
181
|
+
|
|
182
|
+
### Supported Identity Providers
|
|
183
|
+
|
|
184
|
+
<Tabs>
|
|
185
|
+
<TabItem label="Okta">
|
|
186
|
+
**Configuration**:
|
|
187
|
+
```bash
|
|
188
|
+
OKTA_DOMAIN=dev-12345.okta.com
|
|
189
|
+
OKTA_CLIENT_ID=0oa1b2c3d4e5f6g7h8i9
|
|
190
|
+
OKTA_CLIENT_SECRET=secret
|
|
191
|
+
OKTA_REDIRECT_URI=https://platform.com/auth/okta/callback
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Features**:
|
|
195
|
+
- User authentication via Okta
|
|
196
|
+
- Group synchronization from Okta groups
|
|
197
|
+
- Automatic user provisioning
|
|
198
|
+
- Support for Okta Universal Directory
|
|
199
|
+
</TabItem>
|
|
200
|
+
|
|
201
|
+
<TabItem label="Microsoft EntraId">
|
|
202
|
+
**Configuration**:
|
|
203
|
+
```bash
|
|
204
|
+
ENTRAID_TENANT_ID=12345678-1234-1234-1234-123456789012
|
|
205
|
+
ENTRAID_CLIENT_ID=87654321-4321-4321-4321-210987654321
|
|
206
|
+
ENTRAID_CLIENT_SECRET=secret
|
|
207
|
+
ENTRAID_REDIRECT_URI=https://platform.com/auth/entraid/callback
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Features**:
|
|
211
|
+
- Azure AD authentication
|
|
212
|
+
- Integration with Microsoft 365
|
|
213
|
+
- Security group synchronization
|
|
214
|
+
- Conditional access policy support
|
|
215
|
+
</TabItem>
|
|
216
|
+
|
|
217
|
+
<TabItem label="OneLogin">
|
|
218
|
+
**Configuration**:
|
|
219
|
+
```bash
|
|
220
|
+
ONELOGIN_SUBDOMAIN=acme
|
|
221
|
+
ONELOGIN_CLIENT_ID=abc123def456
|
|
222
|
+
ONELOGIN_CLIENT_SECRET=secret
|
|
223
|
+
ONELOGIN_REDIRECT_URI=https://platform.com/auth/onelogin/callback
|
|
224
|
+
ONELOGIN_REGION=us
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Features**:
|
|
228
|
+
- OneLogin SSO
|
|
229
|
+
- Role and group mapping
|
|
230
|
+
- API access for user management
|
|
231
|
+
</TabItem>
|
|
232
|
+
|
|
233
|
+
<TabItem label="Github">
|
|
234
|
+
**Configuration**:
|
|
235
|
+
```bash
|
|
236
|
+
GITHUB_CLIENT_ID=Iv1.a1b2c3d4e5f6g7h8
|
|
237
|
+
GITHUB_CLIENT_SECRET=secret
|
|
238
|
+
GITHUB_REDIRECT_URI=https://platform.com/auth/github/callback
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Features**:
|
|
242
|
+
- GitHub OAuth authentication
|
|
243
|
+
- Organization membership sync
|
|
244
|
+
- Team-based authorization
|
|
245
|
+
</TabItem>
|
|
246
|
+
|
|
247
|
+
<TabItem label="AWS Cognito">
|
|
248
|
+
**Configuration**:
|
|
249
|
+
```bash
|
|
250
|
+
COGNITO_USER_POOL_ID=us-east-1_ABC123DEF
|
|
251
|
+
COGNITO_CLIENT_ID=1a2b3c4d5e6f7g8h9i0j1k2l3m
|
|
252
|
+
COGNITO_CLIENT_SECRET=secret
|
|
253
|
+
COGNITO_DOMAIN=acme-auth.auth.us-east-1.amazoncognito.com
|
|
254
|
+
COGNITO_REDIRECT_URI=https://platform.com/auth/cognito/callback
|
|
255
|
+
COGNITO_REGION=us-east-1
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Features**:
|
|
259
|
+
- AWS Cognito user pools
|
|
260
|
+
- Custom attributes support
|
|
261
|
+
- MFA integration
|
|
262
|
+
- Lambda trigger support
|
|
263
|
+
</TabItem>
|
|
264
|
+
</Tabs>
|
|
265
|
+
|
|
266
|
+
### OAuth Provider Architecture
|
|
267
|
+
|
|
268
|
+
**Location**: `apps/pae-nestjs-gateway-service/src/authentication/oauth-providers/`
|
|
269
|
+
|
|
270
|
+
Each provider implements a standard interface:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
interface OAuthProvider {
|
|
274
|
+
// Provider metadata
|
|
275
|
+
name: string;
|
|
276
|
+
displayName: string;
|
|
277
|
+
|
|
278
|
+
// OAuth configuration
|
|
279
|
+
getConfig(): OAuthConfig;
|
|
280
|
+
|
|
281
|
+
// User info mapping
|
|
282
|
+
mapUserInfo(rawUserInfo: any): UserInfo;
|
|
283
|
+
|
|
284
|
+
// Group synchronization
|
|
285
|
+
getUserGroups(userId: string, accessToken: string): Promise<string[]>;
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Provider Structure**:
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
oauth-providers/
|
|
293
|
+
├── okta/
|
|
294
|
+
│ ├── index.ts # Provider implementation
|
|
295
|
+
│ ├── provider.config.ts # OAuth configuration
|
|
296
|
+
│ ├── mapper.ts # User info mapping
|
|
297
|
+
│ ├── types.ts # TypeScript types
|
|
298
|
+
│ └── groups.service.ts # Group sync logic
|
|
299
|
+
├── entra-id/
|
|
300
|
+
│ ├── index.ts
|
|
301
|
+
│ ├── provider.config.ts
|
|
302
|
+
│ ├── mapper.ts
|
|
303
|
+
│ └── api/ # MS Graph API integration
|
|
304
|
+
│ ├── get-groups.ts
|
|
305
|
+
│ └── get-groups-for-user.ts
|
|
306
|
+
├── onelogin/
|
|
307
|
+
│ ├── index.ts
|
|
308
|
+
│ ├── provider.config.ts
|
|
309
|
+
│ ├── mapper.ts
|
|
310
|
+
│ └── api/ # OneLogin API integration
|
|
311
|
+
├── github/
|
|
312
|
+
│ ├── index.ts
|
|
313
|
+
│ ├── provider.config.ts
|
|
314
|
+
│ └── mapper.ts
|
|
315
|
+
├── cognito/
|
|
316
|
+
│ ├── index.ts
|
|
317
|
+
│ ├── provider.config.ts
|
|
318
|
+
│ ├── mapper.ts
|
|
319
|
+
│ └── api/ # Cognito API integration
|
|
320
|
+
└── providers.registry.ts # Provider registration
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## OAuth 2.0 Authorization Code Flow
|
|
326
|
+
|
|
327
|
+
The platform implements the standard OAuth 2.0 Authorization Code flow with PKCE.
|
|
328
|
+
|
|
329
|
+
### Authentication Flow Diagram
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
┌─────────┐ ┌──────────┐
|
|
333
|
+
│ Browser │ │ IDP │
|
|
334
|
+
│ │ │ (Okta) │
|
|
335
|
+
└────┬────┘ └─────┬────┘
|
|
336
|
+
│ │
|
|
337
|
+
│ 1. User clicks "Login" │
|
|
338
|
+
│────────────────────────────────▶ │
|
|
339
|
+
│ │
|
|
340
|
+
│ 2. Redirect to /auth/login │
|
|
341
|
+
│────────────────────────────────▶ │
|
|
342
|
+
│ │
|
|
343
|
+
│ 3. Redirect to IDP login page │
|
|
344
|
+
│─────────────────────────────────────────────────────▶
|
|
345
|
+
│ GET /authorize? │
|
|
346
|
+
│ client_id=... │
|
|
347
|
+
│ &redirect_uri=... │
|
|
348
|
+
│ &response_type=code │
|
|
349
|
+
│ &scope=openid profile email │
|
|
350
|
+
│ &state=random-state │
|
|
351
|
+
│ &code_challenge=... │
|
|
352
|
+
│ │
|
|
353
|
+
│ 4. User authenticates with IDP │
|
|
354
|
+
│◀─────────────────────────────────────────────────────
|
|
355
|
+
│ (Username + Password + MFA) │
|
|
356
|
+
│ │
|
|
357
|
+
│ 5. IDP redirects back with code │
|
|
358
|
+
│◀─────────────────────────────────────────────────────
|
|
359
|
+
│ GET /auth/callback? │
|
|
360
|
+
│ code=auth-code-123 │
|
|
361
|
+
│ &state=random-state │
|
|
362
|
+
│ │
|
|
363
|
+
│ 6. Gateway receives callback │
|
|
364
|
+
│────────────────────────────────▶ │
|
|
365
|
+
│ │
|
|
366
|
+
│ 7. Exchange code for tokens │
|
|
367
|
+
│─────────────────────────────────────────────────────▶
|
|
368
|
+
│ POST /token │
|
|
369
|
+
│ code=auth-code-123 │
|
|
370
|
+
│ &client_id=... │
|
|
371
|
+
│ &client_secret=... │
|
|
372
|
+
│ &redirect_uri=... │
|
|
373
|
+
│ &code_verifier=... │
|
|
374
|
+
│ │
|
|
375
|
+
│ 8. Receive tokens │
|
|
376
|
+
│◀─────────────────────────────────────────────────────
|
|
377
|
+
│ { │
|
|
378
|
+
│ "access_token": "eyJhbGc...", │
|
|
379
|
+
│ "id_token": "eyJhbGc...", │
|
|
380
|
+
│ "refresh_token": "...", │
|
|
381
|
+
│ "expires_in": 3600 │
|
|
382
|
+
│ } │
|
|
383
|
+
│ │
|
|
384
|
+
│ 9. Get user info from IDP │
|
|
385
|
+
│─────────────────────────────────────────────────────▶
|
|
386
|
+
│ GET /userinfo │
|
|
387
|
+
│ Authorization: Bearer eyJhbGc... │
|
|
388
|
+
│ │
|
|
389
|
+
│ 10. Receive user info │
|
|
390
|
+
│◀─────────────────────────────────────────────────────
|
|
391
|
+
│ { │
|
|
392
|
+
│ "sub": "user-123", │
|
|
393
|
+
│ "email": "john@acme.com", │
|
|
394
|
+
│ "name": "John Doe" │
|
|
395
|
+
│ } │
|
|
396
|
+
│ │
|
|
397
|
+
│ 11. Create platform user (if new) │
|
|
398
|
+
│ 12. Get user's groups │
|
|
399
|
+
│ 13. Generate platform JWT │
|
|
400
|
+
│ 14. Set HTTP-only cookie │
|
|
401
|
+
│◀──────────────────────────────── │
|
|
402
|
+
│ Set-Cookie: auth_token=eyJhbGc... │
|
|
403
|
+
│ │
|
|
404
|
+
│ 15. Redirect to application │
|
|
405
|
+
│◀──────────────────────────────── │
|
|
406
|
+
│ Location: / │
|
|
407
|
+
│ │
|
|
408
|
+
│ 16. Access application with cookie │
|
|
409
|
+
│────────────────────────────────▶ │
|
|
410
|
+
│ │
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Implementation Details
|
|
414
|
+
|
|
415
|
+
<Tabs>
|
|
416
|
+
<TabItem label="1. Login Initiation">
|
|
417
|
+
```typescript
|
|
418
|
+
// apps/pae-nestjs-gateway-service/src/authentication/authentication.controller.ts
|
|
419
|
+
|
|
420
|
+
@Get('auth/login')
|
|
421
|
+
async login(
|
|
422
|
+
@Query('provider') provider: string,
|
|
423
|
+
@Res() response: FastifyReply
|
|
424
|
+
) {
|
|
425
|
+
// Get OAuth provider configuration
|
|
426
|
+
const oauthProvider = this.providersRegistry.getProvider(provider);
|
|
427
|
+
const config = oauthProvider.getConfig();
|
|
428
|
+
|
|
429
|
+
// Generate PKCE code verifier and challenge
|
|
430
|
+
const codeVerifier = generateCodeVerifier();
|
|
431
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
432
|
+
|
|
433
|
+
// Generate state for CSRF protection
|
|
434
|
+
const state = generateRandomState();
|
|
435
|
+
|
|
436
|
+
// Store in session
|
|
437
|
+
await this.sessionStore.set(state, {
|
|
438
|
+
codeVerifier,
|
|
439
|
+
provider,
|
|
440
|
+
returnTo: request.query.returnTo
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Build authorization URL
|
|
444
|
+
const authUrl = new URL(config.authorizationEndpoint);
|
|
445
|
+
authUrl.searchParams.set('client_id', config.clientId);
|
|
446
|
+
authUrl.searchParams.set('redirect_uri', config.redirectUri);
|
|
447
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
448
|
+
authUrl.searchParams.set('scope', config.scope);
|
|
449
|
+
authUrl.searchParams.set('state', state);
|
|
450
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
451
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
452
|
+
|
|
453
|
+
// Redirect to IDP
|
|
454
|
+
response.redirect(authUrl.toString());
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
</TabItem>
|
|
458
|
+
|
|
459
|
+
<TabItem label="2. Callback Handling">
|
|
460
|
+
```typescript
|
|
461
|
+
@Get('auth/callback')
|
|
462
|
+
async callback(
|
|
463
|
+
@Query('code') code: string,
|
|
464
|
+
@Query('state') state: string,
|
|
465
|
+
@Res() response: FastifyReply
|
|
466
|
+
) {
|
|
467
|
+
// Retrieve session data
|
|
468
|
+
const session = await this.sessionStore.get(state);
|
|
469
|
+
if (!session) {
|
|
470
|
+
throw new UnauthorizedException('Invalid state');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Get provider
|
|
474
|
+
const oauthProvider = this.providersRegistry.getProvider(session.provider);
|
|
475
|
+
const config = oauthProvider.getConfig();
|
|
476
|
+
|
|
477
|
+
// Exchange code for tokens
|
|
478
|
+
const tokenResponse = await axios.post(config.tokenEndpoint, {
|
|
479
|
+
grant_type: 'authorization_code',
|
|
480
|
+
code,
|
|
481
|
+
client_id: config.clientId,
|
|
482
|
+
client_secret: config.clientSecret,
|
|
483
|
+
redirect_uri: config.redirectUri,
|
|
484
|
+
code_verifier: session.codeVerifier
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const { access_token, id_token, refresh_token } = tokenResponse.data;
|
|
488
|
+
|
|
489
|
+
// Get user info
|
|
490
|
+
const userInfoResponse = await axios.get(config.userInfoEndpoint, {
|
|
491
|
+
headers: {
|
|
492
|
+
Authorization: `Bearer ${access_token}`
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Map to platform user
|
|
497
|
+
const userInfo = oauthProvider.mapUserInfo(userInfoResponse.data);
|
|
498
|
+
|
|
499
|
+
// Continue with user creation/update...
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
</TabItem>
|
|
503
|
+
|
|
504
|
+
<TabItem label="3. User Provisioning">
|
|
505
|
+
```typescript
|
|
506
|
+
// Create or update user in platform database
|
|
507
|
+
let user = await this.usersService.findByEmail(userInfo.email);
|
|
508
|
+
|
|
509
|
+
if (!user) {
|
|
510
|
+
// Create new user
|
|
511
|
+
user = await this.usersService.create({
|
|
512
|
+
email: userInfo.email,
|
|
513
|
+
displayName: userInfo.name,
|
|
514
|
+
authProviderName: session.provider,
|
|
515
|
+
authProviderUserId: userInfo.id,
|
|
516
|
+
tenantId: userInfo.tenantId // Determined from email domain or IDP config
|
|
517
|
+
});
|
|
518
|
+
} else {
|
|
519
|
+
// Update existing user
|
|
520
|
+
await this.usersService.update(user.id, {
|
|
521
|
+
displayName: userInfo.name,
|
|
522
|
+
lastLoginAt: new Date()
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Sync groups from IDP
|
|
527
|
+
const groups = await oauthProvider.getUserGroups(userInfo.id, access_token);
|
|
528
|
+
await this.groupsService.syncUserGroups(user.id, groups);
|
|
529
|
+
|
|
530
|
+
// Get user's allowed applications
|
|
531
|
+
const applications = await this.authzService.getAllowedApplications(user.username);
|
|
532
|
+
|
|
533
|
+
// Generate platform JWT
|
|
534
|
+
const jwtPayload = {
|
|
535
|
+
id: user.id,
|
|
536
|
+
username: user.email,
|
|
537
|
+
displayName: user.displayName,
|
|
538
|
+
authProviderName: session.provider,
|
|
539
|
+
groups,
|
|
540
|
+
applications: applications.map(app => app.id)
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const jwtToken = this.jwtService.sign(jwtPayload);
|
|
544
|
+
|
|
545
|
+
// Set HTTP-only cookie
|
|
546
|
+
response.cookie('auth_token', jwtToken, {
|
|
547
|
+
httpOnly: true,
|
|
548
|
+
secure: true,
|
|
549
|
+
sameSite: 'lax',
|
|
550
|
+
maxAge: 24 * 60 * 60 * 1000
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Redirect to application
|
|
554
|
+
response.redirect(session.returnTo || '/');
|
|
555
|
+
```
|
|
556
|
+
</TabItem>
|
|
557
|
+
</Tabs>
|
|
558
|
+
|
|
559
|
+
<Aside type="note" title="PKCE Security">
|
|
560
|
+
The platform uses PKCE (Proof Key for Code Exchange) to protect against authorization code interception attacks. The `code_challenge` is sent during authorization, and the `code_verifier` is sent during token exchange, preventing attackers from using stolen authorization codes.
|
|
561
|
+
</Aside>
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## API Key Authentication
|
|
566
|
+
|
|
567
|
+
API keys provide long-lived authentication for programmatic access.
|
|
568
|
+
|
|
569
|
+
### API Key Structure
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
interface ApiKey {
|
|
573
|
+
id: string;
|
|
574
|
+
name: string; // Human-readable name
|
|
575
|
+
key: string; // The actual key (hashed in DB)
|
|
576
|
+
userId: string; // Associated user
|
|
577
|
+
tenantId: string; // Tenant scope
|
|
578
|
+
permissions: string[]; // Allowed operations
|
|
579
|
+
expiresAt?: Date; // Optional expiration
|
|
580
|
+
lastUsedAt?: Date; // Track usage
|
|
581
|
+
createdAt: Date;
|
|
582
|
+
createdBy: string;
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### API Key Generation
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
@Post('api-keys')
|
|
590
|
+
async createApiKey(
|
|
591
|
+
@Body() dto: CreateApiKeyDto,
|
|
592
|
+
@User() user: AuthUserWithApplications
|
|
593
|
+
) {
|
|
594
|
+
// Generate secure random key
|
|
595
|
+
const apiKey = crypto.randomBytes(32).toString('base64url');
|
|
596
|
+
|
|
597
|
+
// Hash for storage (using bcrypt)
|
|
598
|
+
const hashedKey = await bcrypt.hash(apiKey, 10);
|
|
599
|
+
|
|
600
|
+
// Store in database
|
|
601
|
+
await this.apiKeysService.create({
|
|
602
|
+
name: dto.name,
|
|
603
|
+
key: hashedKey,
|
|
604
|
+
userId: user.id,
|
|
605
|
+
tenantId: user.tenantId,
|
|
606
|
+
permissions: dto.permissions,
|
|
607
|
+
expiresAt: dto.expiresAt
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Return key ONCE (cannot be retrieved again)
|
|
611
|
+
return {
|
|
612
|
+
apiKey,
|
|
613
|
+
message: 'Save this key securely - it cannot be retrieved again'
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### API Key Usage
|
|
619
|
+
|
|
620
|
+
```bash
|
|
621
|
+
# Use API key in Authorization header
|
|
622
|
+
curl -H "Authorization: Bearer pae_abc123def456ghi789jkl012mno345pqr678stu901vwx234" \
|
|
623
|
+
https://platform.com/api/data
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
**Validation Flow**:
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
async validateApiKey(apiKey: string): Promise<AuthUserWithApplications | null> {
|
|
630
|
+
// Find API key in database
|
|
631
|
+
const storedKey = await this.apiKeysRepository.findByKey(apiKey);
|
|
632
|
+
if (!storedKey) {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Verify key matches (compare hashes)
|
|
637
|
+
const isValid = await bcrypt.compare(apiKey, storedKey.key);
|
|
638
|
+
if (!isValid) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Check expiration
|
|
643
|
+
if (storedKey.expiresAt && storedKey.expiresAt < new Date()) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Update last used
|
|
648
|
+
await this.apiKeysRepository.updateLastUsed(storedKey.id);
|
|
649
|
+
|
|
650
|
+
// Load user
|
|
651
|
+
const user = await this.usersService.findById(storedKey.userId);
|
|
652
|
+
|
|
653
|
+
// Return user context with API key permissions
|
|
654
|
+
return {
|
|
655
|
+
id: user.id,
|
|
656
|
+
username: user.email,
|
|
657
|
+
displayName: user.displayName,
|
|
658
|
+
authProviderName: 'api-key',
|
|
659
|
+
groups: user.groups,
|
|
660
|
+
applications: storedKey.permissions
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## Service-to-Service Authentication
|
|
668
|
+
|
|
669
|
+
Services authenticate with each other using a shared secret.
|
|
670
|
+
|
|
671
|
+
### Configuration
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
# Set the same secret on all services
|
|
675
|
+
SERVICE_ACCESS_SECRET=shared-secret-for-internal-services
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Usage
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
// Service A calling Service B
|
|
682
|
+
const response = await axios.get('http://service-b/api/data', {
|
|
683
|
+
headers: {
|
|
684
|
+
'Authorization': `Bearer ${process.env.SERVICE_ACCESS_SECRET}`,
|
|
685
|
+
'x-forwarded-user': 'service-a' // Optional: identify the calling service
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Validation
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
async validateApiKey(apiKey: string, req: FastifyRequest["raw"]): Promise<AuthUserWithApplications | null> {
|
|
694
|
+
// Check if it's the service access secret
|
|
695
|
+
if (apiKey === process.env.SERVICE_ACCESS_SECRET) {
|
|
696
|
+
// Extract calling service name
|
|
697
|
+
const author = req.headers['x-forwarded-user'] as string | undefined;
|
|
698
|
+
|
|
699
|
+
// Return service account
|
|
700
|
+
return {
|
|
701
|
+
id: 'service',
|
|
702
|
+
username: author ?? 'service',
|
|
703
|
+
displayName: 'Service Account',
|
|
704
|
+
authProviderName: 'service-access',
|
|
705
|
+
groups: [],
|
|
706
|
+
applications: [] // Services typically have full access
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Otherwise, check if it's a regular API key
|
|
711
|
+
return this.apiKeysService.getUserForApiKey(apiKey);
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
<Aside type="caution" title="Security Warning">
|
|
716
|
+
The `SERVICE_ACCESS_SECRET` grants full platform access. Rotate it regularly and never expose it in logs or client-side code. Use it only for internal service-to-service communication.
|
|
717
|
+
</Aside>
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## User Impersonation
|
|
722
|
+
|
|
723
|
+
Admins can impersonate users for support and debugging purposes.
|
|
724
|
+
|
|
725
|
+
### Impersonation Configuration
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
// Database: impersonation_configs table
|
|
729
|
+
interface ImpersonationConfig {
|
|
730
|
+
id: string;
|
|
731
|
+
username: string; // Admin who can impersonate
|
|
732
|
+
restrictedTo: string[]; // Optional: limit to specific users
|
|
733
|
+
createdAt: Date;
|
|
734
|
+
createdBy: string;
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### Impersonation Flow
|
|
739
|
+
|
|
740
|
+
```
|
|
741
|
+
1. Admin requests to impersonate user
|
|
742
|
+
↓
|
|
743
|
+
2. Check if admin has impersonation permission
|
|
744
|
+
↓
|
|
745
|
+
3. Check if target user is in restricted list (if applicable)
|
|
746
|
+
↓
|
|
747
|
+
4. Create new JWT with impersonation data
|
|
748
|
+
↓
|
|
749
|
+
5. Set impersonation mode cookie
|
|
750
|
+
↓
|
|
751
|
+
6. All requests now use impersonated user's context
|
|
752
|
+
↓
|
|
753
|
+
7. Admin can stop impersonation to return to their account
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
**Implementation**:
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
@Post('impersonate')
|
|
760
|
+
async startImpersonation(
|
|
761
|
+
@Body() dto: { targetUsername: string },
|
|
762
|
+
@User() admin: AuthUserWithApplications
|
|
763
|
+
) {
|
|
764
|
+
// Check if admin can impersonate
|
|
765
|
+
const config = await this.impersonationService.getConfig(admin.username);
|
|
766
|
+
if (!config) {
|
|
767
|
+
throw new ForbiddenException('You do not have impersonation permission');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Check if target user is allowed
|
|
771
|
+
if (config.restrictedTo.length > 0 && !config.restrictedTo.includes(dto.targetUsername)) {
|
|
772
|
+
throw new ForbiddenException('You cannot impersonate this user');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Load target user
|
|
776
|
+
const targetUser = await this.usersService.findByUsername(dto.targetUsername);
|
|
777
|
+
if (!targetUser) {
|
|
778
|
+
throw new NotFoundException('User not found');
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Create JWT with impersonation
|
|
782
|
+
const jwtPayload = {
|
|
783
|
+
...targetUser,
|
|
784
|
+
impersonatedBy: {
|
|
785
|
+
username: admin.username,
|
|
786
|
+
displayName: admin.displayName,
|
|
787
|
+
orig: admin.orig
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const jwtToken = this.jwtService.sign(jwtPayload);
|
|
792
|
+
|
|
793
|
+
// Set cookies
|
|
794
|
+
response.cookie('auth_token', jwtToken, { httpOnly: true, secure: true });
|
|
795
|
+
response.cookie('impersonation_mode', 'true', { httpOnly: false }); // Client needs to read this
|
|
796
|
+
|
|
797
|
+
return {
|
|
798
|
+
message: 'Impersonation started',
|
|
799
|
+
targetUser: dto.targetUsername
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
<Aside type="tip" title="Audit Trail">
|
|
805
|
+
All actions performed during impersonation are logged with both the impersonated user and the admin who initiated impersonation. This ensures accountability and compliance.
|
|
806
|
+
</Aside>
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
## Session Management
|
|
811
|
+
|
|
812
|
+
### Session States
|
|
813
|
+
|
|
814
|
+
The platform tracks three session states:
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
type SessionStatus = 'valid' | 'expired' | 'invalid';
|
|
818
|
+
|
|
819
|
+
async getSessionStatus(token: string): Promise<SessionStatus> {
|
|
820
|
+
if (!token) return 'invalid';
|
|
821
|
+
|
|
822
|
+
try {
|
|
823
|
+
await this.validateToken(token);
|
|
824
|
+
return 'valid';
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if (error?.name === 'TokenExpiredError') {
|
|
827
|
+
return 'expired';
|
|
828
|
+
}
|
|
829
|
+
return 'invalid';
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### Handling Invalid Sessions
|
|
835
|
+
|
|
836
|
+
```typescript
|
|
837
|
+
handleInvalidSession(context: ExecutionContext, session: Session) {
|
|
838
|
+
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
|
839
|
+
const response = context.switchToHttp().getResponse<FastifyReply>();
|
|
840
|
+
|
|
841
|
+
// Check if this is a browser request
|
|
842
|
+
if (this.isNoRedirectRequest(request)) {
|
|
843
|
+
// API/non-browser request: return 401
|
|
844
|
+
response.status(401).send({
|
|
845
|
+
message: session.status === 'expired' ? 'Session expired' : 'Unauthorized',
|
|
846
|
+
authRedirectURL: '/'
|
|
847
|
+
});
|
|
848
|
+
} else {
|
|
849
|
+
// Browser request: redirect to login
|
|
850
|
+
const returnTo = request.url;
|
|
851
|
+
response.redirect(`/auth/login?returnTo=${encodeURIComponent(returnTo)}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
## Security Considerations
|
|
859
|
+
|
|
860
|
+
<Aside type="caution" title="Authentication Security Best Practices">
|
|
861
|
+
|
|
862
|
+
**JWT Security**:
|
|
863
|
+
- Use strong secrets (minimum 256 bits)
|
|
864
|
+
- Rotate secrets regularly
|
|
865
|
+
- Keep tokens short-lived (< 24 hours)
|
|
866
|
+
- Never store JWTs in localStorage (XSS risk)
|
|
867
|
+
|
|
868
|
+
**Cookie Security**:
|
|
869
|
+
- Always use `httpOnly` flag
|
|
870
|
+
- Always use `secure` flag in production
|
|
871
|
+
- Use `sameSite: 'lax'` or `strict` for CSRF protection
|
|
872
|
+
- Set appropriate `domain` for subdomain sharing
|
|
873
|
+
|
|
874
|
+
**API Key Security**:
|
|
875
|
+
- Hash API keys before storage (never store plaintext)
|
|
876
|
+
- Use cryptographically secure random generation
|
|
877
|
+
- Implement rate limiting
|
|
878
|
+
- Support key rotation
|
|
879
|
+
- Monitor and alert on unusual usage
|
|
880
|
+
|
|
881
|
+
**IDP Integration**:
|
|
882
|
+
- Validate redirect URIs strictly
|
|
883
|
+
- Implement PKCE for OAuth flows
|
|
884
|
+
- Verify state parameter for CSRF protection
|
|
885
|
+
- Use short-lived authorization codes
|
|
886
|
+
- Store IDP tokens securely (encrypted)
|
|
887
|
+
|
|
888
|
+
**General**:
|
|
889
|
+
- Log all authentication events (success and failure)
|
|
890
|
+
- Implement account lockout after failed attempts
|
|
891
|
+
- Use HTTPS everywhere
|
|
892
|
+
- Implement session timeout
|
|
893
|
+
- Support MFA where possible
|
|
894
|
+
|
|
895
|
+
</Aside>
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## Next Steps
|
|
900
|
+
|
|
901
|
+
- **[Authorization System](/_/docs/architecture/authorization-system/)** - Understanding RBAC and permissions
|
|
902
|
+
- **[Gateway Architecture](/_/docs/architecture/gateway-architecture/)** - How gateway processes authentication
|
|
903
|
+
- **[Multi-Tenancy](/_/docs/architecture/multi-tenancy/)** - Tenant isolation and user management
|