@tekcify/auth-backend 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +495 -0
- package/package.json +66 -0
- package/src/__tests__/verify.test.ts +80 -0
- package/src/express/index.ts +2 -0
- package/src/express/middleware.ts +61 -0
- package/src/index.ts +5 -0
- package/src/nestjs/decorator.ts +12 -0
- package/src/nestjs/guard.ts +57 -0
- package/src/nestjs/index.ts +4 -0
- package/src/types.ts +24 -0
- package/src/userinfo.ts +26 -0
- package/src/verify.ts +36 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# @tekcify/auth-backend
|
|
2
|
+
|
|
3
|
+
Backend authentication helpers for Tekcify Auth. Provides middleware, guards, and utilities for validating JWT tokens and protecting API routes in NestJS and Express applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tekcify/auth-backend
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @tekcify/auth-backend
|
|
11
|
+
# or
|
|
12
|
+
yarn add @tekcify/auth-backend
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- ✅ **NestJS Support** - Guards and decorators for NestJS applications
|
|
18
|
+
- ✅ **Express Support** - Middleware for Express applications
|
|
19
|
+
- ✅ **Token Verification** - JWT token validation with HMAC/RS256
|
|
20
|
+
- ✅ **User Info Fetching** - Helper to get user information from auth server
|
|
21
|
+
- ✅ **TypeScript Support** - Full type definitions included
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Prerequisites
|
|
26
|
+
|
|
27
|
+
You need the JWT access secret from your Tekcify Auth server. This should match the `JWT_ACCESS_SECRET` environment variable used by the auth server.
|
|
28
|
+
|
|
29
|
+
```env
|
|
30
|
+
JWT_ACCESS_SECRET=your-jwt-access-secret-here
|
|
31
|
+
AUTH_SERVER_URL=http://localhost:7001
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## NestJS Integration
|
|
35
|
+
|
|
36
|
+
### Step 1: Install Dependencies
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add @tekcify/auth-backend @nestjs/common @nestjs/core
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Step 2: Create and Configure the Guard
|
|
43
|
+
|
|
44
|
+
Create a guard provider in your module:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { Module } from '@nestjs/common';
|
|
48
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
49
|
+
import { JwtAuthGuard } from '@tekcify/auth-backend/nestjs';
|
|
50
|
+
|
|
51
|
+
@Module({
|
|
52
|
+
providers: [
|
|
53
|
+
{
|
|
54
|
+
provide: APP_GUARD,
|
|
55
|
+
useFactory: () => {
|
|
56
|
+
return new JwtAuthGuard({
|
|
57
|
+
secret: process.env.JWT_ACCESS_SECRET!,
|
|
58
|
+
issuer: 'tekcify-auth',
|
|
59
|
+
audience: 'tekcify-api',
|
|
60
|
+
getUserInfo: async (userId: string) => {
|
|
61
|
+
// Optional: Fetch user info from your database
|
|
62
|
+
// This is called only if getUserInfo is provided
|
|
63
|
+
const user = await userRepository.findById(userId);
|
|
64
|
+
return user ? { email: user.email } : null;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
})
|
|
71
|
+
export class AppModule {}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Step 3: Use the Guard in Controllers
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { Controller, Get, UseGuards } from '@nestjs/common';
|
|
78
|
+
import { JwtAuthGuard, CurrentUser } from '@tekcify/auth-backend/nestjs';
|
|
79
|
+
import type { UserPayload } from '@tekcify/auth-backend/nestjs';
|
|
80
|
+
|
|
81
|
+
@Controller('api')
|
|
82
|
+
@UseGuards(JwtAuthGuard) // Protect entire controller
|
|
83
|
+
export class ApiController {
|
|
84
|
+
@Get('profile')
|
|
85
|
+
getProfile(@CurrentUser() user: UserPayload) {
|
|
86
|
+
return {
|
|
87
|
+
userId: user.userId,
|
|
88
|
+
email: user.email,
|
|
89
|
+
scopes: user.scopes,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Get('public')
|
|
94
|
+
// This route is still protected by the controller-level guard
|
|
95
|
+
getPublic() {
|
|
96
|
+
return { message: 'Public endpoint' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Step 4: Per-Route Guard Usage
|
|
102
|
+
|
|
103
|
+
You can also use the guard on individual routes:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { Controller, Get } from '@nestjs/common';
|
|
107
|
+
import { JwtAuthGuard, CurrentUser } from '@tekcify/auth-backend/nestjs';
|
|
108
|
+
|
|
109
|
+
@Controller('api')
|
|
110
|
+
export class ApiController {
|
|
111
|
+
@Get('public')
|
|
112
|
+
getPublic() {
|
|
113
|
+
return { message: 'Public endpoint' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Get('protected')
|
|
117
|
+
@UseGuards(new JwtAuthGuard({
|
|
118
|
+
secret: process.env.JWT_ACCESS_SECRET!,
|
|
119
|
+
issuer: 'tekcify-auth',
|
|
120
|
+
audience: 'tekcify-api',
|
|
121
|
+
}))
|
|
122
|
+
getProtected(@CurrentUser() user: UserPayload) {
|
|
123
|
+
return {
|
|
124
|
+
message: 'Protected endpoint',
|
|
125
|
+
user: user.userId,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Step 5: Access User Information
|
|
132
|
+
|
|
133
|
+
The `@CurrentUser()` decorator provides access to the authenticated user:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
@Get('me')
|
|
137
|
+
@UseGuards(JwtAuthGuard)
|
|
138
|
+
getCurrentUser(@CurrentUser() user: UserPayload) {
|
|
139
|
+
return {
|
|
140
|
+
userId: user.userId,
|
|
141
|
+
email: user.email,
|
|
142
|
+
scopes: user.scopes || [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Express Integration
|
|
148
|
+
|
|
149
|
+
### Step 1: Install Dependencies
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pnpm add @tekcify/auth-backend express
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Step 2: Create Auth Middleware
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import express from 'express';
|
|
159
|
+
import { createAuthMiddleware } from '@tekcify/auth-backend/express';
|
|
160
|
+
|
|
161
|
+
const app = express();
|
|
162
|
+
|
|
163
|
+
const authMiddleware = createAuthMiddleware({
|
|
164
|
+
secret: process.env.JWT_ACCESS_SECRET!,
|
|
165
|
+
issuer: 'tekcify-auth',
|
|
166
|
+
audience: 'tekcify-api',
|
|
167
|
+
getUserInfo: async (userId: string) => {
|
|
168
|
+
// Optional: Fetch user info from your database
|
|
169
|
+
const user = await userRepository.findById(userId);
|
|
170
|
+
return user ? { email: user.email } : null;
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
app.use(express.json());
|
|
175
|
+
|
|
176
|
+
// Apply middleware to all /api routes
|
|
177
|
+
app.use('/api', authMiddleware);
|
|
178
|
+
|
|
179
|
+
app.get('/api/profile', (req, res) => {
|
|
180
|
+
// req.user is now available
|
|
181
|
+
res.json({
|
|
182
|
+
userId: req.user!.userId,
|
|
183
|
+
email: req.user!.email,
|
|
184
|
+
scopes: req.user!.scopes,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Step 3: Route-Level Middleware
|
|
190
|
+
|
|
191
|
+
You can also apply middleware to specific routes:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
app.get('/api/public', (req, res) => {
|
|
195
|
+
res.json({ message: 'Public endpoint' });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
app.get('/api/protected', authMiddleware, (req, res) => {
|
|
199
|
+
res.json({
|
|
200
|
+
message: 'Protected endpoint',
|
|
201
|
+
user: req.user!.userId,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Step 4: TypeScript Support
|
|
207
|
+
|
|
208
|
+
The middleware extends Express's `Request` type:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import type { Request, Response } from 'express';
|
|
212
|
+
|
|
213
|
+
app.get('/api/user', authMiddleware, (req: Request, res: Response) => {
|
|
214
|
+
// TypeScript knows req.user exists
|
|
215
|
+
const userId = req.user!.userId;
|
|
216
|
+
const email = req.user!.email;
|
|
217
|
+
|
|
218
|
+
res.json({ userId, email });
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Token Verification
|
|
223
|
+
|
|
224
|
+
For cases where you need to verify tokens directly (e.g., in background jobs, WebSocket connections):
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { verifyAccessToken } from '@tekcify/auth-backend';
|
|
228
|
+
|
|
229
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
230
|
+
|
|
231
|
+
if (!token) {
|
|
232
|
+
throw new Error('No token provided');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const result = verifyAccessToken(token, {
|
|
236
|
+
secret: process.env.JWT_ACCESS_SECRET!,
|
|
237
|
+
issuer: 'tekcify-auth',
|
|
238
|
+
audience: 'tekcify-api',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!result.valid) {
|
|
242
|
+
throw new Error('Invalid token');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log('User ID:', result.payload.sub);
|
|
246
|
+
console.log('Scopes:', result.payload.scopes);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Token Introspection
|
|
250
|
+
|
|
251
|
+
For cases where you can't verify tokens directly (e.g., different signing keys, remote verification):
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { introspectToken } from '@tekcify/auth-core-client';
|
|
255
|
+
|
|
256
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
257
|
+
|
|
258
|
+
const result = await introspectToken(process.env.AUTH_SERVER_URL!, {
|
|
259
|
+
token: token!,
|
|
260
|
+
clientId: process.env.CLIENT_ID,
|
|
261
|
+
clientSecret: process.env.CLIENT_SECRET,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (result.active) {
|
|
265
|
+
console.log('Token is valid');
|
|
266
|
+
console.log('User ID:', result.sub);
|
|
267
|
+
console.log('Scopes:', result.scope);
|
|
268
|
+
} else {
|
|
269
|
+
throw new Error('Token is invalid or expired');
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Getting User Information
|
|
274
|
+
|
|
275
|
+
Fetch user information from the auth server:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import { fetchUserInfo } from '@tekcify/auth-backend';
|
|
279
|
+
|
|
280
|
+
const userInfo = await fetchUserInfo(
|
|
281
|
+
process.env.AUTH_SERVER_URL!,
|
|
282
|
+
accessToken
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
console.log('Email:', userInfo.email);
|
|
286
|
+
console.log('Name:', userInfo.name);
|
|
287
|
+
console.log('Verified:', userInfo.email_verified);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Complete NestJS Example
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { Module, Controller, Get, UseGuards } from '@nestjs/common';
|
|
294
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
295
|
+
import { JwtAuthGuard, CurrentUser } from '@tekcify/auth-backend/nestjs';
|
|
296
|
+
import type { UserPayload } from '@tekcify/auth-backend/nestjs';
|
|
297
|
+
|
|
298
|
+
@Module({
|
|
299
|
+
providers: [
|
|
300
|
+
{
|
|
301
|
+
provide: APP_GUARD,
|
|
302
|
+
useFactory: () => {
|
|
303
|
+
return new JwtAuthGuard({
|
|
304
|
+
secret: process.env.JWT_ACCESS_SECRET!,
|
|
305
|
+
issuer: 'tekcify-auth',
|
|
306
|
+
audience: 'tekcify-api',
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
})
|
|
312
|
+
export class AppModule {}
|
|
313
|
+
|
|
314
|
+
@Controller('api')
|
|
315
|
+
@UseGuards(JwtAuthGuard)
|
|
316
|
+
export class ApiController {
|
|
317
|
+
@Get('profile')
|
|
318
|
+
getProfile(@CurrentUser() user: UserPayload) {
|
|
319
|
+
return {
|
|
320
|
+
userId: user.userId,
|
|
321
|
+
email: user.email,
|
|
322
|
+
scopes: user.scopes || [],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@Get('posts')
|
|
327
|
+
async getPosts(@CurrentUser() user: UserPayload) {
|
|
328
|
+
// Only return posts for the authenticated user
|
|
329
|
+
return await postRepository.findByUserId(user.userId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Complete Express Example
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import express from 'express';
|
|
338
|
+
import { createAuthMiddleware } from '@tekcify/auth-backend/express';
|
|
339
|
+
|
|
340
|
+
const app = express();
|
|
341
|
+
app.use(express.json());
|
|
342
|
+
|
|
343
|
+
const authMiddleware = createAuthMiddleware({
|
|
344
|
+
secret: process.env.JWT_ACCESS_SECRET!,
|
|
345
|
+
issuer: 'tekcify-auth',
|
|
346
|
+
audience: 'tekcify-api',
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Public routes
|
|
350
|
+
app.get('/health', (req, res) => {
|
|
351
|
+
res.json({ status: 'ok' });
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Protected routes
|
|
355
|
+
app.use('/api', authMiddleware);
|
|
356
|
+
|
|
357
|
+
app.get('/api/profile', (req, res) => {
|
|
358
|
+
res.json({
|
|
359
|
+
userId: req.user!.userId,
|
|
360
|
+
email: req.user!.email,
|
|
361
|
+
scopes: req.user!.scopes,
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
app.get('/api/data', async (req, res) => {
|
|
366
|
+
const data = await fetchDataForUser(req.user!.userId);
|
|
367
|
+
res.json(data);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
app.listen(3000, () => {
|
|
371
|
+
console.log('Server running on port 3000');
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## API Reference
|
|
376
|
+
|
|
377
|
+
### NestJS
|
|
378
|
+
|
|
379
|
+
#### `JwtAuthGuard`
|
|
380
|
+
|
|
381
|
+
Guard class for protecting routes.
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
new JwtAuthGuard({
|
|
385
|
+
secret: string; // JWT secret
|
|
386
|
+
issuer?: string; // Token issuer (default: 'tekcify-auth')
|
|
387
|
+
audience?: string; // Token audience (default: 'tekcify-api')
|
|
388
|
+
getUserInfo?: (userId: string) => Promise<{ email: string } | null>;
|
|
389
|
+
})
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
#### `@CurrentUser()`
|
|
393
|
+
|
|
394
|
+
Parameter decorator to inject the current user.
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
@CurrentUser() user: UserPayload
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Express
|
|
401
|
+
|
|
402
|
+
#### `createAuthMiddleware(options)`
|
|
403
|
+
|
|
404
|
+
Creates Express middleware for authentication.
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
createAuthMiddleware({
|
|
408
|
+
secret: string;
|
|
409
|
+
issuer?: string;
|
|
410
|
+
audience?: string;
|
|
411
|
+
getUserInfo?: (userId: string) => Promise<{ email: string } | null>;
|
|
412
|
+
})
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Utilities
|
|
416
|
+
|
|
417
|
+
#### `verifyAccessToken(token, options)`
|
|
418
|
+
|
|
419
|
+
Verifies a JWT access token.
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
verifyAccessToken(token: string, {
|
|
423
|
+
secret: string;
|
|
424
|
+
issuer?: string;
|
|
425
|
+
audience?: string;
|
|
426
|
+
}): VerifiedToken
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### `fetchUserInfo(authServerUrl, accessToken)`
|
|
430
|
+
|
|
431
|
+
Fetches user information from the auth server.
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
fetchUserInfo(
|
|
435
|
+
authServerUrl: string,
|
|
436
|
+
accessToken: string
|
|
437
|
+
): Promise<UserInfo>
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Types
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
interface UserPayload {
|
|
444
|
+
userId: string;
|
|
445
|
+
email: string;
|
|
446
|
+
scopes?: string[];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
interface VerifiedToken {
|
|
450
|
+
payload: TokenPayload;
|
|
451
|
+
valid: boolean;
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Error Handling
|
|
456
|
+
|
|
457
|
+
All middleware and guards throw `UnauthorizedException` (NestJS) or return 401 status (Express) when:
|
|
458
|
+
|
|
459
|
+
- Token is missing
|
|
460
|
+
- Token is invalid
|
|
461
|
+
- Token is expired
|
|
462
|
+
- User not found (if `getUserInfo` is provided)
|
|
463
|
+
|
|
464
|
+
## Security Best Practices
|
|
465
|
+
|
|
466
|
+
1. **Never expose JWT secrets** - Keep secrets in environment variables
|
|
467
|
+
2. **Use HTTPS in production** - Always use secure connections
|
|
468
|
+
3. **Validate token issuer and audience** - Prevents token reuse across services
|
|
469
|
+
4. **Implement rate limiting** - Protect against brute force attacks
|
|
470
|
+
5. **Log authentication failures** - Monitor for suspicious activity
|
|
471
|
+
6. **Use short-lived tokens** - Refresh tokens regularly
|
|
472
|
+
|
|
473
|
+
## Troubleshooting
|
|
474
|
+
|
|
475
|
+
### "Invalid token" errors
|
|
476
|
+
|
|
477
|
+
- Verify `JWT_ACCESS_SECRET` matches the auth server
|
|
478
|
+
- Check token hasn't expired
|
|
479
|
+
- Ensure issuer and audience match
|
|
480
|
+
|
|
481
|
+
### "User not found" errors
|
|
482
|
+
|
|
483
|
+
- Verify `getUserInfo` function returns correct format
|
|
484
|
+
- Check database connection
|
|
485
|
+
- Ensure user exists in your system
|
|
486
|
+
|
|
487
|
+
### Token verification fails
|
|
488
|
+
|
|
489
|
+
- Verify token format (should start with "Bearer ")
|
|
490
|
+
- Check token hasn't been tampered with
|
|
491
|
+
- Ensure token type is "access" (not "refresh")
|
|
492
|
+
|
|
493
|
+
## License
|
|
494
|
+
|
|
495
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tekcify/auth-backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Backend authentication helpers for Tekcify Auth. Provides middleware, guards, and utilities for validating JWT tokens and protecting API routes in NestJS and Express applications.",
|
|
5
|
+
"author": "Tekcify",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/tekcify/auth.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/tekcify/auth/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/tekcify/auth#readme",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"require": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./nestjs": {
|
|
23
|
+
"types": "./dist/nestjs/index.d.ts",
|
|
24
|
+
"import": "./dist/nestjs/index.js",
|
|
25
|
+
"require": "./dist/nestjs/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./express": {
|
|
28
|
+
"types": "./dist/express/index.d.ts",
|
|
29
|
+
"import": "./dist/express/index.js",
|
|
30
|
+
"require": "./dist/express/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"clean": "rm -rf dist",
|
|
36
|
+
"lint": "eslint \"src/**/*.ts\" --max-warnings=0",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"nestjs",
|
|
42
|
+
"express",
|
|
43
|
+
"oauth",
|
|
44
|
+
"jwt",
|
|
45
|
+
"auth"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@tekcify/auth-core-client": "^1.0.0",
|
|
50
|
+
"jsonwebtoken": "^9.0.2"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@nestjs/common": "^11.0.1",
|
|
54
|
+
"@nestjs/core": "^11.0.1",
|
|
55
|
+
"@types/express": "^5.0.0",
|
|
56
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
57
|
+
"@types/node": "^22.10.7",
|
|
58
|
+
"typescript": "^5.7.3",
|
|
59
|
+
"vitest": "^4.0.15"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@nestjs/common": "^11.0.0",
|
|
63
|
+
"@nestjs/core": "^11.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import { verifyAccessToken } from '../verify';
|
|
4
|
+
|
|
5
|
+
describe('verifyAccessToken', () => {
|
|
6
|
+
const secret = 'test-secret';
|
|
7
|
+
const issuer = 'tekcify-auth';
|
|
8
|
+
const audience = 'tekcify-api';
|
|
9
|
+
|
|
10
|
+
it('should verify a valid access token', () => {
|
|
11
|
+
const payload = {
|
|
12
|
+
sub: 'user-123',
|
|
13
|
+
type: 'access',
|
|
14
|
+
scopes: ['read:profile'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const token = jwt.sign(payload, secret, {
|
|
18
|
+
issuer,
|
|
19
|
+
audience,
|
|
20
|
+
expiresIn: '1h',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = verifyAccessToken(token, { secret, issuer, audience });
|
|
24
|
+
|
|
25
|
+
expect(result.valid).toBe(true);
|
|
26
|
+
expect(result.payload.sub).toBe('user-123');
|
|
27
|
+
expect(result.payload.type).toBe('access');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should reject a refresh token', () => {
|
|
31
|
+
const payload = {
|
|
32
|
+
sub: 'user-123',
|
|
33
|
+
type: 'refresh',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const token = jwt.sign(payload, secret, {
|
|
37
|
+
issuer,
|
|
38
|
+
audience,
|
|
39
|
+
expiresIn: '7d',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const result = verifyAccessToken(token, { secret, issuer, audience });
|
|
43
|
+
|
|
44
|
+
expect(result.valid).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should reject an expired token', () => {
|
|
48
|
+
const payload = {
|
|
49
|
+
sub: 'user-123',
|
|
50
|
+
type: 'access',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const token = jwt.sign(payload, secret, {
|
|
54
|
+
issuer,
|
|
55
|
+
audience,
|
|
56
|
+
expiresIn: '-1h',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = verifyAccessToken(token, { secret, issuer, audience });
|
|
60
|
+
|
|
61
|
+
expect(result.valid).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should reject a token with wrong secret', () => {
|
|
65
|
+
const payload = {
|
|
66
|
+
sub: 'user-123',
|
|
67
|
+
type: 'access',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const token = jwt.sign(payload, 'wrong-secret', {
|
|
71
|
+
issuer,
|
|
72
|
+
audience,
|
|
73
|
+
expiresIn: '1h',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const result = verifyAccessToken(token, { secret, issuer, audience });
|
|
77
|
+
|
|
78
|
+
expect(result.valid).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { verifyAccessToken } from '../verify';
|
|
3
|
+
import type { VerifyTokenOptions, UserPayload } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface ExpressAuthOptions extends VerifyTokenOptions {
|
|
6
|
+
getUserInfo?: (userId: string) => Promise<{ email: string } | null>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
11
|
+
namespace Express {
|
|
12
|
+
interface Request {
|
|
13
|
+
user?: UserPayload;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createAuthMiddleware(options: ExpressAuthOptions) {
|
|
19
|
+
return async (
|
|
20
|
+
req: Request,
|
|
21
|
+
res: Response,
|
|
22
|
+
next: NextFunction,
|
|
23
|
+
): Promise<void> => {
|
|
24
|
+
const authHeader = req.headers.authorization as string | undefined;
|
|
25
|
+
|
|
26
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
27
|
+
res
|
|
28
|
+
.status(401)
|
|
29
|
+
.json({ message: 'Missing or invalid authorization header' });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const token = authHeader.substring(7);
|
|
34
|
+
const verified = verifyAccessToken(token, options);
|
|
35
|
+
|
|
36
|
+
if (!verified.valid) {
|
|
37
|
+
res.status(401).json({ message: 'Invalid or expired token' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let email = '';
|
|
42
|
+
if (options.getUserInfo) {
|
|
43
|
+
const userInfo = await options.getUserInfo(verified.payload.sub);
|
|
44
|
+
if (!userInfo) {
|
|
45
|
+
res.status(401).json({ message: 'User not found' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
email = userInfo.email;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
req.user = {
|
|
52
|
+
userId: verified.payload.sub,
|
|
53
|
+
email,
|
|
54
|
+
scopes: Array.isArray(verified.payload.scopes)
|
|
55
|
+
? verified.payload.scopes
|
|
56
|
+
: [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
next();
|
|
60
|
+
};
|
|
61
|
+
}
|