kroxt 1.0.1 → 1.1.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 +169 -82
- package/dist-lib/core.d.ts +17 -5
- package/dist-lib/core.js +56 -25
- package/dist-lib/index.d.ts +2 -1
- package/dist-lib/index.js +2 -1
- package/dist-lib/security.d.ts +16 -0
- package/dist-lib/security.js +29 -0
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kroxt Auth
|
|
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
CHANGED
|
@@ -1,82 +1,169 @@
|
|
|
1
|
-
# kroxt
|
|
2
|
-
|
|
3
|
-
A framework-agnostic, modular authentication engine for modern TypeScript applications. Built for security, extensibility, and ease of use.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- 🔐 **Secure Hashing**: Powered by `argon2` for industry-standard password security.
|
|
8
|
-
- 🎟️ **
|
|
9
|
-
- 🌍 **OAuth Ready**: Built-in support for GitHub and Google OAuth via `arctic`.
|
|
10
|
-
- 🧩 **Database Agnostic**: Use Mongoose, Prisma, Drizzle, or
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
# kroxt
|
|
2
|
+
|
|
3
|
+
A framework-agnostic, modular authentication engine for modern TypeScript applications. Built for security, extensibility, and ease of use.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔐 **Secure Hashing**: Powered by `argon2` for industry-standard password security.
|
|
8
|
+
- 🎟️ **Dual-Token Sessions**: Native support for Access and Refresh tokens via `jose`.
|
|
9
|
+
- 🌍 **OAuth Ready**: Built-in support for GitHub and Google OAuth via `arctic`.
|
|
10
|
+
- 🧩 **Database Agnostic**: Use Mongoose, Prisma, Drizzle, or any store via the `AuthAdapter` pattern.
|
|
11
|
+
- 🌶️ **Password Peppering**: Server-side pepper support for enhanced hash protection.
|
|
12
|
+
- 🛡️ **Timing Attack Protection**: Built-in safeguards against side-channel analysis during login.
|
|
13
|
+
- ✅ **Zod Schema Support**: Perfectly preserves and types your user metadata.
|
|
14
|
+
- 🚀 **ESM First**: Native support for NodeNext module resolution.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install kroxt
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Guide: Full Authentication Flow
|
|
25
|
+
|
|
26
|
+
This guide walks you through setting up Kroxt from scratch in your application.
|
|
27
|
+
|
|
28
|
+
### Step 1: The Adapter Pattern
|
|
29
|
+
|
|
30
|
+
Kroxt doesn't care which database you use. You just need to implement the `AuthAdapter` interface.
|
|
31
|
+
|
|
32
|
+
In this example, we use a simple user structure: `name`, `email`, and `password`.
|
|
33
|
+
> [!NOTE]
|
|
34
|
+
> Kroxt's adapter can accept **any** additional fields your application requires (e.g., `role`, `avatar`, `preferences`) with no limits.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import type { AuthAdapter, User } from "kroxt/adapter";
|
|
38
|
+
|
|
39
|
+
export const myAdapter: AuthAdapter = {
|
|
40
|
+
createUser: async (data) => {
|
|
41
|
+
// Save to your DB: { name, email, passwordHash, ...anyOtherFields }
|
|
42
|
+
// return the created user including its unique id
|
|
43
|
+
},
|
|
44
|
+
findUserByEmail: async (email) => {
|
|
45
|
+
// Find user by email in your DB
|
|
46
|
+
},
|
|
47
|
+
findUserById: async (id) => {
|
|
48
|
+
// Find user by ID in your DB
|
|
49
|
+
},
|
|
50
|
+
linkOAuthAccount: async (user, provider, providerId) => {
|
|
51
|
+
// Link an OAuth provider to an existing user
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Step 2: Initialize the Auth Engine
|
|
57
|
+
|
|
58
|
+
Configure Kroxt with your adapter and security settings.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { createAuth } from "kroxt";
|
|
62
|
+
import { myAdapter } from "./myAdapter.js";
|
|
63
|
+
|
|
64
|
+
export const auth = createAuth({
|
|
65
|
+
adapter: myAdapter,
|
|
66
|
+
secret: process.env.AUTH_SECRET, // High-entropy secret for JWT signing
|
|
67
|
+
pepper: process.env.AUTH_PEPPER, // Optional: Server-side pepper for password hashing
|
|
68
|
+
session: {
|
|
69
|
+
expires: "15m", // Access token duration
|
|
70
|
+
refreshExpires: "7d" // Refresh token duration
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Step 3: Implement Controllers & Routes
|
|
76
|
+
|
|
77
|
+
Use the engine in your application logic. Examples below use an Express-like structure.
|
|
78
|
+
|
|
79
|
+
#### Registration
|
|
80
|
+
```typescript
|
|
81
|
+
app.post("/register", async (req, res) => {
|
|
82
|
+
const { name, email, password, ...extraFields } = req.body;
|
|
83
|
+
|
|
84
|
+
// Kroxt handles argon2 hashing (with pepper) and token generation
|
|
85
|
+
const { user, accessToken, refreshToken } = await auth.signup({
|
|
86
|
+
name,
|
|
87
|
+
email,
|
|
88
|
+
...extraFields
|
|
89
|
+
}, password);
|
|
90
|
+
|
|
91
|
+
res.json({ user, accessToken, refreshToken });
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Login
|
|
96
|
+
```typescript
|
|
97
|
+
app.post("/login", async (req, res) => {
|
|
98
|
+
const { email, password } = req.body;
|
|
99
|
+
|
|
100
|
+
// Kroxt verifies password (timing-attack safe) and returns tokens
|
|
101
|
+
const { user, accessToken, refreshToken } = await auth.loginWithPassword(email, password);
|
|
102
|
+
|
|
103
|
+
res.json({ user, accessToken, refreshToken });
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Token Refresh
|
|
108
|
+
Keep users logged in by rotating access tokens using a valid refresh token.
|
|
109
|
+
```typescript
|
|
110
|
+
app.post("/refresh", async (req, res) => {
|
|
111
|
+
const { refreshToken } = req.body;
|
|
112
|
+
|
|
113
|
+
// Returns a fresh access token
|
|
114
|
+
const { accessToken } = await auth.refresh(refreshToken);
|
|
115
|
+
|
|
116
|
+
res.json({ accessToken });
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Protecting Routes (Middleware)
|
|
121
|
+
```typescript
|
|
122
|
+
app.get("/me", async (req, res) => {
|
|
123
|
+
const token = req.headers.authorization?.split(" ")[1];
|
|
124
|
+
|
|
125
|
+
// Verify the JWT and get the payload { sub: string, role: string, ... }
|
|
126
|
+
const payload = await auth.verifyToken(token, "access");
|
|
127
|
+
|
|
128
|
+
if (!payload) return res.status(401).send("Unauthorized");
|
|
129
|
+
|
|
130
|
+
const user = await myAdapter.findUserById(payload.sub);
|
|
131
|
+
res.json(user);
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Security Best Practices
|
|
138
|
+
|
|
139
|
+
### 1. Password Peppering
|
|
140
|
+
Always use a `pepper` in production. It's a server-side secret added to passwords before hashing. If your database is leaked, the hashes cannot be cracked without this pepper.
|
|
141
|
+
|
|
142
|
+
### 2. CSRF Protection
|
|
143
|
+
Kroxt provides helpers for the double-submit cookie pattern. Use these if you are storing tokens in cookies.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { generateCsrfToken, verifyCsrf } from "kroxt/security";
|
|
147
|
+
|
|
148
|
+
const token = generateCsrfToken();
|
|
149
|
+
const isValid = verifyCsrf(tokenInRequest, tokenInCookie);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 3. Secure Cookies
|
|
153
|
+
If using cookies, always set these flags:
|
|
154
|
+
- `httpOnly: true` (Prevents XSS)
|
|
155
|
+
- `secure: true` (Requires HTTPS)
|
|
156
|
+
- `sameSite: 'strict'` (Prevents CSRF)
|
|
157
|
+
|
|
158
|
+
### 4. Rate Limiting
|
|
159
|
+
Implement rate limiting (e.g., `express-rate-limit`) on `/login` and `/register` to block brute-force attempts.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Reference Project
|
|
164
|
+
|
|
165
|
+
Check out the `kroxt-example` folder or the [GitHub repository](https://github.com/adepoju-oluwatobi/kroxt-example) for a complete **Express + MongoDB** implementation using this library.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
package/dist-lib/core.d.ts
CHANGED
|
@@ -2,22 +2,34 @@ import type { AuthAdapter, User } from "./adapter.js";
|
|
|
2
2
|
import type { Provider } from "./providers.js";
|
|
3
3
|
export interface CreateAuthOptions {
|
|
4
4
|
adapter: AuthAdapter<any>;
|
|
5
|
-
secret: string;
|
|
5
|
+
secret: string | Uint8Array;
|
|
6
|
+
pepper?: string;
|
|
6
7
|
session?: {
|
|
7
8
|
expires?: string | number;
|
|
9
|
+
refreshExpires?: string | number;
|
|
8
10
|
};
|
|
9
11
|
providers?: Provider[];
|
|
10
12
|
}
|
|
11
13
|
export declare function createAuth(options: CreateAuthOptions): {
|
|
12
14
|
signup: (userData: Omit<User<any>, "id">, password?: string) => Promise<{
|
|
13
15
|
user: any;
|
|
14
|
-
|
|
16
|
+
accessToken: string;
|
|
17
|
+
refreshToken: string;
|
|
15
18
|
}>;
|
|
16
19
|
loginWithPassword: (email: string, password: string) => Promise<{
|
|
17
20
|
user: any;
|
|
18
|
-
|
|
21
|
+
accessToken: string;
|
|
22
|
+
refreshToken: string;
|
|
19
23
|
}>;
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
refresh: (refreshToken: string) => Promise<{
|
|
25
|
+
accessToken: string;
|
|
26
|
+
}>;
|
|
27
|
+
verifyToken: (token: string, expectedType?: "access" | "refresh") => Promise<import("jose").JWTPayload>;
|
|
28
|
+
generateToken: (user: User<any>, type?: "access" | "refresh") => Promise<string>;
|
|
22
29
|
_providers: Provider[];
|
|
23
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Utility to generate a high-entropy cryptographically secure secret.
|
|
33
|
+
* Useful for initializing the 'secret' option in createAuth.
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateSecret(length?: number): Uint8Array;
|
package/dist-lib/core.js
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import * as argon2 from "argon2";
|
|
2
2
|
import { SignJWT, jwtVerify } from "jose";
|
|
3
|
+
import crypto from "crypto";
|
|
3
4
|
export function createAuth(options) {
|
|
4
|
-
const { adapter, secret, session, providers } = options;
|
|
5
|
-
const encodedSecret = new TextEncoder().encode(secret);
|
|
6
|
-
const expiration = session?.expires || "
|
|
5
|
+
const { adapter, secret, pepper, session, providers } = options;
|
|
6
|
+
const encodedSecret = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
|
|
7
|
+
const expiration = session?.expires || "1h"; // Default access token to 1h
|
|
8
|
+
const refreshExpiration = session?.refreshExpires || "7d";
|
|
7
9
|
/**
|
|
8
10
|
* Generates a stateless JWT for a user session
|
|
9
11
|
*/
|
|
10
|
-
async function generateToken(user) {
|
|
11
|
-
return new SignJWT({ sub: user.id, role: user.role })
|
|
12
|
+
async function generateToken(user, type = "access") {
|
|
13
|
+
return new SignJWT({ sub: user.id, role: user.role, type })
|
|
12
14
|
.setProtectedHeader({ alg: "HS256" })
|
|
13
15
|
.setIssuedAt()
|
|
14
|
-
.setExpirationTime(expiration)
|
|
16
|
+
.setExpirationTime(type === "access" ? expiration : refreshExpiration)
|
|
15
17
|
.sign(encodedSecret);
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
|
-
* Verifies a JWT and returns the payload
|
|
20
|
+
* Verifies a JWT and returns the payload.
|
|
21
|
+
* Optionally checks for a specific token type (access/refresh).
|
|
19
22
|
*/
|
|
20
|
-
async function verifyToken(token) {
|
|
23
|
+
async function verifyToken(token, expectedType = "access") {
|
|
21
24
|
try {
|
|
22
25
|
const { payload } = await jwtVerify(token, encodedSecret);
|
|
26
|
+
if (payload.type !== expectedType)
|
|
27
|
+
return null;
|
|
23
28
|
return payload;
|
|
24
29
|
}
|
|
25
30
|
catch (e) {
|
|
@@ -27,41 +32,67 @@ export function createAuth(options) {
|
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
|
-
*
|
|
31
|
-
|
|
35
|
+
* Refreshes an access token using a valid refresh token.
|
|
36
|
+
*/
|
|
37
|
+
async function refresh(refreshToken) {
|
|
38
|
+
const payload = await verifyToken(refreshToken, "refresh");
|
|
39
|
+
if (!payload || !payload.sub) {
|
|
40
|
+
throw new Error("Invalid or expired refresh token");
|
|
41
|
+
}
|
|
42
|
+
const user = await adapter.findUserById(payload.sub);
|
|
43
|
+
if (!user) {
|
|
44
|
+
throw new Error("User not found");
|
|
45
|
+
}
|
|
46
|
+
const accessToken = await generateToken(user, "access");
|
|
47
|
+
return { accessToken };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Signup with a new user payload.
|
|
51
|
+
* Incorporates server-side pepper for password hashing if provided.
|
|
32
52
|
*/
|
|
33
53
|
async function signup(userData, password) {
|
|
34
54
|
let dataToSave = { ...userData };
|
|
35
55
|
if (password) {
|
|
36
|
-
|
|
56
|
+
const passwordWithPepper = pepper ? `${password}${pepper}` : password;
|
|
57
|
+
dataToSave.passwordHash = await argon2.hash(passwordWithPepper);
|
|
37
58
|
}
|
|
38
59
|
const newUser = await adapter.createUser(dataToSave);
|
|
39
|
-
const
|
|
40
|
-
|
|
60
|
+
const accessToken = await generateToken(newUser, "access");
|
|
61
|
+
const refreshToken = await generateToken(newUser, "refresh");
|
|
62
|
+
return { user: newUser, accessToken, refreshToken };
|
|
41
63
|
}
|
|
42
64
|
/**
|
|
43
|
-
* Standard Email/Password Login
|
|
65
|
+
* Standard Email/Password Login.
|
|
66
|
+
* Includes timing attack protection and password peppering.
|
|
44
67
|
*/
|
|
45
68
|
async function loginWithPassword(email, password) {
|
|
46
69
|
const user = await adapter.findUserByEmail(email);
|
|
47
|
-
if
|
|
70
|
+
// Timing attack protection: Always verify a hash, even if user doesn't exist.
|
|
71
|
+
// We use a dummy hash to keep execution time consistent.
|
|
72
|
+
const dummyHash = "$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RytpInY7i6C9M5l0D4n8Q+7j/J+i";
|
|
73
|
+
const targetHash = user?.passwordHash || dummyHash;
|
|
74
|
+
const passwordWithPepper = pepper ? `${password}${pepper}` : password;
|
|
75
|
+
const isValid = await argon2.verify(targetHash, passwordWithPepper);
|
|
76
|
+
if (!user || !user.passwordHash || !isValid) {
|
|
48
77
|
throw new Error("Invalid credentials");
|
|
49
78
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
const isValid = await argon2.verify(user.passwordHash, password);
|
|
54
|
-
if (!isValid) {
|
|
55
|
-
throw new Error("Invalid credentials");
|
|
56
|
-
}
|
|
57
|
-
const token = await generateToken(user);
|
|
58
|
-
return { user, token };
|
|
79
|
+
const accessToken = await generateToken(user, "access");
|
|
80
|
+
const refreshToken = await generateToken(user, "refresh");
|
|
81
|
+
return { user, accessToken, refreshToken };
|
|
59
82
|
}
|
|
60
83
|
return {
|
|
61
84
|
signup,
|
|
62
85
|
loginWithPassword,
|
|
86
|
+
refresh,
|
|
63
87
|
verifyToken,
|
|
64
88
|
generateToken,
|
|
65
|
-
_providers: providers
|
|
89
|
+
_providers: providers
|
|
66
90
|
};
|
|
67
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Utility to generate a high-entropy cryptographically secure secret.
|
|
94
|
+
* Useful for initializing the 'secret' option in createAuth.
|
|
95
|
+
*/
|
|
96
|
+
export function generateSecret(length = 32) {
|
|
97
|
+
return crypto.getRandomValues(new Uint8Array(length));
|
|
98
|
+
}
|
package/dist-lib/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type { AuthAdapter, User, BaseUser } from "./adapter.js";
|
|
2
2
|
export { GitHub, Google } from "./providers.js";
|
|
3
3
|
export type { Provider, ProviderConfig } from "./providers.js";
|
|
4
|
-
export { createAuth } from "./core.js";
|
|
4
|
+
export { createAuth, generateSecret } from "./core.js";
|
|
5
5
|
export type { CreateAuthOptions } from "./core.js";
|
|
6
6
|
export { createMemoryAdapter } from "./memoryAdapter.js";
|
|
7
|
+
export * from "./security.js";
|
package/dist-lib/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a stateless CSRF token using the double-submit cookie pattern.
|
|
3
|
+
* This is recommended for Express/Kroxt setups using cookies for sessions.
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateCsrfToken(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Simple middleware-ready check for CSRF tokens.
|
|
8
|
+
* Matches a token from the request body/headers against a cookie.
|
|
9
|
+
*/
|
|
10
|
+
export declare function verifyCsrf(tokenInRequest: string, tokenInCookie: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Security Recommendations for Kroxt:
|
|
13
|
+
* 1. Always set cookies with: httpOnly: true, secure: true, sameSite: 'strict'
|
|
14
|
+
* 2. Use a 'pepper' in createAuth to protect hashes.
|
|
15
|
+
* 3. Implement rate limiting on /login and /register endpoints.
|
|
16
|
+
*/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Generates a stateless CSRF token using the double-submit cookie pattern.
|
|
4
|
+
* This is recommended for Express/Kroxt setups using cookies for sessions.
|
|
5
|
+
*/
|
|
6
|
+
export function generateCsrfToken() {
|
|
7
|
+
return crypto.randomBytes(32).toString("hex");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Simple middleware-ready check for CSRF tokens.
|
|
11
|
+
* Matches a token from the request body/headers against a cookie.
|
|
12
|
+
*/
|
|
13
|
+
export function verifyCsrf(tokenInRequest, tokenInCookie) {
|
|
14
|
+
if (!tokenInRequest || !tokenInCookie)
|
|
15
|
+
return false;
|
|
16
|
+
// Constant time comparison
|
|
17
|
+
try {
|
|
18
|
+
return crypto.timingSafeEqual(Buffer.from(tokenInRequest), Buffer.from(tokenInCookie));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Security Recommendations for Kroxt:
|
|
26
|
+
* 1. Always set cookies with: httpOnly: true, secure: true, sameSite: 'strict'
|
|
27
|
+
* 2. Use a 'pepper' in createAuth to protect hashes.
|
|
28
|
+
* 3. Implement rate limiting on /login and /register endpoints.
|
|
29
|
+
*/
|