@nik2208/node-auth 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/LICENSE +21 -0
- package/README.md +636 -0
- package/dist/abstract/base-auth-strategy.abstract.d.ts +7 -0
- package/dist/abstract/base-auth-strategy.abstract.d.ts.map +1 -0
- package/dist/abstract/base-auth-strategy.abstract.js +7 -0
- package/dist/abstract/base-auth-strategy.abstract.js.map +1 -0
- package/dist/abstract/base-oauth-strategy.abstract.d.ts +26 -0
- package/dist/abstract/base-oauth-strategy.abstract.d.ts.map +1 -0
- package/dist/abstract/base-oauth-strategy.abstract.js +11 -0
- package/dist/abstract/base-oauth-strategy.abstract.js.map +1 -0
- package/dist/auth-configurator.d.ts +24 -0
- package/dist/auth-configurator.d.ts.map +1 -0
- package/dist/auth-configurator.js +42 -0
- package/dist/auth-configurator.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/auth-strategy.interface.d.ts +6 -0
- package/dist/interfaces/auth-strategy.interface.d.ts.map +1 -0
- package/dist/interfaces/auth-strategy.interface.js +3 -0
- package/dist/interfaces/auth-strategy.interface.js.map +1 -0
- package/dist/interfaces/token-store.interface.d.ts +10 -0
- package/dist/interfaces/token-store.interface.d.ts.map +1 -0
- package/dist/interfaces/token-store.interface.js +3 -0
- package/dist/interfaces/token-store.interface.js.map +1 -0
- package/dist/interfaces/user-store.interface.d.ts +23 -0
- package/dist/interfaces/user-store.interface.d.ts.map +1 -0
- package/dist/interfaces/user-store.interface.js +3 -0
- package/dist/interfaces/user-store.interface.js.map +1 -0
- package/dist/middleware/auth.middleware.d.ts +12 -0
- package/dist/middleware/auth.middleware.d.ts.map +1 -0
- package/dist/middleware/auth.middleware.js +23 -0
- package/dist/middleware/auth.middleware.js.map +1 -0
- package/dist/models/auth-config.model.d.ts +96 -0
- package/dist/models/auth-config.model.d.ts.map +1 -0
- package/dist/models/auth-config.model.js +3 -0
- package/dist/models/auth-config.model.js.map +1 -0
- package/dist/models/errors.d.ts +6 -0
- package/dist/models/errors.d.ts.map +1 -0
- package/dist/models/errors.js +13 -0
- package/dist/models/errors.js.map +1 -0
- package/dist/models/token.model.d.ts +12 -0
- package/dist/models/token.model.d.ts.map +1 -0
- package/dist/models/token.model.js +3 -0
- package/dist/models/token.model.js.map +1 -0
- package/dist/models/user.model.d.ts +18 -0
- package/dist/models/user.model.d.ts.map +1 -0
- package/dist/models/user.model.js +3 -0
- package/dist/models/user.model.js.map +1 -0
- package/dist/router/auth.router.d.ts +13 -0
- package/dist/router/auth.router.d.ts.map +1 -0
- package/dist/router/auth.router.js +329 -0
- package/dist/router/auth.router.js.map +1 -0
- package/dist/services/mailer.service.d.ts +11 -0
- package/dist/services/mailer.service.d.ts.map +1 -0
- package/dist/services/mailer.service.js +138 -0
- package/dist/services/mailer.service.js.map +1 -0
- package/dist/services/password.service.d.ts +5 -0
- package/dist/services/password.service.d.ts.map +1 -0
- package/dist/services/password.service.js +17 -0
- package/dist/services/password.service.js.map +1 -0
- package/dist/services/sms.service.d.ts +14 -0
- package/dist/services/sms.service.d.ts.map +1 -0
- package/dist/services/sms.service.js +51 -0
- package/dist/services/sms.service.js.map +1 -0
- package/dist/services/token.service.d.ts +13 -0
- package/dist/services/token.service.d.ts.map +1 -0
- package/dist/services/token.service.js +78 -0
- package/dist/services/token.service.js.map +1 -0
- package/dist/strategies/local/local.strategy.d.ts +19 -0
- package/dist/strategies/local/local.strategy.d.ts.map +1 -0
- package/dist/strategies/local/local.strategy.js +29 -0
- package/dist/strategies/local/local.strategy.js.map +1 -0
- package/dist/strategies/magic-link/magic-link.strategy.d.ts +8 -0
- package/dist/strategies/magic-link/magic-link.strategy.d.ts.map +1 -0
- package/dist/strategies/magic-link/magic-link.strategy.js +50 -0
- package/dist/strategies/magic-link/magic-link.strategy.js.map +1 -0
- package/dist/strategies/oauth/github.strategy.d.ts +29 -0
- package/dist/strategies/oauth/github.strategy.d.ts.map +1 -0
- package/dist/strategies/oauth/github.strategy.js +69 -0
- package/dist/strategies/oauth/github.strategy.js.map +1 -0
- package/dist/strategies/oauth/google.strategy.d.ts +29 -0
- package/dist/strategies/oauth/google.strategy.d.ts.map +1 -0
- package/dist/strategies/oauth/google.strategy.js +61 -0
- package/dist/strategies/oauth/google.strategy.js.map +1 -0
- package/dist/strategies/sms/sms.strategy.d.ts +7 -0
- package/dist/strategies/sms/sms.strategy.d.ts.map +1 -0
- package/dist/strategies/sms/sms.strategy.js +39 -0
- package/dist/strategies/sms/sms.strategy.js.map +1 -0
- package/dist/strategies/two-factor/totp.strategy.d.ts +12 -0
- package/dist/strategies/two-factor/totp.strategy.d.ts.map +1 -0
- package/dist/strategies/two-factor/totp.strategy.js +32 -0
- package/dist/strategies/two-factor/totp.strategy.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nik2208
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
# node-auth
|
|
2
|
+
|
|
3
|
+
A production-ready, **database-agnostic** JWT authentication library for Node.js written in TypeScript. Compatible with any Node.js framework (NestJS, Next.js, Express, Fastify, etc.) and any database through a simple interface pattern.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔐 **JWT Authentication** – Access & refresh token pair management with HttpOnly cookies
|
|
8
|
+
- 🏠 **Local Strategy** – Email/password authentication with bcrypt hashing
|
|
9
|
+
- 🔄 **OAuth 2.0** – Extensible Google & GitHub OAuth strategies
|
|
10
|
+
- 🪄 **Magic Links** – Passwordless email authentication
|
|
11
|
+
- 📱 **SMS OTP** – Phone number verification codes
|
|
12
|
+
- 🔑 **TOTP 2FA** – Time-based one-time passwords (Google Authenticator compatible)
|
|
13
|
+
- 🗃️ **Database Agnostic** – Implement a simple interface to use any database
|
|
14
|
+
- 🧩 **Strategy Pattern** – Plug in only the auth methods you need
|
|
15
|
+
- 🛡️ **Middleware** – Express-compatible JWT verification middleware
|
|
16
|
+
- 🚀 **Express Router** – Drop-in `/auth` router with all endpoints
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install node-auth
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import express from 'express';
|
|
28
|
+
import { AuthConfigurator } from 'node-auth';
|
|
29
|
+
import { myUserStore } from './my-user-store'; // Your IUserStore implementation
|
|
30
|
+
|
|
31
|
+
const app = express();
|
|
32
|
+
app.use(express.json());
|
|
33
|
+
|
|
34
|
+
const auth = new AuthConfigurator(
|
|
35
|
+
{
|
|
36
|
+
accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!,
|
|
37
|
+
refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!,
|
|
38
|
+
accessTokenExpiresIn: '15m',
|
|
39
|
+
refreshTokenExpiresIn: '7d',
|
|
40
|
+
},
|
|
41
|
+
myUserStore
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Mount the auth router at /auth
|
|
45
|
+
app.use('/auth', auth.router());
|
|
46
|
+
|
|
47
|
+
// Protect routes
|
|
48
|
+
app.get('/protected', auth.middleware(), (req, res) => {
|
|
49
|
+
res.json({ user: req.user });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
app.listen(3000);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Database Integration — Implementing IUserStore
|
|
56
|
+
|
|
57
|
+
The library is **completely database-agnostic**. The only coupling point to your database is the
|
|
58
|
+
`IUserStore` interface. Implement it once for your DB and pass the instance to `AuthConfigurator`.
|
|
59
|
+
|
|
60
|
+
### Interface contract
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { IUserStore, BaseUser } from 'node-auth';
|
|
64
|
+
|
|
65
|
+
export class MyUserStore implements IUserStore {
|
|
66
|
+
// ---- Required: core CRUD ---------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/** Find a user by email address (used for login, magic link, password reset). */
|
|
69
|
+
async findByEmail(email: string): Promise<BaseUser | null> { /* ... */ }
|
|
70
|
+
|
|
71
|
+
/** Find a user by primary key (used for token refresh, 2FA, SMS). */
|
|
72
|
+
async findById(id: string): Promise<BaseUser | null> { /* ... */ }
|
|
73
|
+
|
|
74
|
+
/** Create a new user (used by OAuth strategies when user doesn't exist yet). */
|
|
75
|
+
async create(data: Partial<BaseUser>): Promise<BaseUser> { /* ... */ }
|
|
76
|
+
|
|
77
|
+
// ---- Required: token field updates ----------------------------------------
|
|
78
|
+
|
|
79
|
+
async updateRefreshToken(userId: string, token: string | null, expiry: Date | null): Promise<void> { /* ... */ }
|
|
80
|
+
async updateResetToken(userId: string, token: string | null, expiry: Date | null): Promise<void> { /* ... */ }
|
|
81
|
+
async updatePassword(userId: string, hashedPassword: string): Promise<void> { /* ... */ }
|
|
82
|
+
async updateTotpSecret(userId: string, secret: string | null): Promise<void> { /* ... */ }
|
|
83
|
+
async updateMagicLinkToken(userId: string, token: string | null, expiry: Date | null): Promise<void> { /* ... */ }
|
|
84
|
+
async updateSmsCode(userId: string, code: string | null, expiry: Date | null): Promise<void> { /* ... */ }
|
|
85
|
+
|
|
86
|
+
// ---- Optional: token look-ups (required for specific features) ------------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Required for: POST /auth/reset-password
|
|
90
|
+
* Find a user whose `resetToken` field matches the given token.
|
|
91
|
+
*/
|
|
92
|
+
async findByResetToken(token: string): Promise<BaseUser | null> { /* ... */ }
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Required for: POST /auth/magic-link/verify
|
|
96
|
+
* Find a user whose `magicLinkToken` field matches the given token.
|
|
97
|
+
*/
|
|
98
|
+
async findByMagicLinkToken(token: string): Promise<BaseUser | null> { /* ... */ }
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Ready-to-use example implementations
|
|
103
|
+
|
|
104
|
+
The `examples/` directory contains complete implementations for the most common databases and frameworks:
|
|
105
|
+
|
|
106
|
+
| File | Description |
|
|
107
|
+
|------|-------------|
|
|
108
|
+
| `examples/in-memory-user-store.ts` | In-memory store — ideal for testing and prototyping |
|
|
109
|
+
| `examples/sqlite-user-store.example.ts` | `better-sqlite3` store — production-ready SQL example |
|
|
110
|
+
| `examples/mysql-user-store.example.ts` | `mysql2` store — MySQL / MariaDB example |
|
|
111
|
+
| `examples/mongodb-user-store.example.ts` | `mongodb` store — MongoDB example |
|
|
112
|
+
| `examples/nestjs-integration.example.ts` | NestJS module, guard, controller and DI integration |
|
|
113
|
+
| `examples/nextjs-integration.example.ts` | Next.js App Router & Pages Router integration |
|
|
114
|
+
|
|
115
|
+
Copy the relevant file(s) into your project and adapt the schema to your needs.
|
|
116
|
+
|
|
117
|
+
**In-memory store (testing/prototyping):**
|
|
118
|
+
```typescript
|
|
119
|
+
import { InMemoryUserStore } from './examples/in-memory-user-store';
|
|
120
|
+
const userStore = new InMemoryUserStore();
|
|
121
|
+
const auth = new AuthConfigurator(config, userStore);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**SQLite with `better-sqlite3`:**
|
|
125
|
+
```typescript
|
|
126
|
+
import Database from 'better-sqlite3';
|
|
127
|
+
import { SqliteUserStore } from './examples/sqlite-user-store.example';
|
|
128
|
+
|
|
129
|
+
const db = new Database('app.db');
|
|
130
|
+
db.pragma('journal_mode = WAL');
|
|
131
|
+
db.pragma('foreign_keys = ON');
|
|
132
|
+
|
|
133
|
+
const userStore = new SqliteUserStore(db); // creates the `users` table automatically
|
|
134
|
+
const auth = new AuthConfigurator(config, userStore);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**MySQL / MariaDB with `mysql2`:**
|
|
138
|
+
```typescript
|
|
139
|
+
import mysql from 'mysql2/promise';
|
|
140
|
+
import { MySqlUserStore } from './examples/mysql-user-store.example';
|
|
141
|
+
|
|
142
|
+
const pool = mysql.createPool({
|
|
143
|
+
host: process.env.DB_HOST,
|
|
144
|
+
user: process.env.DB_USER,
|
|
145
|
+
password: process.env.DB_PASSWORD,
|
|
146
|
+
database: process.env.DB_NAME,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const userStore = new MySqlUserStore(pool);
|
|
150
|
+
await userStore.init(); // creates the `users` table automatically
|
|
151
|
+
const auth = new AuthConfigurator(config, userStore);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**MongoDB with the `mongodb` driver:**
|
|
155
|
+
```typescript
|
|
156
|
+
import { MongoClient } from 'mongodb';
|
|
157
|
+
import { MongoDbUserStore } from './examples/mongodb-user-store.example';
|
|
158
|
+
|
|
159
|
+
const client = new MongoClient(process.env.MONGODB_URI!);
|
|
160
|
+
await client.connect();
|
|
161
|
+
|
|
162
|
+
const userStore = new MongoDbUserStore(client.db('myapp'));
|
|
163
|
+
await userStore.init(); // creates indexes automatically
|
|
164
|
+
const auth = new AuthConfigurator(config, userStore);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**PostgreSQL (example skeleton):**
|
|
168
|
+
```typescript
|
|
169
|
+
import { Pool } from 'pg';
|
|
170
|
+
import { IUserStore, BaseUser } from 'node-auth';
|
|
171
|
+
|
|
172
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
173
|
+
|
|
174
|
+
export class PgUserStore implements IUserStore {
|
|
175
|
+
async findByEmail(email: string) {
|
|
176
|
+
const { rows } = await pool.query('SELECT * FROM users WHERE email=$1', [email]);
|
|
177
|
+
return rows[0] ?? null;
|
|
178
|
+
}
|
|
179
|
+
async findById(id: string) {
|
|
180
|
+
const { rows } = await pool.query('SELECT * FROM users WHERE id=$1', [id]);
|
|
181
|
+
return rows[0] ?? null;
|
|
182
|
+
}
|
|
183
|
+
// ... implement remaining methods
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
## Framework Integration
|
|
189
|
+
|
|
190
|
+
### NestJS
|
|
191
|
+
|
|
192
|
+
See `examples/nestjs-integration.example.ts` for a full working example that includes:
|
|
193
|
+
|
|
194
|
+
- **`AuthModule.forRoot()`** — NestJS DynamicModule wrapping `AuthConfigurator`
|
|
195
|
+
- **`JwtAuthGuard`** — NestJS `CanActivate` guard backed by `auth.middleware()`
|
|
196
|
+
- **`@CurrentUser()`** — parameter decorator that extracts `req.user`
|
|
197
|
+
- **`AuthController`** — catch-all controller that forwards `/auth/*` traffic to `auth.router()`
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// app.module.ts
|
|
201
|
+
import { AuthModule } from './auth.module';
|
|
202
|
+
import { MyUserStore } from './my-user-store';
|
|
203
|
+
|
|
204
|
+
@Module({
|
|
205
|
+
imports: [
|
|
206
|
+
AuthModule.forRoot({
|
|
207
|
+
config: authConfig,
|
|
208
|
+
userStore: new MyUserStore(),
|
|
209
|
+
}),
|
|
210
|
+
],
|
|
211
|
+
})
|
|
212
|
+
export class AppModule {}
|
|
213
|
+
|
|
214
|
+
// Protect a route
|
|
215
|
+
@Controller('profile')
|
|
216
|
+
export class ProfileController {
|
|
217
|
+
@Get()
|
|
218
|
+
@UseGuards(JwtAuthGuard)
|
|
219
|
+
getProfile(@CurrentUser() user: BaseUser) {
|
|
220
|
+
return user;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Next.js
|
|
226
|
+
|
|
227
|
+
See `examples/nextjs-integration.example.ts` for a full working example that covers both the **App Router** (Next.js 13+) and the legacy **Pages Router**.
|
|
228
|
+
|
|
229
|
+
**Pages Router (simplest approach):**
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// pages/api/auth/[...auth].ts
|
|
233
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
234
|
+
import { getAuth } from '../../lib/auth';
|
|
235
|
+
|
|
236
|
+
export const config = { api: { bodyParser: false } };
|
|
237
|
+
|
|
238
|
+
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
239
|
+
const router = getAuth().router();
|
|
240
|
+
req.url = req.url!.replace(/^\/api\/auth/, '') || '/';
|
|
241
|
+
router(req as any, res as any, () => res.status(404).end());
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Protecting a Server Component (App Router):**
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// app/dashboard/page.tsx
|
|
249
|
+
import { cookies } from 'next/headers';
|
|
250
|
+
import { redirect } from 'next/navigation';
|
|
251
|
+
import { TokenService } from 'node-auth';
|
|
252
|
+
import { authConfig } from '../../lib/auth';
|
|
253
|
+
|
|
254
|
+
export default async function DashboardPage() {
|
|
255
|
+
const token = cookies().get('access_token')?.value;
|
|
256
|
+
if (!token) redirect('/login');
|
|
257
|
+
|
|
258
|
+
const payload = new TokenService().verifyAccessToken(token, authConfig);
|
|
259
|
+
if (!payload) redirect('/login');
|
|
260
|
+
|
|
261
|
+
return <div>Welcome, {payload.email}!</div>;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Auth Router Endpoints
|
|
266
|
+
|
|
267
|
+
When you mount `auth.router()`, the following endpoints are available:
|
|
268
|
+
|
|
269
|
+
| Method | Path | Description |
|
|
270
|
+
|--------|------|-------------|
|
|
271
|
+
| `POST` | `/auth/login` | Login with email/password |
|
|
272
|
+
| `POST` | `/auth/logout` | Logout and clear cookies |
|
|
273
|
+
| `POST` | `/auth/refresh` | Refresh access token |
|
|
274
|
+
| `GET` | `/auth/me` | Get current user (protected) |
|
|
275
|
+
| `POST` | `/auth/forgot-password` | Send password reset email |
|
|
276
|
+
| `POST` | `/auth/reset-password` | Reset password with token |
|
|
277
|
+
| `POST` | `/auth/2fa/setup` | Get TOTP secret + QR code (protected) |
|
|
278
|
+
| `POST` | `/auth/2fa/verify-setup` | Verify TOTP code and enable 2FA (protected) |
|
|
279
|
+
| `POST` | `/auth/2fa/verify` | Complete 2FA login |
|
|
280
|
+
| `POST` | `/auth/2fa/disable` | Disable 2FA (protected) |
|
|
281
|
+
| `POST` | `/auth/magic-link/send` | Send magic link email |
|
|
282
|
+
| `POST` | `/auth/magic-link/verify` | Verify magic link token |
|
|
283
|
+
| `POST` | `/auth/sms/send` | Send SMS verification code |
|
|
284
|
+
| `POST` | `/auth/sms/verify` | Verify SMS code |
|
|
285
|
+
| `GET` | `/auth/oauth/google` | Initiate Google OAuth |
|
|
286
|
+
| `GET` | `/auth/oauth/google/callback` | Google OAuth callback |
|
|
287
|
+
| `GET` | `/auth/oauth/github` | Initiate GitHub OAuth |
|
|
288
|
+
| `GET` | `/auth/oauth/github/callback` | GitHub OAuth callback |
|
|
289
|
+
|
|
290
|
+
## Configuration
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { AuthConfig } from 'node-auth';
|
|
294
|
+
|
|
295
|
+
const config: AuthConfig = {
|
|
296
|
+
// Required
|
|
297
|
+
accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!,
|
|
298
|
+
refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!,
|
|
299
|
+
|
|
300
|
+
// Token lifetimes (default: 15m / 7d)
|
|
301
|
+
accessTokenExpiresIn: '15m',
|
|
302
|
+
refreshTokenExpiresIn: '7d',
|
|
303
|
+
|
|
304
|
+
// Cookie options
|
|
305
|
+
cookieOptions: {
|
|
306
|
+
secure: true, // HTTPS only (recommended in production)
|
|
307
|
+
sameSite: 'lax',
|
|
308
|
+
domain: 'yourdomain.com',
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
// bcrypt salt rounds (default: 12)
|
|
312
|
+
bcryptSaltRounds: 12,
|
|
313
|
+
|
|
314
|
+
// Email — see "Mailer Configuration" section below
|
|
315
|
+
email: {
|
|
316
|
+
siteUrl: 'https://yourapp.com',
|
|
317
|
+
mailer: {
|
|
318
|
+
endpoint: process.env.MAILER_ENDPOINT!, // HTTP POST endpoint
|
|
319
|
+
apiKey: process.env.MAILER_API_KEY!,
|
|
320
|
+
from: 'noreply@yourapp.com',
|
|
321
|
+
fromName: 'My App',
|
|
322
|
+
defaultLang: 'en', // 'en' or 'it'
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
// SMS (for OTP verification codes)
|
|
327
|
+
sms: {
|
|
328
|
+
endpoint: 'https://sms.example.com/sendsms',
|
|
329
|
+
apiKey: process.env.SMS_API_KEY!,
|
|
330
|
+
username: process.env.SMS_USERNAME!,
|
|
331
|
+
password: process.env.SMS_PASSWORD!,
|
|
332
|
+
codeExpiresInMinutes: 10,
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
// OAuth
|
|
336
|
+
oauth: {
|
|
337
|
+
google: {
|
|
338
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
339
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
340
|
+
callbackUrl: 'https://yourapp.com/auth/oauth/google/callback',
|
|
341
|
+
},
|
|
342
|
+
github: {
|
|
343
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
344
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
345
|
+
callbackUrl: 'https://yourapp.com/auth/oauth/github/callback',
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// 2FA app name shown in authenticator apps
|
|
350
|
+
twoFactor: {
|
|
351
|
+
appName: 'My App',
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Mailer Configuration
|
|
357
|
+
|
|
358
|
+
The library ships a built-in **HTTP mailer transport** (`MailerService`) that sends transactional
|
|
359
|
+
emails (password reset, magic links, welcome) via an HTTP POST to any configurable endpoint — no
|
|
360
|
+
SMTP required. Built-in templates are available in **English** (`en`) and **Italian** (`it`).
|
|
361
|
+
|
|
362
|
+
### Option A — Built-in HTTP mailer transport (recommended)
|
|
363
|
+
|
|
364
|
+
Configure `email.mailer` in `AuthConfig`. The library will automatically send emails using the
|
|
365
|
+
built-in templates whenever a reset link or magic link needs to go out.
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { AuthConfig, MailerConfig } from 'node-auth';
|
|
369
|
+
|
|
370
|
+
const config: AuthConfig = {
|
|
371
|
+
// ...jwt secrets, cookies...
|
|
372
|
+
email: {
|
|
373
|
+
siteUrl: 'https://yourapp.com',
|
|
374
|
+
mailer: {
|
|
375
|
+
/** Full URL of your mailer API endpoint. Receives a JSON POST. */
|
|
376
|
+
endpoint: process.env.MAILER_ENDPOINT!, // e.g. 'https://api.mailgun.net/v3/...'
|
|
377
|
+
/** API key sent as the X-API-Key request header. */
|
|
378
|
+
apiKey: process.env.MAILER_API_KEY!,
|
|
379
|
+
/** Sender address. */
|
|
380
|
+
from: 'noreply@yourapp.com',
|
|
381
|
+
/** Sender display name (optional). */
|
|
382
|
+
fromName: 'My App',
|
|
383
|
+
/**
|
|
384
|
+
* Default language for built-in templates.
|
|
385
|
+
* Supported: 'en' (default) | 'it'
|
|
386
|
+
* Can be overridden per-request by passing emailLang in the request body.
|
|
387
|
+
*/
|
|
388
|
+
defaultLang: 'en',
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The mailer sends a **POST** request to `endpoint` with the following JSON body:
|
|
395
|
+
|
|
396
|
+
```json
|
|
397
|
+
{
|
|
398
|
+
"to": "user@example.com",
|
|
399
|
+
"from": "noreply@yourapp.com",
|
|
400
|
+
"fromName": "My App",
|
|
401
|
+
"subject": "Reset your password",
|
|
402
|
+
"html": "<p>Click the link...</p>",
|
|
403
|
+
"text": "Click the link..."
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
and the header `X-API-Key: <apiKey>`.
|
|
408
|
+
|
|
409
|
+
Your mailer API (Mailgun, Resend, SendGrid, a custom proxy, etc.) only needs to accept this JSON
|
|
410
|
+
shape and forward it to the email provider. The content-type is `application/json`.
|
|
411
|
+
|
|
412
|
+
#### Per-request language override
|
|
413
|
+
|
|
414
|
+
For `POST /auth/forgot-password` and `POST /auth/magic-link/send`, pass `emailLang` in the request
|
|
415
|
+
body to override `defaultLang` for a single request:
|
|
416
|
+
|
|
417
|
+
```json
|
|
418
|
+
{ "email": "user@example.com", "emailLang": "it" }
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### Using `MailerService` directly
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { MailerService } from 'node-auth';
|
|
425
|
+
|
|
426
|
+
const mailer = new MailerService({
|
|
427
|
+
endpoint: 'https://mailer.example.com/send',
|
|
428
|
+
apiKey: 'key-xxx',
|
|
429
|
+
from: 'noreply@example.com',
|
|
430
|
+
defaultLang: 'it',
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
await mailer.sendPasswordReset(to, token, resetLink, 'it');
|
|
434
|
+
await mailer.sendMagicLink(to, token, magicLink);
|
|
435
|
+
await mailer.sendWelcome(to, { loginUrl: 'https://yourapp.com/login', tempPassword: 'Temp@123' }, 'it');
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Option B — Custom callbacks
|
|
439
|
+
|
|
440
|
+
If you prefer full control, provide callback functions instead of (or in addition to) `mailer`.
|
|
441
|
+
**Callbacks always take precedence over the `mailer` transport.**
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
email: {
|
|
445
|
+
siteUrl: 'https://yourapp.com',
|
|
446
|
+
sendPasswordReset: async (to, token, link, lang) => {
|
|
447
|
+
await myEmailClient.send({ to, subject: 'Reset your password', html: `...${link}...` });
|
|
448
|
+
},
|
|
449
|
+
sendMagicLink: async (to, token, link, lang) => {
|
|
450
|
+
await myEmailClient.send({ to, subject: 'Your sign-in link', html: `...${link}...` });
|
|
451
|
+
},
|
|
452
|
+
sendWelcome: async (to, data, lang) => {
|
|
453
|
+
await myEmailClient.send({ to, subject: 'Welcome!', html: `...${data.loginUrl}...` });
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
## OAuth Strategies
|
|
460
|
+
|
|
461
|
+
OAuth strategies are abstract—extend them to implement your own user lookup logic:
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
import { GoogleStrategy, BaseUser, AuthConfig } from 'node-auth';
|
|
465
|
+
|
|
466
|
+
class MyGoogleStrategy extends GoogleStrategy<BaseUser> {
|
|
467
|
+
constructor(config: AuthConfig, private userStore: MyUserStore) {
|
|
468
|
+
super(config);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async findOrCreateUser(profile: { id: string; email: string; name?: string; picture?: string }) {
|
|
472
|
+
let user = await this.userStore.findByEmail(profile.email);
|
|
473
|
+
if (!user) {
|
|
474
|
+
user = await this.userStore.create({ email: profile.email, role: 'user' });
|
|
475
|
+
}
|
|
476
|
+
return user;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Pass to router
|
|
481
|
+
app.use('/auth', auth.router({
|
|
482
|
+
googleStrategy: new MyGoogleStrategy(config, userStore),
|
|
483
|
+
githubStrategy: new MyGithubStrategy(config, userStore),
|
|
484
|
+
}));
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Using Services Directly
|
|
488
|
+
|
|
489
|
+
Access the underlying services for custom flows:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const auth = new AuthConfigurator(config, userStore);
|
|
493
|
+
|
|
494
|
+
// Hash passwords
|
|
495
|
+
const hash = await auth.passwordService.hash('mypassword');
|
|
496
|
+
const valid = await auth.passwordService.compare('mypassword', hash);
|
|
497
|
+
|
|
498
|
+
// Generate/verify tokens
|
|
499
|
+
const tokens = auth.tokenService.generateTokenPair({ sub: userId, email }, config);
|
|
500
|
+
const payload = auth.tokenService.verifyAccessToken(token, config);
|
|
501
|
+
|
|
502
|
+
// Get a local strategy instance
|
|
503
|
+
const localStrategy = auth.strategy('local');
|
|
504
|
+
const user = await localStrategy.authenticate({ email, password }, config);
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## Using Strategies Independently
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
import {
|
|
511
|
+
MagicLinkStrategy,
|
|
512
|
+
SmsStrategy,
|
|
513
|
+
TotpStrategy,
|
|
514
|
+
LocalStrategy,
|
|
515
|
+
PasswordService,
|
|
516
|
+
} from 'node-auth';
|
|
517
|
+
|
|
518
|
+
// Magic Links
|
|
519
|
+
const magicLink = new MagicLinkStrategy();
|
|
520
|
+
await magicLink.sendMagicLink(email, userStore, config);
|
|
521
|
+
const user = await magicLink.verify(token, userStore);
|
|
522
|
+
|
|
523
|
+
// SMS OTP
|
|
524
|
+
const sms = new SmsStrategy();
|
|
525
|
+
await sms.sendCode(phone, userId, userStore, config);
|
|
526
|
+
const valid = await sms.verify(userId, code, userStore);
|
|
527
|
+
|
|
528
|
+
// TOTP 2FA
|
|
529
|
+
const totpStrategy = new TotpStrategy();
|
|
530
|
+
const { secret, otpauthUrl, qrCode } = totpStrategy.generateSecret(email, 'MyApp');
|
|
531
|
+
const qrDataUrl = await qrCode; // data:image/png;base64,...
|
|
532
|
+
const isValid = await totpStrategy.verify(token, secret);
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## BaseUser Model
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
interface BaseUser {
|
|
539
|
+
id: string;
|
|
540
|
+
email: string;
|
|
541
|
+
password?: string;
|
|
542
|
+
role?: string;
|
|
543
|
+
refreshToken?: string | null;
|
|
544
|
+
refreshTokenExpiry?: Date | null;
|
|
545
|
+
resetToken?: string | null;
|
|
546
|
+
resetTokenExpiry?: Date | null;
|
|
547
|
+
totpSecret?: string | null;
|
|
548
|
+
isTotpEnabled?: boolean;
|
|
549
|
+
magicLinkToken?: string | null;
|
|
550
|
+
magicLinkTokenExpiry?: Date | null;
|
|
551
|
+
smsCode?: string | null;
|
|
552
|
+
smsCodeExpiry?: Date | null;
|
|
553
|
+
phoneNumber?: string | null;
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## Error Handling
|
|
558
|
+
|
|
559
|
+
The library throws `AuthError` for authentication failures:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
import { AuthError } from 'node-auth';
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
await localStrategy.authenticate({ email, password }, config);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
if (err instanceof AuthError) {
|
|
568
|
+
console.log(err.code); // e.g. 'INVALID_CREDENTIALS'
|
|
569
|
+
console.log(err.statusCode); // e.g. 401
|
|
570
|
+
console.log(err.message); // e.g. 'Invalid credentials'
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
## Custom Strategies
|
|
576
|
+
|
|
577
|
+
Extend `BaseAuthStrategy` to create custom authentication strategies:
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
import { BaseAuthStrategy, AuthConfig } from 'node-auth';
|
|
581
|
+
|
|
582
|
+
class ApiKeyStrategy extends BaseAuthStrategy<{ apiKey: string }, MyUser> {
|
|
583
|
+
name = 'api-key';
|
|
584
|
+
|
|
585
|
+
async authenticate(input: { apiKey: string }, config: AuthConfig): Promise<MyUser> {
|
|
586
|
+
const user = await myStore.findByApiKey(input.apiKey);
|
|
587
|
+
if (!user) throw new AuthError('Invalid API key', 'INVALID_API_KEY', 401);
|
|
588
|
+
return user;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
## Rate Limiting
|
|
594
|
+
|
|
595
|
+
Auth routes should be rate-limited in production to prevent brute-force attacks. Pass an optional `rateLimiter` middleware to `createAuthRouter()`:
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
import rateLimit from 'express-rate-limit';
|
|
599
|
+
|
|
600
|
+
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20 });
|
|
601
|
+
|
|
602
|
+
app.use('/auth', auth.router({ rateLimiter: limiter }));
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
All sensitive endpoints (login, refresh, password reset, 2FA, magic links, SMS) will be protected.
|
|
606
|
+
|
|
607
|
+
## Building
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
npm run build
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
## Testing
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
npm test
|
|
617
|
+
npm run test:coverage
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## Architecture
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
src/
|
|
624
|
+
├── interfaces/ # IUserStore, ITokenStore, IAuthStrategy
|
|
625
|
+
├── models/ # BaseUser, TokenPair, AuthConfig, AuthError
|
|
626
|
+
├── abstract/ # BaseAuthStrategy, BaseOAuthStrategy
|
|
627
|
+
├── strategies/ # Local, Google, GitHub, MagicLink, SMS, TOTP
|
|
628
|
+
├── services/ # TokenService, PasswordService, SmsService
|
|
629
|
+
├── middleware/ # createAuthMiddleware()
|
|
630
|
+
├── router/ # createAuthRouter() – full Express router
|
|
631
|
+
└── auth-configurator.ts # Main entry point
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## License
|
|
635
|
+
|
|
636
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AuthConfig } from '../models/auth-config.model';
|
|
2
|
+
import { BaseUser } from '../models/user.model';
|
|
3
|
+
export declare abstract class BaseAuthStrategy<TInput = unknown, TUser = BaseUser> {
|
|
4
|
+
abstract name: string;
|
|
5
|
+
abstract authenticate(input: TInput, config: AuthConfig): Promise<TUser>;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=base-auth-strategy.abstract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-auth-strategy.abstract.d.ts","sourceRoot":"","sources":["../../src/abstract/base-auth-strategy.abstract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEhD,8BAAsB,gBAAgB,CAAC,MAAM,GAAG,OAAO,EAAE,KAAK,GAAG,QAAQ;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC;CACzE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-auth-strategy.abstract.js","sourceRoot":"","sources":["../../src/abstract/base-auth-strategy.abstract.ts"],"names":[],"mappings":";;;AAGA,MAAsB,gBAAgB;CAGrC;AAHD,4CAGC"}
|