@nik2208/node-auth 1.0.2 → 1.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 +473 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,11 @@ A production-ready, **database-agnostic** JWT authentication library for Node.js
|
|
|
14
14
|
- 🧩 **Strategy Pattern** – Plug in only the auth methods you need
|
|
15
15
|
- 🛡️ **Middleware** – Express-compatible JWT verification middleware
|
|
16
16
|
- 🚀 **Express Router** – Drop-in `/auth` router with all endpoints
|
|
17
|
+
- 🏷️ **Custom JWT Claims** – Inject project-specific data into tokens via `buildTokenPayload`
|
|
18
|
+
- 📋 **User Metadata** – Optional `IUserMetadataStore` for arbitrary per-user key/value data
|
|
19
|
+
- 🛡️ **Roles & Permissions** – Optional `IRolesPermissionsStore` for RBAC with tenant awareness
|
|
20
|
+
- 📅 **Session Management** – Optional `ISessionStore` for device-aware session listing & revocation
|
|
21
|
+
- 🏢 **Multi-Tenancy** – Optional `ITenantStore` for isolated multi-tenant applications
|
|
17
22
|
|
|
18
23
|
## Installation
|
|
19
24
|
|
|
@@ -274,6 +279,11 @@ When you mount `auth.router()`, the following endpoints are available:
|
|
|
274
279
|
| `GET` | `/auth/me` | Get current user (protected) |
|
|
275
280
|
| `POST` | `/auth/forgot-password` | Send password reset email |
|
|
276
281
|
| `POST` | `/auth/reset-password` | Reset password with token |
|
|
282
|
+
| `POST` | `/auth/change-password` | Change password (authenticated, requires `currentPassword` + `newPassword`) |
|
|
283
|
+
| `POST` | `/auth/send-verification-email` | Send email verification link (authenticated) |
|
|
284
|
+
| `GET` | `/auth/verify-email?token=...` | Verify email address from link |
|
|
285
|
+
| `POST` | `/auth/change-email/request` | Request email change — sends verification to `newEmail` (authenticated) |
|
|
286
|
+
| `POST` | `/auth/change-email/confirm` | Confirm email change with token |
|
|
277
287
|
| `POST` | `/auth/2fa/setup` | Get TOTP secret + QR code (protected) |
|
|
278
288
|
| `POST` | `/auth/2fa/verify-setup` | Verify TOTP code and enable 2FA (protected) |
|
|
279
289
|
| `POST` | `/auth/2fa/verify` | Complete 2FA login |
|
|
@@ -551,6 +561,14 @@ interface BaseUser {
|
|
|
551
561
|
smsCode?: string | null;
|
|
552
562
|
smsCodeExpiry?: Date | null;
|
|
553
563
|
phoneNumber?: string | null;
|
|
564
|
+
// Email verification
|
|
565
|
+
isEmailVerified?: boolean;
|
|
566
|
+
emailVerificationToken?: string | null;
|
|
567
|
+
emailVerificationTokenExpiry?: Date | null;
|
|
568
|
+
// Change email
|
|
569
|
+
pendingEmail?: string | null;
|
|
570
|
+
emailChangeToken?: string | null;
|
|
571
|
+
emailChangeTokenExpiry?: Date | null;
|
|
554
572
|
}
|
|
555
573
|
```
|
|
556
574
|
|
|
@@ -590,7 +608,159 @@ class ApiKeyStrategy extends BaseAuthStrategy<{ apiKey: string }, MyUser> {
|
|
|
590
608
|
}
|
|
591
609
|
```
|
|
592
610
|
|
|
593
|
-
##
|
|
611
|
+
## Email Verification
|
|
612
|
+
|
|
613
|
+
The email-verification flow reuses the same token infrastructure as password reset and is available out of the box once you implement the three optional store methods.
|
|
614
|
+
|
|
615
|
+
### IUserStore additions
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// Required to support /auth/send-verification-email and /auth/verify-email
|
|
619
|
+
updateEmailVerificationToken(userId, token, expiry): Promise<void>
|
|
620
|
+
updateEmailVerified(userId, isVerified): Promise<void>
|
|
621
|
+
findByEmailVerificationToken(token): Promise<U | null>
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### AuthConfig email callbacks
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
email: {
|
|
628
|
+
// Called when a verification email is needed (takes precedence over mailer)
|
|
629
|
+
sendVerificationEmail: async (to, token, link, lang?) => { /* ... */ },
|
|
630
|
+
// Called after a successful email change (notifies the old address)
|
|
631
|
+
sendEmailChanged: async (to, newEmail, lang?) => { /* ... */ },
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### Flow
|
|
636
|
+
|
|
637
|
+
1. After registration, call `POST /auth/send-verification-email` (authenticated) — the library generates a 24-hour token, calls `updateEmailVerificationToken`, then fires `sendVerificationEmail`.
|
|
638
|
+
2. The user clicks the link in their inbox; the link points to `GET /auth/verify-email?token=<token>` — the library calls `updateEmailVerified(userId, true)` and clears the token.
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
// Example: send on registration
|
|
642
|
+
app.post('/register', async (req, res) => {
|
|
643
|
+
const user = await userStore.create({ email: req.body.email, password: hashedPw });
|
|
644
|
+
// Log them in
|
|
645
|
+
const tokens = tokenService.generateTokenPair({ sub: user.id, email: user.email }, config);
|
|
646
|
+
tokenService.setTokenCookies(res, tokens, config);
|
|
647
|
+
// Trigger verification email (the library does this automatically via the auth router)
|
|
648
|
+
// or call the endpoint directly:
|
|
649
|
+
await fetch('/auth/send-verification-email', {
|
|
650
|
+
method: 'POST',
|
|
651
|
+
headers: { Cookie: `accessToken=${tokens.accessToken}` },
|
|
652
|
+
});
|
|
653
|
+
res.json({ success: true });
|
|
654
|
+
});
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
## Change Password
|
|
658
|
+
|
|
659
|
+
`POST /auth/change-password` — **authenticated** — lets users update their password without going through the forgot-password flow.
|
|
660
|
+
|
|
661
|
+
**Request body:**
|
|
662
|
+
```json
|
|
663
|
+
{ "currentPassword": "OldP@ss1", "newPassword": "NewP@ss2" }
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
The endpoint verifies `currentPassword` against the stored bcrypt hash before applying the change. It returns `401` if the current password is wrong, or `400` for OAuth accounts that have no password set.
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
// Client example (fetch)
|
|
670
|
+
await fetch('/auth/change-password', {
|
|
671
|
+
method: 'POST',
|
|
672
|
+
credentials: 'include', // include HttpOnly cookies
|
|
673
|
+
headers: { 'Content-Type': 'application/json' },
|
|
674
|
+
body: JSON.stringify({ currentPassword: 'old', newPassword: 'new' }),
|
|
675
|
+
});
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
No extra `IUserStore` methods are needed — `updatePassword` is already a required method.
|
|
679
|
+
|
|
680
|
+
## Change Email
|
|
681
|
+
|
|
682
|
+
The change-email flow sends a confirmation link to the new address before committing the update, preventing account hijacking.
|
|
683
|
+
|
|
684
|
+
### IUserStore additions
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
// Required to support /auth/change-email/request and /auth/change-email/confirm
|
|
688
|
+
updateEmailChangeToken(userId, pendingEmail, token, expiry): Promise<void>
|
|
689
|
+
updateEmail(userId, newEmail): Promise<void>
|
|
690
|
+
findByEmailChangeToken(token): Promise<U | null>
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### Flow
|
|
694
|
+
|
|
695
|
+
1. Authenticated user calls `POST /auth/change-email/request` with `{ "newEmail": "new@example.com" }`.
|
|
696
|
+
- The library checks the new address is not already in use.
|
|
697
|
+
- A 1-hour token is generated, stored via `updateEmailChangeToken`, and a verification email is sent to the **new address**.
|
|
698
|
+
2. User clicks the link; it points to `POST /auth/change-email/confirm` with `{ "token": "..." }`.
|
|
699
|
+
- The library calls `updateEmail` (commits the change) and sends an email-changed notification to the **old address** via `sendEmailChanged`.
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
// 1. Request change
|
|
703
|
+
await fetch('/auth/change-email/request', {
|
|
704
|
+
method: 'POST',
|
|
705
|
+
credentials: 'include',
|
|
706
|
+
headers: { 'Content-Type': 'application/json' },
|
|
707
|
+
body: JSON.stringify({ newEmail: 'new@example.com' }),
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// 2. Confirm (called from the link in the email)
|
|
711
|
+
await fetch('/auth/change-email/confirm', {
|
|
712
|
+
method: 'POST',
|
|
713
|
+
headers: { 'Content-Type': 'application/json' },
|
|
714
|
+
body: JSON.stringify({ token: tokenFromLink }),
|
|
715
|
+
});
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
## Admin Panel
|
|
719
|
+
|
|
720
|
+
`createAdminRouter` mounts a **self-contained admin panel** — both the REST API and a vanilla-JS UI — at any path you choose. No build step, no external UI dependencies.
|
|
721
|
+
|
|
722
|
+
```typescript
|
|
723
|
+
import { createAdminRouter } from 'node-auth';
|
|
724
|
+
|
|
725
|
+
app.use('/admin', createAdminRouter(userStore, {
|
|
726
|
+
adminSecret: process.env.ADMIN_SECRET!, // Bearer token required for all admin routes
|
|
727
|
+
sessionStore, // optional — enables Sessions tab
|
|
728
|
+
rbacStore, // optional — enables Roles & Permissions tab
|
|
729
|
+
tenantStore, // optional — enables Tenants tab
|
|
730
|
+
}));
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Open `http://localhost:3000/admin/` in your browser, enter the admin secret, and you get a tabbed dashboard:
|
|
734
|
+
|
|
735
|
+
| Tab | Requires | Features |
|
|
736
|
+
|-----|----------|---------|
|
|
737
|
+
| **Users** | `IUserStore.listUsers` | Paginated user table, delete user |
|
|
738
|
+
| **Sessions** | `ISessionStore.getAllSessions` | All active sessions, revoke by handle |
|
|
739
|
+
| **Roles & Permissions** | `IRolesPermissionsStore.getAllRoles` | List roles with permissions, create/delete roles |
|
|
740
|
+
| **Tenants** | `ITenantStore.getAllTenants` | List tenants, create/delete tenants |
|
|
741
|
+
|
|
742
|
+
Tabs that are not configured are hidden automatically.
|
|
743
|
+
|
|
744
|
+
### Admin REST API
|
|
745
|
+
|
|
746
|
+
All admin API endpoints require `Authorization: Bearer <adminSecret>`.
|
|
747
|
+
|
|
748
|
+
| Method | Path | Description |
|
|
749
|
+
|--------|------|-------------|
|
|
750
|
+
| `GET` | `/admin/api/ping` | Health check / auth verification |
|
|
751
|
+
| `GET` | `/admin/api/users` | List users (`?limit=&offset=`) |
|
|
752
|
+
| `GET` | `/admin/api/users/:id` | Get single user |
|
|
753
|
+
| `DELETE` | `/admin/api/users/:id` | Delete user (requires `IUserStore.deleteUser`) |
|
|
754
|
+
| `GET` | `/admin/api/sessions` | List all sessions (`?limit=&offset=`) |
|
|
755
|
+
| `DELETE` | `/admin/api/sessions/:handle` | Revoke a session |
|
|
756
|
+
| `GET` | `/admin/api/roles` | List all roles with permissions |
|
|
757
|
+
| `POST` | `/admin/api/roles` | Create a role |
|
|
758
|
+
| `DELETE` | `/admin/api/roles/:name` | Delete a role |
|
|
759
|
+
| `GET` | `/admin/api/tenants` | List all tenants |
|
|
760
|
+
| `POST` | `/admin/api/tenants` | Create a tenant |
|
|
761
|
+
| `DELETE` | `/admin/api/tenants/:id` | Delete a tenant |
|
|
762
|
+
|
|
763
|
+
> **Security note:** Mount the admin router behind a VPN or IP allow-list in production. The `adminSecret` is a single shared token — treat it like a root password.
|
|
594
764
|
|
|
595
765
|
Auth routes should be rate-limited in production to prevent brute-force attacks. Pass an optional `rateLimiter` middleware to `createAuthRouter()`:
|
|
596
766
|
|
|
@@ -604,6 +774,273 @@ app.use('/auth', auth.router({ rateLimiter: limiter }));
|
|
|
604
774
|
|
|
605
775
|
All sensitive endpoints (login, refresh, password reset, 2FA, magic links, SMS) will be protected.
|
|
606
776
|
|
|
777
|
+
## Custom JWT Claims
|
|
778
|
+
|
|
779
|
+
Inject arbitrary project-specific data into both the access and refresh JWTs by providing `buildTokenPayload` in `AuthConfig`. The returned object is **merged** on top of the standard `{ sub, email, role }` claims.
|
|
780
|
+
|
|
781
|
+
```typescript
|
|
782
|
+
import { AuthConfigurator, AuthConfig } from 'node-auth';
|
|
783
|
+
|
|
784
|
+
const config: AuthConfig = {
|
|
785
|
+
accessTokenSecret: '...',
|
|
786
|
+
refreshTokenSecret: '...',
|
|
787
|
+
buildTokenPayload: (user) => ({
|
|
788
|
+
permissions: user.permissions, // your extended user fields
|
|
789
|
+
tenantId: user.tenantId,
|
|
790
|
+
plan: user.plan,
|
|
791
|
+
}),
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
const auth = new AuthConfigurator(config, userStore);
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
After login the custom claims are available on `req.user` in any protected route:
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
app.get('/protected', auth.middleware(), (req, res) => {
|
|
801
|
+
// req.user.tenantId, req.user.permissions, etc.
|
|
802
|
+
res.json(req.user);
|
|
803
|
+
});
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
## User Metadata
|
|
807
|
+
|
|
808
|
+
`IUserMetadataStore` is an **optional** interface for attaching arbitrary key/value metadata to users without altering `BaseUser` or your users table.
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
import { IUserMetadataStore } from 'node-auth';
|
|
812
|
+
|
|
813
|
+
export class MyUserMetadataStore implements IUserMetadataStore {
|
|
814
|
+
/** Return all metadata for a user; empty object when none exists. */
|
|
815
|
+
async getMetadata(userId: string): Promise<Record<string, unknown>> {
|
|
816
|
+
const row = await db('user_metadata').where({ userId }).first();
|
|
817
|
+
return row ? JSON.parse(row.data) : {};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** Shallow-merge new key/value pairs into the existing metadata. */
|
|
821
|
+
async updateMetadata(userId: string, metadata: Record<string, unknown>): Promise<void> {
|
|
822
|
+
const existing = await this.getMetadata(userId);
|
|
823
|
+
const merged = { ...existing, ...metadata };
|
|
824
|
+
await db('user_metadata')
|
|
825
|
+
.insert({ userId, data: JSON.stringify(merged) })
|
|
826
|
+
.onConflict('userId').merge();
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** Remove all metadata for the user (e.g. on account deletion). */
|
|
830
|
+
async clearMetadata(userId: string): Promise<void> {
|
|
831
|
+
await db('user_metadata').where({ userId }).delete();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### Usage
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
const metaStore = new MyUserMetadataStore();
|
|
840
|
+
|
|
841
|
+
// Store preferences after login
|
|
842
|
+
await metaStore.updateMetadata(userId, { theme: 'dark', lang: 'it', onboarded: true });
|
|
843
|
+
|
|
844
|
+
// Read them back
|
|
845
|
+
const meta = await metaStore.getMetadata(userId);
|
|
846
|
+
console.log(meta.theme); // 'dark'
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
## Roles & Permissions
|
|
850
|
+
|
|
851
|
+
`IRolesPermissionsStore` is an **optional** interface for role-based access control (RBAC). It supports both single-tenant and multi-tenant applications via an optional `tenantId` parameter.
|
|
852
|
+
|
|
853
|
+
```typescript
|
|
854
|
+
import { IRolesPermissionsStore } from 'node-auth';
|
|
855
|
+
|
|
856
|
+
export class MyRbacStore implements IRolesPermissionsStore {
|
|
857
|
+
// User ↔ Role
|
|
858
|
+
async addRoleToUser(userId: string, role: string, tenantId?: string): Promise<void> { /* ... */ }
|
|
859
|
+
async removeRoleFromUser(userId: string, role: string, tenantId?: string): Promise<void> { /* ... */ }
|
|
860
|
+
async getRolesForUser(userId: string, tenantId?: string): Promise<string[]> { /* ... */ }
|
|
861
|
+
|
|
862
|
+
// Role management
|
|
863
|
+
async createRole(role: string, permissions?: string[]): Promise<void> { /* ... */ }
|
|
864
|
+
async deleteRole(role: string): Promise<void> { /* ... */ }
|
|
865
|
+
|
|
866
|
+
// Role ↔ Permission
|
|
867
|
+
async addPermissionToRole(role: string, permission: string): Promise<void> { /* ... */ }
|
|
868
|
+
async removePermissionFromRole(role: string, permission: string): Promise<void> { /* ... */ }
|
|
869
|
+
async getPermissionsForRole(role: string): Promise<string[]> { /* ... */ }
|
|
870
|
+
|
|
871
|
+
// Convenience
|
|
872
|
+
async getPermissionsForUser(userId: string, tenantId?: string): Promise<string[]> { /* ... */ }
|
|
873
|
+
async userHasPermission(userId: string, permission: string, tenantId?: string): Promise<boolean> { /* ... */ }
|
|
874
|
+
}
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
### Usage
|
|
878
|
+
|
|
879
|
+
```typescript
|
|
880
|
+
const rbac = new MyRbacStore();
|
|
881
|
+
|
|
882
|
+
// Create roles with permissions
|
|
883
|
+
await rbac.createRole('editor', ['posts:read', 'posts:write']);
|
|
884
|
+
await rbac.createRole('admin', ['posts:read', 'posts:write', 'users:manage']);
|
|
885
|
+
|
|
886
|
+
// Assign a role to a user (optionally scoped to a tenant)
|
|
887
|
+
await rbac.addRoleToUser(userId, 'editor', 'tenant-acme');
|
|
888
|
+
|
|
889
|
+
// Protect a route
|
|
890
|
+
app.delete('/posts/:id', auth.middleware(), async (req, res) => {
|
|
891
|
+
const allowed = await rbac.userHasPermission(req.user!.sub, 'posts:write', req.user!.tenantId as string | undefined);
|
|
892
|
+
if (!allowed) return res.status(403).json({ error: 'Forbidden' });
|
|
893
|
+
// ... delete post
|
|
894
|
+
});
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### Combining with `buildTokenPayload`
|
|
898
|
+
|
|
899
|
+
You can embed roles/permissions directly in the JWT so route guards do not need an async DB call:
|
|
900
|
+
|
|
901
|
+
```typescript
|
|
902
|
+
buildTokenPayload: async (user) => ({
|
|
903
|
+
roles: await rbac.getRolesForUser(user.id),
|
|
904
|
+
permissions: await rbac.getPermissionsForUser(user.id),
|
|
905
|
+
}),
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
## Session Management
|
|
909
|
+
|
|
910
|
+
`ISessionStore` is an **optional** interface for device-aware session management — useful for "active sessions" screens where users can see and revoke individual devices.
|
|
911
|
+
|
|
912
|
+
```typescript
|
|
913
|
+
import { ISessionStore, SessionInfo } from 'node-auth';
|
|
914
|
+
|
|
915
|
+
export class MySessionStore implements ISessionStore {
|
|
916
|
+
async createSession(info: Omit<SessionInfo, 'sessionHandle'>): Promise<SessionInfo> {
|
|
917
|
+
const sessionHandle = crypto.randomUUID();
|
|
918
|
+
await db('sessions').insert({ sessionHandle, ...info });
|
|
919
|
+
return { sessionHandle, ...info };
|
|
920
|
+
}
|
|
921
|
+
async getSession(sessionHandle: string): Promise<SessionInfo | null> {
|
|
922
|
+
return db('sessions').where({ sessionHandle }).first() ?? null;
|
|
923
|
+
}
|
|
924
|
+
async getSessionsForUser(userId: string, tenantId?: string): Promise<SessionInfo[]> {
|
|
925
|
+
return db('sessions').where({ userId, ...(tenantId ? { tenantId } : {}) });
|
|
926
|
+
}
|
|
927
|
+
async updateSessionLastActive(sessionHandle: string): Promise<void> {
|
|
928
|
+
await db('sessions').where({ sessionHandle }).update({ lastActiveAt: new Date() });
|
|
929
|
+
}
|
|
930
|
+
async revokeSession(sessionHandle: string): Promise<void> {
|
|
931
|
+
await db('sessions').where({ sessionHandle }).delete();
|
|
932
|
+
}
|
|
933
|
+
async revokeAllSessionsForUser(userId: string, tenantId?: string): Promise<void> {
|
|
934
|
+
await db('sessions').where({ userId, ...(tenantId ? { tenantId } : {}) }).delete();
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
### Usage
|
|
940
|
+
|
|
941
|
+
```typescript
|
|
942
|
+
const sessions = new MySessionStore();
|
|
943
|
+
|
|
944
|
+
// Create a session on login (combine with the auth router's login handler)
|
|
945
|
+
const session = await sessions.createSession({
|
|
946
|
+
userId,
|
|
947
|
+
createdAt: new Date(),
|
|
948
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
949
|
+
userAgent: req.headers['user-agent'],
|
|
950
|
+
ipAddress: req.ip,
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
// List active sessions for a "Manage devices" page
|
|
954
|
+
app.get('/sessions', auth.middleware(), async (req, res) => {
|
|
955
|
+
const list = await sessions.getSessionsForUser(req.user!.sub);
|
|
956
|
+
res.json(list);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// Revoke a specific session
|
|
960
|
+
app.delete('/sessions/:handle', auth.middleware(), async (req, res) => {
|
|
961
|
+
await sessions.revokeSession(req.params.handle);
|
|
962
|
+
res.json({ success: true });
|
|
963
|
+
});
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
### `SessionInfo` model
|
|
967
|
+
|
|
968
|
+
```typescript
|
|
969
|
+
interface SessionInfo {
|
|
970
|
+
sessionHandle: string; // Unique opaque identifier
|
|
971
|
+
userId: string;
|
|
972
|
+
tenantId?: string; // Optional tenant scope
|
|
973
|
+
createdAt: Date;
|
|
974
|
+
expiresAt: Date;
|
|
975
|
+
lastActiveAt?: Date; // Bumped on each authenticated request
|
|
976
|
+
userAgent?: string;
|
|
977
|
+
ipAddress?: string;
|
|
978
|
+
data?: Record<string, unknown>; // Any extra device/context data
|
|
979
|
+
}
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
## Multi-Tenancy
|
|
983
|
+
|
|
984
|
+
`ITenantStore` is an **optional** interface for applications that serve multiple independent tenants (organisations, workspaces, teams).
|
|
985
|
+
|
|
986
|
+
```typescript
|
|
987
|
+
import { ITenantStore, Tenant } from 'node-auth';
|
|
988
|
+
|
|
989
|
+
export class MyTenantStore implements ITenantStore {
|
|
990
|
+
// Tenant CRUD
|
|
991
|
+
async createTenant(data: Omit<Tenant, 'id'>): Promise<Tenant> { /* ... */ }
|
|
992
|
+
async getTenantById(id: string): Promise<Tenant | null> { /* ... */ }
|
|
993
|
+
async getAllTenants(): Promise<Tenant[]> { /* ... */ }
|
|
994
|
+
async updateTenant(id: string, data: Partial<Omit<Tenant, 'id'>>): Promise<void> { /* ... */ }
|
|
995
|
+
async deleteTenant(id: string): Promise<void> { /* ... */ }
|
|
996
|
+
|
|
997
|
+
// User ↔ Tenant membership
|
|
998
|
+
async associateUserWithTenant(userId: string, tenantId: string): Promise<void> { /* ... */ }
|
|
999
|
+
async disassociateUserFromTenant(userId: string, tenantId: string): Promise<void> { /* ... */ }
|
|
1000
|
+
async getTenantsForUser(userId: string): Promise<Tenant[]> { /* ... */ }
|
|
1001
|
+
async getUsersForTenant(tenantId: string): Promise<string[]> { /* ... */ }
|
|
1002
|
+
}
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
### Usage
|
|
1006
|
+
|
|
1007
|
+
```typescript
|
|
1008
|
+
const tenants = new MyTenantStore();
|
|
1009
|
+
|
|
1010
|
+
// Onboarding: create a tenant and assign the first user as owner
|
|
1011
|
+
const tenant = await tenants.createTenant({ name: 'Acme Corp', isActive: true });
|
|
1012
|
+
await tenants.associateUserWithTenant(userId, tenant.id);
|
|
1013
|
+
|
|
1014
|
+
// Inject tenantId into the JWT via buildTokenPayload.
|
|
1015
|
+
// For users that belong to a single tenant this works directly.
|
|
1016
|
+
// For users with multiple tenants, embed the full list and let the
|
|
1017
|
+
// client pass an `X-Tenant-ID` header that is validated per-request.
|
|
1018
|
+
buildTokenPayload: async (user) => ({
|
|
1019
|
+
tenants: (await tenants.getTenantsForUser(user.id)).map(t => t.id),
|
|
1020
|
+
}),
|
|
1021
|
+
|
|
1022
|
+
// Guard: ensure user belongs to the requested tenant
|
|
1023
|
+
app.get('/tenants/:id/data', auth.middleware(), async (req, res) => {
|
|
1024
|
+
const userTenants = await tenants.getTenantsForUser(req.user!.sub);
|
|
1025
|
+
if (!userTenants.find(t => t.id === req.params.id)) {
|
|
1026
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1027
|
+
}
|
|
1028
|
+
// ... return tenant data
|
|
1029
|
+
});
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### `Tenant` model
|
|
1033
|
+
|
|
1034
|
+
```typescript
|
|
1035
|
+
interface Tenant {
|
|
1036
|
+
id: string; // Unique identifier (slug, UUID, etc.)
|
|
1037
|
+
name: string;
|
|
1038
|
+
isActive?: boolean; // Defaults to true
|
|
1039
|
+
config?: Record<string, unknown>; // Per-tenant settings (branding, feature flags, etc.)
|
|
1040
|
+
createdAt?: Date;
|
|
1041
|
+
}
|
|
1042
|
+
```
|
|
1043
|
+
|
|
607
1044
|
## Building
|
|
608
1045
|
|
|
609
1046
|
```bash
|
|
@@ -621,16 +1058,47 @@ npm run test:coverage
|
|
|
621
1058
|
|
|
622
1059
|
```
|
|
623
1060
|
src/
|
|
624
|
-
├── interfaces/ # IUserStore, ITokenStore, IAuthStrategy
|
|
625
|
-
|
|
1061
|
+
├── interfaces/ # IUserStore, ITokenStore, IAuthStrategy,
|
|
1062
|
+
│ # IUserMetadataStore, IRolesPermissionsStore,
|
|
1063
|
+
│ # ISessionStore, ITenantStore
|
|
1064
|
+
├── models/ # BaseUser, TokenPair, AuthConfig, AuthError,
|
|
1065
|
+
│ # SessionInfo, Tenant
|
|
626
1066
|
├── abstract/ # BaseAuthStrategy, BaseOAuthStrategy
|
|
627
1067
|
├── strategies/ # Local, Google, GitHub, MagicLink, SMS, TOTP
|
|
628
|
-
├── services/ # TokenService, PasswordService, SmsService
|
|
1068
|
+
├── services/ # TokenService, PasswordService, SmsService, MailerService
|
|
629
1069
|
├── middleware/ # createAuthMiddleware()
|
|
630
|
-
├── router/ # createAuthRouter() –
|
|
1070
|
+
├── router/ # createAuthRouter() – auth endpoints
|
|
1071
|
+
│ # createAdminRouter() – admin panel UI + REST API
|
|
631
1072
|
└── auth-configurator.ts # Main entry point
|
|
632
1073
|
```
|
|
633
1074
|
|
|
1075
|
+
## Comparison with SuperTokens
|
|
1076
|
+
|
|
1077
|
+
The table below maps SuperTokens recipes to node-auth equivalents so you can evaluate feature coverage for your project.
|
|
1078
|
+
|
|
1079
|
+
| SuperTokens Feature | node-auth equivalent | Notes |
|
|
1080
|
+
|---------------------|---------------------|-------|
|
|
1081
|
+
| EmailPassword recipe | `LocalStrategy` + `POST /auth/login` | Full email/password auth with bcrypt |
|
|
1082
|
+
| ThirdParty / OAuth | `GoogleStrategy`, `GithubStrategy` | Extend abstract strategies for any provider |
|
|
1083
|
+
| Passwordless (magic link) | `MagicLinkStrategy` | Email token via built-in mailer or callback |
|
|
1084
|
+
| Passwordless (OTP via SMS) | `SmsStrategy` | SMS code via configurable HTTP endpoint |
|
|
1085
|
+
| TOTP 2FA | `TotpStrategy` | Authenticator-app compatible; QR code included |
|
|
1086
|
+
| Session management | `ISessionStore` _(optional)_ | Device-aware sessions; list & revoke |
|
|
1087
|
+
| User Roles | `IRolesPermissionsStore` _(optional)_ | Full RBAC with tenant scoping |
|
|
1088
|
+
| User Metadata | `IUserMetadataStore` _(optional)_ | Arbitrary key/value store per user |
|
|
1089
|
+
| Multi-tenancy | `ITenantStore` _(optional)_ | Tenant CRUD + user membership |
|
|
1090
|
+
| Custom JWT claims | `buildTokenPayload` callback | Inject any data into access & refresh tokens |
|
|
1091
|
+
| Database agnostic | `IUserStore` interface | One interface, any DB |
|
|
1092
|
+
| Rate limiting | `rateLimiter` option in `router()` | Pass any Express middleware |
|
|
1093
|
+
| Email verification | `POST /send-verification-email` + `GET /verify-email` | Token-based, 24 h expiry, built-in templates |
|
|
1094
|
+
| Change password | `POST /change-password` | Authenticated; verifies current password |
|
|
1095
|
+
| Change email | `POST /change-email/request` + `POST /change-email/confirm` | Verification to new address, notification to old |
|
|
1096
|
+
| Admin dashboard UI | `createAdminRouter()` | Self-contained UI + REST API, Bearer-token protected |
|
|
1097
|
+
| Account linking | _(not built-in)_ | Link OAuth profiles in your `IUserStore.create` |
|
|
1098
|
+
| Attack protection | _(not built-in)_ | Use `rateLimiter` + external WAF |
|
|
1099
|
+
|
|
1100
|
+
> **Roadmap ideas:** account-linking helpers, email-verification enforcement middleware, SCIM provisioning.
|
|
1101
|
+
|
|
634
1102
|
## License
|
|
635
1103
|
|
|
636
1104
|
MIT
|