create-authenik8-app 2.3.5 → 2.4.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/package.json +3 -1
- package/templates/express-auth/package.json +1 -0
- package/templates/express-auth/src/app.ts +4 -1
- package/templates/express-auth/src/controllers/auth.controller.ts +13 -7
- package/templates/express-auth/src/controllers/protected.controller.ts +50 -0
- package/templates/express-auth/src/routes/protected.routes.ts +6 -3
- package/templates/express-auth/src/server.ts +3 -2
- package/templates/express-auth/src/services/auth.services.ts +8 -0
- package/templates/express-auth/src/utils/security.ts +65 -0
- package/templates/express-auth+/package.json +1 -0
- package/templates/express-auth+/src/auth/auth.middleware.ts +10 -0
- package/templates/express-auth+/src/auth/auth.ts +17 -8
- package/templates/express-auth+/src/auth/oauth.controller.ts +38 -0
- package/templates/express-auth+/src/auth/oauth.routes.ts +8 -37
- package/templates/express-auth+/src/auth/password.controller.ts +55 -0
- package/templates/express-auth+/src/auth/password.route.ts +3 -37
- package/templates/express-auth+/src/auth/protected.controller.ts +50 -0
- package/templates/express-auth+/src/auth/protected.routes.ts +6 -11
- package/templates/express-auth+/src/server.ts +12 -8
- package/templates/express-auth+/src/utils/security.ts +61 -0
- package/templates/express-base/app.ts +1 -1
- package/templates/express-base/controllers/base.controller.ts +51 -3
- package/templates/express-base/routes/base.routes.ts +3 -0
- package/templates/express-base/src/server.ts +3 -2
- package/templates/express-base/utils/security.ts +46 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-authenik8-app",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "⚡ Fast Express + TypeScript auth starter with secure JWT, refresh rotation, Redis, RBAC, OAuth & Prisma.\nPowered by the Authenik8 Identity Engine.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-authenik8-app": "dist/index.js"
|
|
@@ -51,7 +51,9 @@
|
|
|
51
51
|
"@types/express": "^5.0.6",
|
|
52
52
|
"@types/fs-extra": "^11.0.4",
|
|
53
53
|
"@types/node": "^25.5.2",
|
|
54
|
+
"@types/supertest": "^7.2.0",
|
|
54
55
|
"@vitest/coverage-v8": "^3.2.4",
|
|
56
|
+
"supertest": "^7.2.2",
|
|
55
57
|
"vitest": "^3.2.4"
|
|
56
58
|
},
|
|
57
59
|
"repository": {
|
|
@@ -5,7 +5,10 @@ import { createProtectedRoutes } from "./routes/protected.routes";
|
|
|
5
5
|
export const createApp = (auth: any) => {
|
|
6
6
|
const app = express();
|
|
7
7
|
|
|
8
|
-
app.use(express.json());
|
|
8
|
+
app.use(express.json({ limit: "16kb", strict: true }));
|
|
9
|
+
|
|
10
|
+
app.use(auth.helmet);
|
|
11
|
+
app.use(auth.rateLimit);
|
|
9
12
|
|
|
10
13
|
app.use("/auth", createAuthRoutes(auth));
|
|
11
14
|
app.use("/", createProtectedRoutes(auth));
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { Request, Response } from "express";
|
|
2
2
|
import { AuthService } from "../services/auth.services";
|
|
3
|
+
import { parseCredentials, parseRefreshToken } from "../utils/security";
|
|
3
4
|
|
|
4
5
|
export const createAuthController = (auth: any) => ({
|
|
5
6
|
async register(req: Request, res: Response) {
|
|
6
7
|
try {
|
|
7
|
-
const { email, password } = req.body;
|
|
8
|
+
const { email, password } = parseCredentials(req.body);
|
|
8
9
|
|
|
9
10
|
const user = await AuthService.register(email, password);
|
|
10
11
|
|
|
11
12
|
res.json({ message: "User created", userId: user.id });
|
|
12
13
|
} catch (err) {
|
|
13
|
-
res.status(400).json({ error: (err as Error).message });
|
|
14
|
+
res.status(400).json({ error: (err as Error).message || "Invalid registration request" });
|
|
14
15
|
}
|
|
15
16
|
},
|
|
16
17
|
|
|
17
18
|
async login(req: Request, res: Response) {
|
|
18
19
|
try {
|
|
19
|
-
const { email, password } = req.body;
|
|
20
|
+
const { email, password } = parseCredentials(req.body);
|
|
20
21
|
|
|
21
22
|
const user = await AuthService.login(email, password);
|
|
22
23
|
|
|
@@ -31,13 +32,18 @@ export const createAuthController = (auth: any) => ({
|
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
res.json({ accessToken, refreshToken });
|
|
34
|
-
} catch
|
|
35
|
-
res.status(401).json({ error:
|
|
35
|
+
} catch {
|
|
36
|
+
res.status(401).json({ error: "Invalid credentials" });
|
|
36
37
|
}
|
|
37
38
|
},
|
|
38
39
|
|
|
39
40
|
async refresh(req: Request, res: Response) {
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
try {
|
|
42
|
+
const refreshToken = parseRefreshToken(req.body);
|
|
43
|
+
const tokens = await auth.refreshToken(refreshToken);
|
|
44
|
+
res.json(tokens);
|
|
45
|
+
} catch {
|
|
46
|
+
res.status(401).json({ error: "Invalid refresh token" });
|
|
47
|
+
}
|
|
42
48
|
},
|
|
43
49
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import { sanitizeSessionResponse } from "../utils/security";
|
|
3
|
+
|
|
4
|
+
export const createProtectedController = () => ({
|
|
5
|
+
protected(req: Request, res: Response) {
|
|
6
|
+
res.json({ message: "Protected route" });
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
async listSessions(req: Request, res: Response) {
|
|
10
|
+
try {
|
|
11
|
+
const actions = (req as any).adminActions;
|
|
12
|
+
if (!actions) {
|
|
13
|
+
return res.status(503).json({ success: false, message: "Session management unavailable" });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sessions = await actions.listSessions(req.params.userId);
|
|
17
|
+
res.json({ sessions: sanitizeSessionResponse(sessions) });
|
|
18
|
+
} catch {
|
|
19
|
+
res.status(500).json({ success: false, message: "Failed to retrieve sessions" });
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async revokeSession(req: Request, res: Response) {
|
|
24
|
+
try {
|
|
25
|
+
const actions = (req as any).adminActions;
|
|
26
|
+
if (!actions) {
|
|
27
|
+
return res.status(503).json({ success: false, message: "Session management unavailable" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await actions.revokeSession(req.params.userId, req.params.sessionId);
|
|
31
|
+
res.json({ success: true, message: "Session revoked" });
|
|
32
|
+
} catch {
|
|
33
|
+
res.status(500).json({ success: false, message: "Failed to revoke session" });
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async revokeAllSessions(req: Request, res: Response) {
|
|
38
|
+
try {
|
|
39
|
+
const actions = (req as any).adminActions;
|
|
40
|
+
if (!actions) {
|
|
41
|
+
return res.status(503).json({ success: false, message: "Session management unavailable" });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await actions.revokeAllSessions(req.params.userId);
|
|
45
|
+
res.json({ success: true, message: "All sessions revoked" });
|
|
46
|
+
} catch {
|
|
47
|
+
res.status(500).json({ success: false, message: "Failed to revoke sessions" });
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
|
+
import { createProtectedController } from "../controllers/protected.controller";
|
|
2
3
|
|
|
3
4
|
export const createProtectedRoutes = (auth: any) => {
|
|
4
5
|
const router = Router();
|
|
6
|
+
const controller = createProtectedController();
|
|
5
7
|
|
|
6
|
-
router.get("/protected", auth.requireAdmin,
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
router.get("/protected", auth.requireAdmin, controller.protected);
|
|
9
|
+
router.get("/admin/sessions/:userId", auth.requireAdmin, controller.listSessions);
|
|
10
|
+
router.delete("/admin/sessions/:userId/:sessionId", auth.requireAdmin, controller.revokeSession);
|
|
11
|
+
router.delete("/admin/sessions/:userId", auth.requireAdmin, controller.revokeAllSessions);
|
|
9
12
|
|
|
10
13
|
return router;
|
|
11
14
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import dotenv from "dotenv";
|
|
2
2
|
import { createAuthenik8 } from "authenik8-core";
|
|
3
3
|
import { createApp } from "./app";
|
|
4
|
+
import { requiredSecret } from "./utils/security";
|
|
4
5
|
|
|
5
6
|
dotenv.config();
|
|
6
7
|
|
|
7
8
|
async function start() {
|
|
8
9
|
const auth = await createAuthenik8({
|
|
9
|
-
jwtSecret:
|
|
10
|
-
refreshSecret:
|
|
10
|
+
jwtSecret: requiredSecret("JWT_SECRET"),
|
|
11
|
+
refreshSecret: requiredSecret("REFRESH_SECRET"),
|
|
11
12
|
});
|
|
12
13
|
|
|
13
14
|
const app = createApp(auth);
|
|
@@ -3,6 +3,14 @@ import { hashPassword, comparePassword } from "../utils/hash";
|
|
|
3
3
|
|
|
4
4
|
export const AuthService = {
|
|
5
5
|
async register(email: string, password: string) {
|
|
6
|
+
const existingUser = await prisma.user.findUnique({
|
|
7
|
+
where: { email },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
if (existingUser) {
|
|
11
|
+
throw new Error("Email is already registered");
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
const hashedPassword = await hashPassword(password);
|
|
7
15
|
|
|
8
16
|
const user = await prisma.user.create({
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const sensitiveKeys = new Set(["token", "accessToken", "refreshToken"]);
|
|
2
|
+
|
|
3
|
+
export function requiredSecret(name: string): string {
|
|
4
|
+
const value = process.env[name];
|
|
5
|
+
|
|
6
|
+
if (typeof value !== "string" || value.trim().length < 32) {
|
|
7
|
+
throw new Error(`${name} must be set to at least 32 characters`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseCredentials(body: unknown) {
|
|
14
|
+
if (!body || typeof body !== "object") {
|
|
15
|
+
throw new Error("Email and password are required");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { email, password } = body as { email?: unknown; password?: unknown };
|
|
19
|
+
|
|
20
|
+
if (typeof email !== "string" || typeof password !== "string") {
|
|
21
|
+
throw new Error("Email and password are required");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
25
|
+
|
|
26
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
|
|
27
|
+
throw new Error("A valid email is required");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (password.length < 8 || password.length > 1024) {
|
|
31
|
+
throw new Error("Password must be between 8 and 1024 characters");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { email: normalizedEmail, password };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseRefreshToken(body: unknown) {
|
|
38
|
+
if (!body || typeof body !== "object") {
|
|
39
|
+
throw new Error("Refresh token is required");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { refreshToken } = body as { refreshToken?: unknown };
|
|
43
|
+
|
|
44
|
+
if (typeof refreshToken !== "string" || refreshToken.trim().length < 16) {
|
|
45
|
+
throw new Error("Refresh token is required");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return refreshToken;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sanitizeSessionResponse(value: unknown): unknown {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.map(sanitizeSessionResponse);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!value || typeof value !== "object") {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Object.fromEntries(
|
|
61
|
+
Object.entries(value as Record<string, unknown>)
|
|
62
|
+
.filter(([key]) => !sensitiveKeys.has(key))
|
|
63
|
+
.map(([key, nestedValue]) => [key, sanitizeSessionResponse(nestedValue)]),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { getAuth } from "./auth";
|
|
3
|
+
|
|
4
|
+
export const adminMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
|
5
|
+
getAuth().requireAdmin(req, res, next);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
|
9
|
+
getAuth().authenticateJWT(req, res, next);
|
|
10
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAuthenik8 } from "authenik8-core";
|
|
2
2
|
import dotenv from "dotenv";
|
|
3
|
+
import { requiredEnv, requiredSecret } from "../utils/security";
|
|
3
4
|
|
|
4
5
|
dotenv.config();
|
|
5
6
|
|
|
@@ -9,19 +10,19 @@ let authInstance: any;
|
|
|
9
10
|
|
|
10
11
|
export async function initAuth() {
|
|
11
12
|
authInstance= await createAuthenik8({
|
|
12
|
-
jwtSecret:
|
|
13
|
-
refreshSecret:
|
|
13
|
+
jwtSecret: requiredSecret("JWT_SECRET"),
|
|
14
|
+
refreshSecret: requiredSecret("REFRESH_SECRET"),
|
|
14
15
|
|
|
15
16
|
oauth: {
|
|
16
17
|
google: {
|
|
17
|
-
clientId:
|
|
18
|
-
clientSecret:
|
|
19
|
-
redirectUri: "
|
|
18
|
+
clientId: requiredEnv("GOOGLE_CLIENT_ID"),
|
|
19
|
+
clientSecret: requiredEnv("GOOGLE_CLIENT_SECRET"),
|
|
20
|
+
redirectUri: requiredEnv("GOOGLE_REDIRECT_URI"),
|
|
20
21
|
},
|
|
21
22
|
github: {
|
|
22
|
-
clientId:
|
|
23
|
-
clientSecret:
|
|
24
|
-
redirectUri: "
|
|
23
|
+
clientId: requiredEnv("GITHUB_CLIENT_ID"),
|
|
24
|
+
clientSecret: requiredEnv("GITHUB_CLIENT_SECRET"),
|
|
25
|
+
redirectUri: requiredEnv("GITHUB_REDIRECT_URI"),
|
|
25
26
|
},
|
|
26
27
|
},
|
|
27
28
|
});
|
|
@@ -35,3 +36,11 @@ export function getAuth() {
|
|
|
35
36
|
return authInstance;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
export const auth = new Proxy(
|
|
40
|
+
{},
|
|
41
|
+
{
|
|
42
|
+
get(_target, property) {
|
|
43
|
+
return getAuth()[property as keyof ReturnType<typeof getAuth>];
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
) as any;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import { getAuth } from "./auth";
|
|
3
|
+
|
|
4
|
+
export const oauthController = {
|
|
5
|
+
googleRedirect(req: Request, res: Response) {
|
|
6
|
+
getAuth().oauth?.google?.redirect(req, res);
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
async googleCallback(req: Request, res: Response) {
|
|
10
|
+
const result = await getAuth().oauth?.google?.handleCallback(req);
|
|
11
|
+
|
|
12
|
+
res.json({
|
|
13
|
+
provider: "google",
|
|
14
|
+
...result,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
githubRedirect(req: Request, res: Response) {
|
|
19
|
+
getAuth().oauth?.github?.redirect(req, res);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async githubCallback(req: Request, res: Response) {
|
|
23
|
+
const result = await getAuth().oauth?.github?.handleCallback(req);
|
|
24
|
+
|
|
25
|
+
res.json({
|
|
26
|
+
provider: "github",
|
|
27
|
+
...result,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
googleLink(req: Request, res: Response) {
|
|
32
|
+
getAuth().oauth?.google?.redirect(req, res, "link");
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
githubLink(req: Request, res: Response) {
|
|
36
|
+
getAuth().oauth?.github?.redirect(req, res, "link");
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -1,43 +1,14 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
-
import {
|
|
2
|
+
import { authMiddleware } from "./auth.middleware";
|
|
3
|
+
import { oauthController } from "./oauth.controller";
|
|
3
4
|
|
|
4
5
|
const router = express.Router();
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
router.get("/google",
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
router.get("/
|
|
12
|
-
const result = await auth.oauth?.google?.handleCallback(req);
|
|
13
|
-
|
|
14
|
-
res.json({
|
|
15
|
-
provider: "google",
|
|
16
|
-
...result,
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
// GITHUB
|
|
21
|
-
router.get("/github", (req, res) => {
|
|
22
|
-
auth.oauth?.github?.redirect(req, res);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
router.get("/github/callback", async (req, res) => {
|
|
26
|
-
const result = await auth.oauth?.github?.handleCallback(req);
|
|
27
|
-
|
|
28
|
-
res.json({
|
|
29
|
-
provider: "github",
|
|
30
|
-
...result,
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
router.get("/google/link", (req, res) => {
|
|
36
|
-
auth.oauth?.google?.redirect(req, res, "link");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
router.get("/github/link", (req, res) => {
|
|
40
|
-
auth.oauth?.github?.redirect(req, res, "link");
|
|
41
|
-
});
|
|
7
|
+
router.get("/google", oauthController.googleRedirect);
|
|
8
|
+
router.get("/google/callback", oauthController.googleCallback);
|
|
9
|
+
router.get("/github", oauthController.githubRedirect);
|
|
10
|
+
router.get("/github/callback", oauthController.githubCallback);
|
|
11
|
+
router.get("/google/link", authMiddleware, oauthController.googleLink);
|
|
12
|
+
router.get("/github/link", authMiddleware, oauthController.githubLink);
|
|
42
13
|
|
|
43
14
|
export default router;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import { getAuth } from "./auth";
|
|
3
|
+
import { prisma } from "../prisma/client";
|
|
4
|
+
import { hashPassword, comparePassword } from "../utils/hash";
|
|
5
|
+
import { parseCredentials } from "../utils/security";
|
|
6
|
+
|
|
7
|
+
export const passwordController = {
|
|
8
|
+
async register(req: Request, res: Response) {
|
|
9
|
+
try {
|
|
10
|
+
const { email, password } = parseCredentials(req.body);
|
|
11
|
+
|
|
12
|
+
const existingUser = await prisma.user.findUnique({ where: { email } });
|
|
13
|
+
if (existingUser) {
|
|
14
|
+
return res.status(400).json({ error: "Email is already registered" });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const user = await prisma.user.create({
|
|
18
|
+
data: {
|
|
19
|
+
email,
|
|
20
|
+
password: await hashPassword(password),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
res.json({ message: "User created", userId: user.id });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
res.status(400).json({ error: (err as Error).message || "Invalid registration request" });
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async login(req: Request, res: Response) {
|
|
31
|
+
try {
|
|
32
|
+
const { email, password } = parseCredentials(req.body);
|
|
33
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
34
|
+
|
|
35
|
+
if (!user || !(await comparePassword(password, user.password))) {
|
|
36
|
+
return res.status(401).json({ error: "Invalid credentials" });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const auth = getAuth();
|
|
40
|
+
const accessToken = auth.signToken({
|
|
41
|
+
userId: user.id,
|
|
42
|
+
email: user.email,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const refreshToken = await auth.generateRefreshToken({
|
|
46
|
+
userId: user.id,
|
|
47
|
+
email: user.email,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
res.json({ accessToken, refreshToken });
|
|
51
|
+
} catch {
|
|
52
|
+
res.status(401).json({ error: "Invalid credentials" });
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -1,43 +1,9 @@
|
|
|
1
|
-
import { auth } from "./auth";
|
|
2
|
-
import { prisma } from "../prisma/client";
|
|
3
|
-
import { hashPassword, comparePassword } from "../utils/hash";
|
|
4
1
|
import express from "express";
|
|
2
|
+
import { passwordController } from "./password.controller";
|
|
5
3
|
|
|
6
4
|
const router = express.Router();
|
|
7
5
|
|
|
8
|
-
router.post("/register",
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const user = await prisma.user.create({
|
|
12
|
-
data: {
|
|
13
|
-
email,
|
|
14
|
-
password: await hashPassword(password),
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
res.json({ message: "User created", userId: user.id });
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
router.post("/login", async (req, res) => {
|
|
22
|
-
const { email, password } = req.body;
|
|
23
|
-
|
|
24
|
-
const user = await prisma.user.findUnique({ where: { email } });
|
|
25
|
-
|
|
26
|
-
if (!user || !(await comparePassword(password, user.password))) {
|
|
27
|
-
return res.status(401).json({ error: "Invalid credentials" });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const accessToken = auth.signToken({
|
|
31
|
-
userId: user.id,
|
|
32
|
-
email: user.email,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const refreshToken = await auth.generateRefreshToken({
|
|
36
|
-
userId: user.id,
|
|
37
|
-
email: user.email,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
res.json({ accessToken, refreshToken });
|
|
41
|
-
});
|
|
6
|
+
router.post("/register", passwordController.register);
|
|
7
|
+
router.post("/login", passwordController.login);
|
|
42
8
|
|
|
43
9
|
export default router;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import { sanitizeSessionResponse } from "../utils/security";
|
|
3
|
+
|
|
4
|
+
export const protectedController = {
|
|
5
|
+
protected(req: Request, res: Response) {
|
|
6
|
+
res.json({ message: "Protected route" });
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
async listSessions(req: Request, res: Response) {
|
|
10
|
+
try {
|
|
11
|
+
const actions = (req as any).adminActions;
|
|
12
|
+
if (!actions) {
|
|
13
|
+
return res.status(503).json({ success: false, message: "Session management unavailable" });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sessions = await actions.listSessions(req.params.userId);
|
|
17
|
+
res.json({ sessions: sanitizeSessionResponse(sessions) });
|
|
18
|
+
} catch {
|
|
19
|
+
res.status(500).json({ success: false, message: "Failed to retrieve sessions" });
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async revokeSession(req: Request, res: Response) {
|
|
24
|
+
try {
|
|
25
|
+
const actions = (req as any).adminActions;
|
|
26
|
+
if (!actions) {
|
|
27
|
+
return res.status(503).json({ success: false, message: "Session management unavailable" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await actions.revokeSession(req.params.userId, req.params.sessionId);
|
|
31
|
+
res.json({ success: true, message: "Session revoked" });
|
|
32
|
+
} catch {
|
|
33
|
+
res.status(500).json({ success: false, message: "Failed to revoke session" });
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async revokeAllSessions(req: Request, res: Response) {
|
|
38
|
+
try {
|
|
39
|
+
const actions = (req as any).adminActions;
|
|
40
|
+
if (!actions) {
|
|
41
|
+
return res.status(503).json({ success: false, message: "Session management unavailable" });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await actions.revokeAllSessions(req.params.userId);
|
|
45
|
+
res.json({ success: true, message: "All sessions revoked" });
|
|
46
|
+
} catch {
|
|
47
|
+
res.status(500).json({ success: false, message: "Failed to revoke sessions" });
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { adminMiddleware } from "./auth.middleware";
|
|
3
|
+
import { protectedController } from "./protected.controller";
|
|
4
4
|
|
|
5
5
|
const router = express.Router();
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
router.get("/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
auth.requireAdmin(req, res, next);
|
|
13
|
-
}, (req, res) => {
|
|
14
|
-
res.json({ message: "Protected route" });
|
|
15
|
-
});
|
|
7
|
+
router.get("/protected", adminMiddleware, protectedController.protected);
|
|
8
|
+
router.get("/admin/sessions/:userId", adminMiddleware, protectedController.listSessions);
|
|
9
|
+
router.delete("/admin/sessions/:userId/:sessionId", adminMiddleware, protectedController.revokeSession);
|
|
10
|
+
router.delete("/admin/sessions/:userId", adminMiddleware, protectedController.revokeAllSessions);
|
|
16
11
|
|
|
17
12
|
export default router;
|
|
@@ -2,16 +2,20 @@ import express from "express";
|
|
|
2
2
|
import passwordRoutes from "./auth/password.route";
|
|
3
3
|
import oauthRoutes from "./auth/oauth.routes";
|
|
4
4
|
import protectedRoutes from "./auth/protected.routes";
|
|
5
|
-
import { initAuth } from "./auth/auth";
|
|
6
|
-
const app = express();
|
|
7
|
-
app.use(express.json());
|
|
8
|
-
|
|
9
|
-
app.use("/auth", passwordRoutes);
|
|
10
|
-
app.use("/auth", oauthRoutes);
|
|
11
|
-
app.use("/", protectedRoutes);
|
|
5
|
+
import { getAuth, initAuth } from "./auth/auth";
|
|
12
6
|
|
|
13
7
|
async function start(){
|
|
14
|
-
|
|
8
|
+
await initAuth();
|
|
9
|
+
const auth = getAuth();
|
|
10
|
+
const app = express();
|
|
11
|
+
|
|
12
|
+
app.use(express.json({ limit: "16kb", strict: true }));
|
|
13
|
+
app.use(auth.helmet);
|
|
14
|
+
app.use(auth.rateLimit);
|
|
15
|
+
|
|
16
|
+
app.use("/auth", passwordRoutes);
|
|
17
|
+
app.use("/auth", oauthRoutes);
|
|
18
|
+
app.use("/", protectedRoutes);
|
|
15
19
|
|
|
16
20
|
app.listen(3000, () => {
|
|
17
21
|
console.log("Auth system running on http://localhost:3000");
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const sensitiveKeys = new Set(["token", "accessToken", "refreshToken"]);
|
|
2
|
+
|
|
3
|
+
export function requiredSecret(name: string): string {
|
|
4
|
+
const value = process.env[name];
|
|
5
|
+
|
|
6
|
+
if (typeof value !== "string" || value.trim().length < 32) {
|
|
7
|
+
throw new Error(`${name} must be set to at least 32 characters`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function requiredEnv(name: string): string {
|
|
14
|
+
const value = process.env[name];
|
|
15
|
+
|
|
16
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
17
|
+
throw new Error(`${name} must be set`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseCredentials(body: unknown) {
|
|
24
|
+
if (!body || typeof body !== "object") {
|
|
25
|
+
throw new Error("Email and password are required");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { email, password } = body as { email?: unknown; password?: unknown };
|
|
29
|
+
|
|
30
|
+
if (typeof email !== "string" || typeof password !== "string") {
|
|
31
|
+
throw new Error("Email and password are required");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
35
|
+
|
|
36
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
|
|
37
|
+
throw new Error("A valid email is required");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (password.length < 8 || password.length > 1024) {
|
|
41
|
+
throw new Error("Password must be between 8 and 1024 characters");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { email: normalizedEmail, password };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function sanitizeSessionResponse(value: unknown): unknown {
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
return value.map(sanitizeSessionResponse);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!value || typeof value !== "object") {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Object.fromEntries(
|
|
57
|
+
Object.entries(value as Record<string, unknown>)
|
|
58
|
+
.filter(([key]) => !sensitiveKeys.has(key))
|
|
59
|
+
.map(([key, nestedValue]) => [key, sanitizeSessionResponse(nestedValue)]),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Request, Response } from "express";
|
|
2
|
+
import { getBearerToken, parseRefreshToken, sanitizeSessionResponse } from "../utils/security";
|
|
2
3
|
|
|
3
4
|
export const createBaseController = (auth: any) => ({
|
|
4
5
|
publicRoute(req: Request, res: Response) {
|
|
@@ -11,7 +12,7 @@ export const createBaseController = (auth: any) => ({
|
|
|
11
12
|
},
|
|
12
13
|
|
|
13
14
|
async protected(req: Request, res: Response) {
|
|
14
|
-
const token = req.headers.authorization
|
|
15
|
+
const token = getBearerToken(req.headers.authorization);
|
|
15
16
|
|
|
16
17
|
try {
|
|
17
18
|
const decoded = await auth.verifyToken(token);
|
|
@@ -22,11 +23,58 @@ export const createBaseController = (auth: any) => ({
|
|
|
22
23
|
},
|
|
23
24
|
|
|
24
25
|
async refresh(req: Request, res: Response) {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
try {
|
|
27
|
+
const refreshToken = parseRefreshToken(req.body);
|
|
28
|
+
const tokens = await auth.refreshToken(refreshToken);
|
|
29
|
+
res.json(tokens);
|
|
30
|
+
} catch {
|
|
31
|
+
res.status(401).json({ error: "Invalid refresh token" });
|
|
32
|
+
}
|
|
27
33
|
},
|
|
28
34
|
|
|
29
35
|
admin(req: Request, res: Response) {
|
|
30
36
|
res.json({ message: "Admin only" });
|
|
31
37
|
},
|
|
38
|
+
|
|
39
|
+
async listSessions(req: Request, res: Response) {
|
|
40
|
+
try {
|
|
41
|
+
const actions = (req as any).adminActions;
|
|
42
|
+
if (!actions) {
|
|
43
|
+
return res.status(503).json({ success: false, message: "Session management unavailable" });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sessions = await actions.listSessions(req.params.userId);
|
|
47
|
+
res.json({ sessions: sanitizeSessionResponse(sessions) });
|
|
48
|
+
} catch {
|
|
49
|
+
res.status(500).json({ success: false, message: "Failed to retrieve sessions" });
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async revokeSession(req: Request, res: Response) {
|
|
54
|
+
try {
|
|
55
|
+
const actions = (req as any).adminActions;
|
|
56
|
+
if (!actions) {
|
|
57
|
+
return res.status(503).json({ success: false, message: "Session management unavailable" });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await actions.revokeSession(req.params.userId, req.params.sessionId);
|
|
61
|
+
res.json({ success: true, message: "Session revoked" });
|
|
62
|
+
} catch {
|
|
63
|
+
res.status(500).json({ success: false, message: "Failed to revoke session" });
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async revokeAllSessions(req: Request, res: Response) {
|
|
68
|
+
try {
|
|
69
|
+
const actions = (req as any).adminActions;
|
|
70
|
+
if (!actions) {
|
|
71
|
+
return res.status(503).json({ success: false, message: "Session management unavailable" });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await actions.revokeAllSessions(req.params.userId);
|
|
75
|
+
res.json({ success: true, message: "All sessions revoked" });
|
|
76
|
+
} catch {
|
|
77
|
+
res.status(500).json({ success: false, message: "Failed to revoke sessions" });
|
|
78
|
+
}
|
|
79
|
+
},
|
|
32
80
|
});
|
|
@@ -11,6 +11,9 @@ export const createBaseRoutes = (auth: any) => {
|
|
|
11
11
|
router.post("/refresh", controller.refresh);
|
|
12
12
|
|
|
13
13
|
router.get("/admin", auth.requireAdmin, controller.admin);
|
|
14
|
+
router.get("/admin/sessions/:userId", auth.requireAdmin, controller.listSessions);
|
|
15
|
+
router.delete("/admin/sessions/:userId/:sessionId", auth.requireAdmin, controller.revokeSession);
|
|
16
|
+
router.delete("/admin/sessions/:userId", auth.requireAdmin, controller.revokeAllSessions);
|
|
14
17
|
|
|
15
18
|
return router;
|
|
16
19
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import dotenv from "dotenv";
|
|
2
2
|
import { createAuthenik8 } from "authenik8-core";
|
|
3
3
|
import { createApp } from "../app";
|
|
4
|
+
import { requiredSecret } from "../utils/security";
|
|
4
5
|
|
|
5
6
|
dotenv.config();
|
|
6
7
|
|
|
7
8
|
async function start() {
|
|
8
9
|
const auth = await createAuthenik8({
|
|
9
|
-
jwtSecret:
|
|
10
|
-
refreshSecret:
|
|
10
|
+
jwtSecret: requiredSecret("JWT_SECRET"),
|
|
11
|
+
refreshSecret: requiredSecret("REFRESH_SECRET"),
|
|
11
12
|
});
|
|
12
13
|
|
|
13
14
|
const app = createApp(auth);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const sensitiveKeys = new Set(["token", "accessToken", "refreshToken"]);
|
|
2
|
+
|
|
3
|
+
export function requiredSecret(name: string): string {
|
|
4
|
+
const value = process.env[name];
|
|
5
|
+
|
|
6
|
+
if (typeof value !== "string" || value.trim().length < 32) {
|
|
7
|
+
throw new Error(`${name} must be set to at least 32 characters`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getBearerToken(authorizationHeader: string | undefined): string | undefined {
|
|
14
|
+
const [scheme, token] = authorizationHeader?.split(" ") ?? [];
|
|
15
|
+
return scheme === "Bearer" && token ? token : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseRefreshToken(body: unknown) {
|
|
19
|
+
if (!body || typeof body !== "object") {
|
|
20
|
+
throw new Error("Refresh token is required");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { refreshToken } = body as { refreshToken?: unknown };
|
|
24
|
+
|
|
25
|
+
if (typeof refreshToken !== "string" || refreshToken.trim().length < 16) {
|
|
26
|
+
throw new Error("Refresh token is required");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return refreshToken;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sanitizeSessionResponse(value: unknown): unknown {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value.map(sanitizeSessionResponse);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!value || typeof value !== "object") {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Object.fromEntries(
|
|
42
|
+
Object.entries(value as Record<string, unknown>)
|
|
43
|
+
.filter(([key]) => !sensitiveKeys.has(key))
|
|
44
|
+
.map(([key, nestedValue]) => [key, sanitizeSessionResponse(nestedValue)]),
|
|
45
|
+
);
|
|
46
|
+
}
|