@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 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,2 @@
1
+ export { createAuthMiddleware } from './middleware';
2
+ export type { ExpressAuthOptions } from './middleware';
@@ -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
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './verify';
3
+ export * from './userinfo';
4
+ export * from './nestjs';
5
+ export * from './express';