@intelicity/gates-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +375 -0
- package/dist/auth/middleware.js +40 -0
- package/dist/auth/verifier.js +37 -0
- package/dist/cache/jwks-cache.js +21 -0
- package/dist/config/env.js +20 -0
- package/dist/errors/error.js +43 -0
- package/dist/index.js +3 -0
- package/dist/models/user.js +1 -0
- package/dist/services/auth-service.js +80 -0
- package/dist/services/user-service.js +97 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# Gates SDK
|
|
2
|
+
|
|
3
|
+
Simple SDK for authenticating users with AWS Cognito JWT tokens and managing user data.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @intelicity/gates-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- š JWT token verification with AWS Cognito
|
|
14
|
+
- š„ User management service with backend integration
|
|
15
|
+
- šÆ TypeScript support with full type definitions
|
|
16
|
+
- š¾ Built-in JWKS caching for better performance
|
|
17
|
+
- šØ Custom error classes for better error handling
|
|
18
|
+
- š Group-based access control
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Authentication Service
|
|
23
|
+
|
|
24
|
+
The `AuthService` provides JWT token verification with AWS Cognito:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { AuthService } from "@intelicity/gates-sdk";
|
|
28
|
+
|
|
29
|
+
const authService = new AuthService(
|
|
30
|
+
"sa-east-1", // AWS region
|
|
31
|
+
"sa-east-1_xxxxxxxxx", // User Pool ID
|
|
32
|
+
"your-client-id", // Audience (client ID)
|
|
33
|
+
["admin", "user"] // Optional: required groups
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Verify a token
|
|
37
|
+
try {
|
|
38
|
+
const user = await authService.verifyToken(token);
|
|
39
|
+
console.log("Authenticated user:", user);
|
|
40
|
+
|
|
41
|
+
// Check group membership
|
|
42
|
+
const isAdmin = authService.isMemberOf(user.groups || []);
|
|
43
|
+
console.log("Has required group:", isAdmin);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("Authentication failed:", error.message);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### User Service
|
|
50
|
+
|
|
51
|
+
The `UserService` provides user management capabilities:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { UserService } from "@intelicity/gates-sdk";
|
|
55
|
+
|
|
56
|
+
const userService = new UserService(
|
|
57
|
+
"https://api.example.com", // Backend API URL
|
|
58
|
+
"your-system-name" // System identifier
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Get all users from the system
|
|
62
|
+
try {
|
|
63
|
+
const users = await userService.getAllUsers(idToken);
|
|
64
|
+
console.log("Users:", users.profiles);
|
|
65
|
+
console.log("Total:", users.total);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Failed to fetch users:", error.message);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get specific user by ID
|
|
71
|
+
try {
|
|
72
|
+
const user = await userService.getUserById(idToken, "user-id-123");
|
|
73
|
+
console.log("User details:", user);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error("Failed to fetch user:", error.message);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Complete Integration Example
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { AuthService, UserService } from "@intelicity/gates-sdk";
|
|
83
|
+
|
|
84
|
+
class MyApplication {
|
|
85
|
+
private authService: AuthService;
|
|
86
|
+
private userService: UserService;
|
|
87
|
+
|
|
88
|
+
constructor() {
|
|
89
|
+
this.authService = new AuthService(
|
|
90
|
+
process.env.AWS_REGION!,
|
|
91
|
+
process.env.COGNITO_USER_POOL_ID!,
|
|
92
|
+
process.env.COGNITO_CLIENT_ID!,
|
|
93
|
+
["admin"] // Only admins can access
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
this.userService = new UserService(
|
|
97
|
+
process.env.BACKEND_URL!,
|
|
98
|
+
process.env.SYSTEM_NAME!
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async handleRequest(authToken: string) {
|
|
103
|
+
try {
|
|
104
|
+
// 1. Authenticate user
|
|
105
|
+
const user = await this.authService.verifyToken(authToken);
|
|
106
|
+
console.log(`User ${user.name} authenticated successfully`);
|
|
107
|
+
|
|
108
|
+
// 2. Get user list if authorized
|
|
109
|
+
const users = await this.userService.getAllUsers(authToken);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
currentUser: user,
|
|
113
|
+
allUsers: users.profiles,
|
|
114
|
+
total: users.total,
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new Error(`Operation failed: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Error Handling
|
|
124
|
+
|
|
125
|
+
The SDK provides custom error classes for better error handling:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import {
|
|
129
|
+
AuthService,
|
|
130
|
+
UserService,
|
|
131
|
+
AuthenticationError,
|
|
132
|
+
TokenExpiredError,
|
|
133
|
+
InvalidTokenError,
|
|
134
|
+
MissingAuthorizationError,
|
|
135
|
+
} from "@intelicity/gates-sdk";
|
|
136
|
+
|
|
137
|
+
const authService = new AuthService(region, userPoolId, audience);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const user = await authService.verifyToken(token);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof TokenExpiredError) {
|
|
143
|
+
console.error("Token expired, please login again");
|
|
144
|
+
} else if (error instanceof InvalidTokenError) {
|
|
145
|
+
console.error("Invalid token provided");
|
|
146
|
+
} else if (error instanceof MissingAuthorizationError) {
|
|
147
|
+
console.error("No authorization token provided");
|
|
148
|
+
} else {
|
|
149
|
+
console.error("Authentication failed:", error.message);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Error handling with User Service
|
|
154
|
+
try {
|
|
155
|
+
const users = await userService.getAllUsers(token);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error.message.includes("HTTP 401")) {
|
|
158
|
+
console.error("Unauthorized - check your token");
|
|
159
|
+
} else if (error.message.includes("HTTP 403")) {
|
|
160
|
+
console.error("Forbidden - insufficient permissions");
|
|
161
|
+
} else {
|
|
162
|
+
console.error("Failed to fetch users:", error.message);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## API Reference
|
|
168
|
+
|
|
169
|
+
### `AuthService`
|
|
170
|
+
|
|
171
|
+
Main service for JWT token verification with AWS Cognito.
|
|
172
|
+
|
|
173
|
+
#### Constructor
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
new AuthService(region, userPoolId, audience, requiredGroup?)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Parameters:**
|
|
180
|
+
|
|
181
|
+
- `region` (string): AWS region (e.g., 'sa-east-1')
|
|
182
|
+
- `userPoolId` (string): Cognito User Pool ID
|
|
183
|
+
- `audience` (string): Expected audience claim (client ID)
|
|
184
|
+
- `requiredGroup` (string | string[], optional): Required Cognito groups
|
|
185
|
+
|
|
186
|
+
#### Methods
|
|
187
|
+
|
|
188
|
+
##### `verifyToken(token: string): Promise<GatesUser>`
|
|
189
|
+
|
|
190
|
+
Verifies a JWT token and returns the authenticated user.
|
|
191
|
+
|
|
192
|
+
##### `isMemberOf(groups: string[]): boolean`
|
|
193
|
+
|
|
194
|
+
Checks if the user groups match the required groups.
|
|
195
|
+
|
|
196
|
+
### `UserService`
|
|
197
|
+
|
|
198
|
+
Service for managing users through a backend API.
|
|
199
|
+
|
|
200
|
+
#### Constructor
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
new UserService(baseUrl, system);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Parameters:**
|
|
207
|
+
|
|
208
|
+
- `baseUrl` (string): Backend API base URL
|
|
209
|
+
- `system` (string): System identifier
|
|
210
|
+
|
|
211
|
+
#### Methods
|
|
212
|
+
|
|
213
|
+
##### `getAllUsers(idToken: string): Promise<UserListResponse>`
|
|
214
|
+
|
|
215
|
+
Retrieves all users from the system.
|
|
216
|
+
|
|
217
|
+
##### `getUserById(idToken: string, userId: string): Promise<Profile>`
|
|
218
|
+
|
|
219
|
+
Retrieves a specific user by ID.
|
|
220
|
+
|
|
221
|
+
### Types
|
|
222
|
+
|
|
223
|
+
#### `GatesUser`
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
type GatesUser = {
|
|
227
|
+
user_id: string; // Mapped from 'sub' claim
|
|
228
|
+
email: string; // User email
|
|
229
|
+
name: string; // User display name
|
|
230
|
+
role: string; // Mapped from 'custom:general_role'
|
|
231
|
+
exp: number; // Token expiration timestamp
|
|
232
|
+
iat: number; // Token issued at timestamp
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `Profile`
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
type Profile = {
|
|
240
|
+
user_id: string;
|
|
241
|
+
email: string;
|
|
242
|
+
name: string;
|
|
243
|
+
enabled: boolean;
|
|
244
|
+
profile_attributes: ProfileAttribute[];
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
type ProfileAttribute = {
|
|
248
|
+
attribute_name: string;
|
|
249
|
+
value: string | boolean | number;
|
|
250
|
+
};
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### `UserListResponse`
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
type UserListResponse = {
|
|
257
|
+
profiles: Profile[];
|
|
258
|
+
total?: number;
|
|
259
|
+
page?: number;
|
|
260
|
+
limit?: number;
|
|
261
|
+
nextToken?: string;
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### `VerifyOptions`
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
type VerifyOptions = {
|
|
269
|
+
region: string;
|
|
270
|
+
userPoolId: string;
|
|
271
|
+
audience: string;
|
|
272
|
+
requiredGroup?: string | string[];
|
|
273
|
+
};
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
#### Custom Errors
|
|
277
|
+
|
|
278
|
+
- `AuthenticationError`: Base authentication error class
|
|
279
|
+
- `TokenExpiredError`: Token has expired
|
|
280
|
+
- `InvalidTokenError`: Token is invalid or malformed
|
|
281
|
+
- `MissingAuthorizationError`: Authorization header is missing
|
|
282
|
+
|
|
283
|
+
## Environment Variables
|
|
284
|
+
|
|
285
|
+
You can configure the SDK using environment variables:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
# .env file
|
|
289
|
+
GATES_REGION=sa-east-1
|
|
290
|
+
GATES_USER_POOL_ID=sa-east-1_xxxxxxxxx
|
|
291
|
+
GATES_CLIENT_ID=your-cognito-client-id
|
|
292
|
+
GATES_BACKEND_URL=https://your-backend-api.com
|
|
293
|
+
GATES_SYSTEM_NAME=your-system-name
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Example usage with environment variables:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { AuthService, UserService } from "@intelicity/gates-sdk";
|
|
300
|
+
|
|
301
|
+
const authService = new AuthService(
|
|
302
|
+
process.env.GATES_REGION!,
|
|
303
|
+
process.env.GATES_USER_POOL_ID!,
|
|
304
|
+
process.env.GATES_CLIENT_ID!
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const userService = new UserService(
|
|
308
|
+
process.env.GATES_BACKEND_URL!,
|
|
309
|
+
process.env.GATES_SYSTEM_NAME!
|
|
310
|
+
);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Note:** The application is responsible for loading the `.env` file (e.g., using `dotenv` package). The SDK reads from `process.env`.
|
|
314
|
+
|
|
315
|
+
## Security Best Practices
|
|
316
|
+
|
|
317
|
+
### Token Handling
|
|
318
|
+
|
|
319
|
+
- Never log or expose JWT tokens in production
|
|
320
|
+
- Always use HTTPS in production environments
|
|
321
|
+
- Implement proper token refresh mechanisms
|
|
322
|
+
- Store tokens securely on the client side
|
|
323
|
+
|
|
324
|
+
### Error Handling
|
|
325
|
+
|
|
326
|
+
- Don't expose sensitive error details to end users
|
|
327
|
+
- Log authentication failures for security monitoring
|
|
328
|
+
- Implement rate limiting for authentication endpoints
|
|
329
|
+
|
|
330
|
+
### Configuration
|
|
331
|
+
|
|
332
|
+
- Use environment variables for sensitive configuration
|
|
333
|
+
- Validate all configuration parameters at startup
|
|
334
|
+
- Use strong, unique audience values (client IDs)
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// Good: Secure error handling
|
|
338
|
+
try {
|
|
339
|
+
const user = await authService.verifyToken(token);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
// Log for monitoring (server-side only)
|
|
342
|
+
console.error('Auth failed:', error.constructor.name);
|
|
343
|
+
|
|
344
|
+
// Return generic error to client
|
|
345
|
+
throw new Error('Authentication failed');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Bad: Exposing sensitive information
|
|
349
|
+
catch (error) {
|
|
350
|
+
throw new Error(`Auth failed: ${error.message}`); // May expose internal details
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Development
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
# Install dependencies
|
|
358
|
+
npm install
|
|
359
|
+
|
|
360
|
+
# Build
|
|
361
|
+
npm run build
|
|
362
|
+
|
|
363
|
+
# Lint
|
|
364
|
+
npm run lint
|
|
365
|
+
|
|
366
|
+
# Type check
|
|
367
|
+
npm run typecheck
|
|
368
|
+
|
|
369
|
+
# Run tests
|
|
370
|
+
npm test
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## License
|
|
374
|
+
|
|
375
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// // src/auth/middleware.ts
|
|
2
|
+
// import type { FastifyRequest, FastifyReply } from "fastify";
|
|
3
|
+
// import { verifyToken, type VerifyOptions } from "./verifier.js";
|
|
4
|
+
// import {
|
|
5
|
+
// MissingAuthorizationError,
|
|
6
|
+
// InvalidTokenError,
|
|
7
|
+
// TokenExpiredError,
|
|
8
|
+
// } from "../errors/error.js";
|
|
9
|
+
// import type { GatesUser } from "../models/user.js";
|
|
10
|
+
export {};
|
|
11
|
+
// // Extend FastifyRequest to include user
|
|
12
|
+
// declare module "fastify" {
|
|
13
|
+
// interface FastifyRequest {
|
|
14
|
+
// user?: GatesUser;
|
|
15
|
+
// }
|
|
16
|
+
// }
|
|
17
|
+
// export type GatesMiddlewareConfig = VerifyOptions;
|
|
18
|
+
// /**
|
|
19
|
+
// * Fastify middleware to verify JWT tokens from AWS Cognito
|
|
20
|
+
// * @param config Configuration with region, userPoolId, and optional audience
|
|
21
|
+
// * @returns Fastify preHandler hook
|
|
22
|
+
// */
|
|
23
|
+
// export function gatesMiddleware(config: GatesMiddlewareConfig) {
|
|
24
|
+
// return async (request: FastifyRequest, reply: FastifyReply) => {
|
|
25
|
+
// const authHeader = request.headers.authorization;
|
|
26
|
+
// if (!authHeader) {
|
|
27
|
+
// throw new MissingAuthorizationError();
|
|
28
|
+
// }
|
|
29
|
+
// const token = authHeader.replace(/^Bearer\s+/i, "");
|
|
30
|
+
// try {
|
|
31
|
+
// const user = await verifyToken(token, config);
|
|
32
|
+
// request.user = user;
|
|
33
|
+
// } catch (err) {
|
|
34
|
+
// if (err instanceof Error && err.message.includes("expired")) {
|
|
35
|
+
// throw new TokenExpiredError();
|
|
36
|
+
// }
|
|
37
|
+
// throw new InvalidTokenError();
|
|
38
|
+
// }
|
|
39
|
+
// };
|
|
40
|
+
// }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jwtVerify } from "jose";
|
|
2
|
+
import { getJwks } from "../cache/jwks-cache.js";
|
|
3
|
+
export async function verifyToken(token, { region, userPoolId, audience, requiredGroup }) {
|
|
4
|
+
if (!token) {
|
|
5
|
+
throw new Error("Token não fornecido");
|
|
6
|
+
}
|
|
7
|
+
const jwks = getJwks(region, userPoolId);
|
|
8
|
+
const issuer = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`;
|
|
9
|
+
const { payload } = await jwtVerify(token, jwks, {
|
|
10
|
+
issuer,
|
|
11
|
+
audience,
|
|
12
|
+
});
|
|
13
|
+
if (requiredGroup) {
|
|
14
|
+
const userGroups = payload["cognito:groups"];
|
|
15
|
+
if (!userGroups || !Array.isArray(userGroups)) {
|
|
16
|
+
throw new Error("UsuÔrio não pertence ao sistema");
|
|
17
|
+
}
|
|
18
|
+
const requiredGroups = Array.isArray(requiredGroup)
|
|
19
|
+
? requiredGroup
|
|
20
|
+
: [requiredGroup];
|
|
21
|
+
// Verifica se o usuÔrio tem pelo menos um dos grupos obrigatórios
|
|
22
|
+
const hasRequiredGroup = requiredGroups.some((group) => userGroups.includes(group));
|
|
23
|
+
if (!hasRequiredGroup) {
|
|
24
|
+
throw new Error(`UsuƔrio deve ser membro de um dos seguintes sistemas: ${requiredGroups.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Mapear o payload do Cognito para o formato do GatesUser
|
|
28
|
+
const user = {
|
|
29
|
+
user_id: payload.sub,
|
|
30
|
+
email: payload.email,
|
|
31
|
+
name: payload.name,
|
|
32
|
+
role: payload["custom:general_role"],
|
|
33
|
+
exp: payload.exp,
|
|
34
|
+
iat: payload.iat,
|
|
35
|
+
};
|
|
36
|
+
return user;
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/cache/jwks-cache.ts
|
|
2
|
+
import { createRemoteJWKSet } from "jose";
|
|
3
|
+
const CACHE = {};
|
|
4
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hora
|
|
5
|
+
export function jwksUrl(region, userPoolId) {
|
|
6
|
+
return `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Retorna um RemoteJWKSet cacheado por (region + userPoolId).
|
|
10
|
+
*/
|
|
11
|
+
export function getJwks(region, userPoolId, ttlMs = DEFAULT_TTL_MS) {
|
|
12
|
+
const key = `${region}::${userPoolId}`;
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
if (CACHE[key] && now - CACHE[key].timestamp < ttlMs) {
|
|
15
|
+
return CACHE[key].jwks;
|
|
16
|
+
}
|
|
17
|
+
const url = jwksUrl(region, userPoolId);
|
|
18
|
+
const remote = createRemoteJWKSet(new URL(url));
|
|
19
|
+
CACHE[key] = { jwks: remote, timestamp: now };
|
|
20
|
+
return remote;
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/config/env.ts
|
|
2
|
+
/**
|
|
3
|
+
* Helper to load environment variables for Gates SDK
|
|
4
|
+
* The client application should load their own .env file before importing this SDK
|
|
5
|
+
*/
|
|
6
|
+
export const getEnvConfig = () => ({
|
|
7
|
+
region: process.env.GATES_REGION || "sa-east-1",
|
|
8
|
+
userPoolId: process.env.GATES_USER_POOL_ID || "",
|
|
9
|
+
audience: process.env.GATES_AUDIENCE,
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Validates that required environment variables are set
|
|
13
|
+
*/
|
|
14
|
+
export const validateEnvConfig = () => {
|
|
15
|
+
const config = getEnvConfig();
|
|
16
|
+
if (!config.userPoolId) {
|
|
17
|
+
throw new Error("GATES_USER_POOL_ID is required. Please set it in your .env file or environment variables.");
|
|
18
|
+
}
|
|
19
|
+
return config;
|
|
20
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/errors/error.ts
|
|
2
|
+
/**
|
|
3
|
+
* Base error class for authentication-related errors
|
|
4
|
+
*/
|
|
5
|
+
export class AuthenticationError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(message, code) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = "AuthenticationError";
|
|
11
|
+
Object.setPrototypeOf(this, AuthenticationError.prototype);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Error thrown when a token has expired
|
|
16
|
+
*/
|
|
17
|
+
export class TokenExpiredError extends AuthenticationError {
|
|
18
|
+
constructor(message = "Token has expired") {
|
|
19
|
+
super(message, "TOKEN_EXPIRED");
|
|
20
|
+
this.name = "TokenExpiredError";
|
|
21
|
+
Object.setPrototypeOf(this, TokenExpiredError.prototype);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Error thrown when a token is invalid
|
|
26
|
+
*/
|
|
27
|
+
export class InvalidTokenError extends AuthenticationError {
|
|
28
|
+
constructor(message = "Invalid token") {
|
|
29
|
+
super(message, "INVALID_TOKEN");
|
|
30
|
+
this.name = "InvalidTokenError";
|
|
31
|
+
Object.setPrototypeOf(this, InvalidTokenError.prototype);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Error thrown when authorization header is missing
|
|
36
|
+
*/
|
|
37
|
+
export class MissingAuthorizationError extends AuthenticationError {
|
|
38
|
+
constructor(message = "Missing Authorization header") {
|
|
39
|
+
super(message, "MISSING_AUTHORIZATION");
|
|
40
|
+
this.name = "MissingAuthorizationError";
|
|
41
|
+
Object.setPrototypeOf(this, MissingAuthorizationError.prototype);
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jwtVerify } from "jose";
|
|
2
|
+
import { getJwks } from "../cache/jwks-cache.js";
|
|
3
|
+
export class AuthService {
|
|
4
|
+
region;
|
|
5
|
+
userPoolId;
|
|
6
|
+
audience;
|
|
7
|
+
requiredGroup;
|
|
8
|
+
constructor(region, userPoolId, audience, requiredGroup) {
|
|
9
|
+
if (!region || typeof region !== "string" || region.trim().length === 0) {
|
|
10
|
+
throw new Error("Region é obrigatória e deve ser uma string vÔlida");
|
|
11
|
+
}
|
|
12
|
+
if (!userPoolId ||
|
|
13
|
+
typeof userPoolId !== "string" ||
|
|
14
|
+
userPoolId.trim().length === 0) {
|
|
15
|
+
throw new Error("UserPoolId é obrigatório e deve ser uma string vÔlida");
|
|
16
|
+
}
|
|
17
|
+
if (!audience ||
|
|
18
|
+
typeof audience !== "string" ||
|
|
19
|
+
audience.trim().length === 0) {
|
|
20
|
+
throw new Error("Audience é obrigatória e deve ser uma string vÔlida");
|
|
21
|
+
}
|
|
22
|
+
// Validar formato do userPoolId (deve seguir padrão AWS)
|
|
23
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(userPoolId)) {
|
|
24
|
+
throw new Error("UserPoolId possui formato invƔlido");
|
|
25
|
+
}
|
|
26
|
+
this.region = region;
|
|
27
|
+
this.userPoolId = userPoolId;
|
|
28
|
+
this.audience = audience;
|
|
29
|
+
this.requiredGroup = requiredGroup;
|
|
30
|
+
}
|
|
31
|
+
get issuer() {
|
|
32
|
+
return `https://cognito-idp.${this.region}.amazonaws.com/${this.userPoolId}`;
|
|
33
|
+
}
|
|
34
|
+
isMemberOf(groups = []) {
|
|
35
|
+
if (!Array.isArray(groups)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (!this.requiredGroup) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const requiredGroups = Array.isArray(this.requiredGroup)
|
|
42
|
+
? this.requiredGroup
|
|
43
|
+
: [this.requiredGroup];
|
|
44
|
+
return groups.some((g) => requiredGroups.includes(g));
|
|
45
|
+
}
|
|
46
|
+
async verifyToken(token) {
|
|
47
|
+
if (!token) {
|
|
48
|
+
throw new Error("Token não fornecido");
|
|
49
|
+
}
|
|
50
|
+
const jwks = getJwks(this.region, this.userPoolId);
|
|
51
|
+
const { payload } = await jwtVerify(token, jwks, {
|
|
52
|
+
issuer: this.issuer,
|
|
53
|
+
audience: this.audience,
|
|
54
|
+
});
|
|
55
|
+
if (this.requiredGroup) {
|
|
56
|
+
const userGroups = payload["cognito:groups"];
|
|
57
|
+
if (!userGroups || !Array.isArray(userGroups)) {
|
|
58
|
+
throw new Error("UsuÔrio não pertence ao sistema");
|
|
59
|
+
}
|
|
60
|
+
const requiredGroups = Array.isArray(this.requiredGroup)
|
|
61
|
+
? this.requiredGroup
|
|
62
|
+
: [this.requiredGroup];
|
|
63
|
+
// Verifica se o usuÔrio tem pelo menos um dos grupos obrigatórios
|
|
64
|
+
const hasRequiredGroup = requiredGroups.some((group) => userGroups.includes(group));
|
|
65
|
+
if (!hasRequiredGroup) {
|
|
66
|
+
throw new Error(`UsuƔrio deve ser membro de um dos seguintes sistemas: ${requiredGroups.join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Mapear o payload do Cognito para o formato do GatesUser
|
|
70
|
+
const user = {
|
|
71
|
+
user_id: payload.sub,
|
|
72
|
+
email: payload.email,
|
|
73
|
+
name: payload.name,
|
|
74
|
+
role: payload["custom:general_role"],
|
|
75
|
+
exp: payload.exp,
|
|
76
|
+
iat: payload.iat,
|
|
77
|
+
};
|
|
78
|
+
return user;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export class UserService {
|
|
2
|
+
baseUrl;
|
|
3
|
+
system;
|
|
4
|
+
defaultHeaders;
|
|
5
|
+
constructor(baseUrl, system) {
|
|
6
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
7
|
+
this.system = system;
|
|
8
|
+
this.defaultHeaders = {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
endpoints = {
|
|
13
|
+
all: "/get-all-profiles-from-system",
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Busca todos os usuƔrios do Cognito atravƩs do backend
|
|
17
|
+
* @param idToken Token de autenticação (ID Token do Cognito)
|
|
18
|
+
* @param options Opções de paginação e filtro
|
|
19
|
+
* @returns Lista de usuƔrios
|
|
20
|
+
*/
|
|
21
|
+
async getAllUsers(idToken
|
|
22
|
+
// options: GetAllUsersOptions = {}
|
|
23
|
+
) {
|
|
24
|
+
if (!idToken) {
|
|
25
|
+
throw new Error("ID Token is required");
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const queryParams = new URLSearchParams();
|
|
29
|
+
// if (options.page) queryParams.append("page", options.page.toString());
|
|
30
|
+
// if (options.limit) queryParams.append("limit", options.limit.toString());
|
|
31
|
+
// if (options.filter) queryParams.append("filter", options.filter);
|
|
32
|
+
// if (options.group) queryParams.append("group", options.group);
|
|
33
|
+
const url = `${this.baseUrl}/${this.endpoints.all}${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
|
|
34
|
+
const response = await fetch(url, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
...this.defaultHeaders,
|
|
38
|
+
Authorization: `Bearer ${idToken}`,
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
system_name: this.system,
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const errorText = await response.text();
|
|
46
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
47
|
+
}
|
|
48
|
+
const data = (await response.json());
|
|
49
|
+
// Adicionar total baseado no length se não vier da API
|
|
50
|
+
if (!data.total && data.profiles) {
|
|
51
|
+
data.total = data.profiles.length;
|
|
52
|
+
}
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof Error) {
|
|
57
|
+
throw new Error(`Failed to fetch users: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
throw new Error("Failed to fetch users: Unknown error");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Busca um usuĆ”rio especĆfico por ID
|
|
64
|
+
* @param idToken Token de autenticação
|
|
65
|
+
* @param userId ID do usuƔrio
|
|
66
|
+
* @returns Dados do usuƔrio
|
|
67
|
+
*/
|
|
68
|
+
async getUserById(idToken, userId) {
|
|
69
|
+
if (!idToken) {
|
|
70
|
+
throw new Error("ID Token is required");
|
|
71
|
+
}
|
|
72
|
+
if (!userId) {
|
|
73
|
+
throw new Error("User ID is required");
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(`${this.baseUrl}/users/${userId}`, {
|
|
77
|
+
method: "GET",
|
|
78
|
+
headers: {
|
|
79
|
+
...this.defaultHeaders,
|
|
80
|
+
Authorization: `Bearer ${idToken}`,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const errorText = await response.text();
|
|
85
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
86
|
+
}
|
|
87
|
+
const user = (await response.json());
|
|
88
|
+
return user;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error instanceof Error) {
|
|
92
|
+
throw new Error(`Failed to fetch user: ${error.message}`);
|
|
93
|
+
}
|
|
94
|
+
throw new Error("Failed to fetch user: Unknown error");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intelicity/gates-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Simple SDK for authenticating users with AWS Cognito JWT tokens",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./dist/index.js",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.0.0"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc && tsc-alias",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test:token": "tsx scripts/test-token.ts",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"aws",
|
|
26
|
+
"cognito",
|
|
27
|
+
"jwt",
|
|
28
|
+
"authentication"
|
|
29
|
+
],
|
|
30
|
+
"author": "Gabriel Godoy",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/inteli-city/gates-sdk.git"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.1.0",
|
|
38
|
+
"tsc-alias": "^1.8.16",
|
|
39
|
+
"tsx": "^4.20.3",
|
|
40
|
+
"typescript": "^5.9.2"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"jose": "^6.1.1"
|
|
44
|
+
}
|
|
45
|
+
}
|