create-authenik8-app 2.4.12 → 2.4.14

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.
Files changed (235) hide show
  1. package/README.md +160 -57
  2. package/assets/logo.svg +53 -0
  3. package/dist/src/bin/index.js +199 -125
  4. package/dist/src/bin/index.js.map +1 -1
  5. package/dist/src/lib/args.d.ts +18 -0
  6. package/dist/src/lib/args.d.ts.map +1 -0
  7. package/dist/src/lib/args.js +82 -0
  8. package/dist/src/lib/args.js.map +1 -0
  9. package/dist/src/lib/constants.d.ts +1 -0
  10. package/dist/src/lib/constants.d.ts.map +1 -1
  11. package/dist/src/lib/constants.js +10 -11
  12. package/dist/src/lib/constants.js.map +1 -1
  13. package/dist/src/lib/preflight.d.ts +3 -0
  14. package/dist/src/lib/preflight.d.ts.map +1 -0
  15. package/dist/src/lib/preflight.js +21 -0
  16. package/dist/src/lib/preflight.js.map +1 -0
  17. package/dist/src/lib/process.d.ts +6 -2
  18. package/dist/src/lib/process.d.ts.map +1 -1
  19. package/dist/src/lib/process.js +66 -8
  20. package/dist/src/lib/process.js.map +1 -1
  21. package/dist/src/lib/projectName.d.ts +2 -0
  22. package/dist/src/lib/projectName.d.ts.map +1 -0
  23. package/dist/src/lib/projectName.js +6 -0
  24. package/dist/src/lib/projectName.js.map +1 -0
  25. package/dist/src/lib/schemas.d.ts +192 -0
  26. package/dist/src/lib/schemas.d.ts.map +1 -0
  27. package/dist/src/lib/schemas.js +171 -0
  28. package/dist/src/lib/schemas.js.map +1 -0
  29. package/dist/src/lib/state.d.ts.map +1 -1
  30. package/dist/src/lib/state.js +20 -3
  31. package/dist/src/lib/state.js.map +1 -1
  32. package/dist/src/lib/types.d.ts +6 -13
  33. package/dist/src/lib/types.d.ts.map +1 -1
  34. package/dist/src/lib/ui.d.ts +8 -3
  35. package/dist/src/lib/ui.d.ts.map +1 -1
  36. package/dist/src/lib/ui.js +76 -112
  37. package/dist/src/lib/ui.js.map +1 -1
  38. package/dist/src/steps/configurePrisma.d.ts.map +1 -1
  39. package/dist/src/steps/configurePrisma.js +83 -9
  40. package/dist/src/steps/configurePrisma.js.map +1 -1
  41. package/dist/src/steps/createProject.d.ts +2 -1
  42. package/dist/src/steps/createProject.d.ts.map +1 -1
  43. package/dist/src/steps/createProject.js +148 -27
  44. package/dist/src/steps/createProject.js.map +1 -1
  45. package/dist/src/steps/finalSetup.d.ts +3 -3
  46. package/dist/src/steps/finalSetup.d.ts.map +1 -1
  47. package/dist/src/steps/finalSetup.js +24 -28
  48. package/dist/src/steps/finalSetup.js.map +1 -1
  49. package/dist/src/steps/installAuth.d.ts +1 -1
  50. package/dist/src/steps/installAuth.d.ts.map +1 -1
  51. package/dist/src/steps/installAuth.js +5 -4
  52. package/dist/src/steps/installAuth.js.map +1 -1
  53. package/dist/src/steps/installDeps.d.ts +11 -4
  54. package/dist/src/steps/installDeps.d.ts.map +1 -1
  55. package/dist/src/steps/installDeps.js +45 -77
  56. package/dist/src/steps/installDeps.js.map +1 -1
  57. package/dist/src/steps/prompts.d.ts +1 -1
  58. package/dist/src/steps/prompts.d.ts.map +1 -1
  59. package/dist/src/steps/prompts.js +42 -30
  60. package/dist/src/steps/prompts.js.map +1 -1
  61. package/dist/src/utils/jwk.d.ts +2 -0
  62. package/dist/src/utils/jwk.d.ts.map +1 -0
  63. package/dist/src/utils/jwk.js +44 -0
  64. package/dist/src/utils/jwk.js.map +1 -0
  65. package/dist/src/utils/output.d.ts +1 -1
  66. package/dist/src/utils/output.d.ts.map +1 -1
  67. package/dist/src/utils/output.js +75 -68
  68. package/dist/src/utils/output.js.map +1 -1
  69. package/package.json +17 -9
  70. package/templates/AGENT_IDENTITY.md +112 -0
  71. package/templates/THREAT_MODEL.md +27 -5
  72. package/templates/express-auth/README.md +13 -10
  73. package/templates/express-auth/docker-compose.yml +10 -0
  74. package/templates/express-auth/gitignore.template +6 -0
  75. package/templates/express-auth/package.json +12 -5
  76. package/templates/express-auth/src/app.ts +2 -0
  77. package/templates/express-auth/src/controllers/auth.controller.ts +2 -6
  78. package/templates/express-auth/src/controllers/protected.controller.ts +25 -10
  79. package/templates/express-auth/src/middleware/auth.middleware.ts +1 -0
  80. package/templates/express-auth/src/routes/protected.routes.ts +3 -1
  81. package/templates/express-auth/src/server.ts +6 -12
  82. package/templates/express-auth/src/utils/security.ts +119 -32
  83. package/templates/express-auth+/README.md +13 -10
  84. package/templates/express-auth+/docker-compose.yml +10 -0
  85. package/templates/express-auth+/gitignore.template +6 -0
  86. package/templates/express-auth+/package.json +12 -5
  87. package/templates/express-auth+/src/auth/auth.ts +3 -2
  88. package/templates/express-auth+/src/auth/controllers/password.controller.ts +3 -7
  89. package/templates/express-auth+/src/auth/controllers/protected.controller.ts +26 -11
  90. package/templates/express-auth+/src/auth/identity.adapter.ts +90 -0
  91. package/templates/express-auth+/src/auth/middleware/auth.middleware.ts +3 -4
  92. package/templates/express-auth+/src/auth/routes/protected.routes.ts +2 -2
  93. package/templates/express-auth+/src/oauth-providers/github/src/auth/auth.ts +3 -2
  94. package/templates/express-auth+/src/oauth-providers/google/src/auth/auth.ts +3 -2
  95. package/templates/express-auth+/src/oauth-providers/google-github/src/auth/auth.ts +3 -2
  96. package/templates/express-auth+/src/server.ts +5 -10
  97. package/templates/express-auth+/src/utils/security.ts +109 -28
  98. package/templates/express-base/README.md +13 -6
  99. package/templates/express-base/app.ts +2 -0
  100. package/templates/express-base/controllers/base.controller.ts +33 -19
  101. package/templates/express-base/docker-compose.yml +10 -0
  102. package/templates/express-base/gitignore.template +6 -0
  103. package/templates/express-base/package.json +11 -5
  104. package/templates/express-base/routes/base.routes.ts +1 -1
  105. package/templates/express-base/src/.env.example +7 -2
  106. package/templates/express-base/src/server.ts +6 -12
  107. package/templates/express-base/tsconfig.json +7 -40
  108. package/templates/express-base/utils/security.ts +111 -15
  109. package/templates/fullstack/.env.example +24 -0
  110. package/templates/fullstack/PRESET_CONTRACT.md +46 -0
  111. package/templates/fullstack/README.md +65 -0
  112. package/templates/fullstack/THREAT_MODEL.md +30 -0
  113. package/templates/fullstack/apps/api/package.json +48 -0
  114. package/templates/fullstack/apps/api/prisma/schema.prisma +124 -0
  115. package/templates/fullstack/apps/api/prisma/seed.ts +36 -0
  116. package/templates/fullstack/apps/api/prisma.config.ts +18 -0
  117. package/templates/fullstack/apps/api/src/app.ts +68 -0
  118. package/templates/fullstack/apps/api/src/auth/auth.controller.ts +125 -0
  119. package/templates/fullstack/apps/api/src/auth/auth.routes.ts +50 -0
  120. package/templates/fullstack/apps/api/src/auth/auth.service.ts +257 -0
  121. package/templates/fullstack/apps/api/src/auth/authenik8.ts +67 -0
  122. package/templates/fullstack/apps/api/src/auth/cookies.ts +36 -0
  123. package/templates/fullstack/apps/api/src/config/env.ts +72 -0
  124. package/templates/fullstack/apps/api/src/config/logger.ts +17 -0
  125. package/templates/fullstack/apps/api/src/config/mailer.ts +32 -0
  126. package/templates/fullstack/apps/api/src/config/origins.ts +27 -0
  127. package/templates/fullstack/apps/api/src/config/prisma.ts +9 -0
  128. package/templates/fullstack/apps/api/src/middleware/authenticate.ts +35 -0
  129. package/templates/fullstack/apps/api/src/middleware/csrf.ts +60 -0
  130. package/templates/fullstack/apps/api/src/middleware/origin.ts +12 -0
  131. package/templates/fullstack/apps/api/src/middleware/request-id.ts +9 -0
  132. package/templates/fullstack/apps/api/src/modules/admin/admin.controller.ts +17 -0
  133. package/templates/fullstack/apps/api/src/modules/admin/admin.routes.ts +14 -0
  134. package/templates/fullstack/apps/api/src/modules/admin/admin.service.ts +54 -0
  135. package/templates/fullstack/apps/api/src/modules/projects/project.controller.ts +22 -0
  136. package/templates/fullstack/apps/api/src/modules/projects/project.policy.ts +14 -0
  137. package/templates/fullstack/apps/api/src/modules/projects/project.repository.ts +20 -0
  138. package/templates/fullstack/apps/api/src/modules/projects/project.routes.ts +21 -0
  139. package/templates/fullstack/apps/api/src/modules/projects/project.service.ts +41 -0
  140. package/templates/fullstack/apps/api/src/modules/users/user.controller.ts +26 -0
  141. package/templates/fullstack/apps/api/src/modules/users/user.presenter.ts +13 -0
  142. package/templates/fullstack/apps/api/src/modules/users/user.routes.ts +21 -0
  143. package/templates/fullstack/apps/api/src/modules/users/user.service.ts +71 -0
  144. package/templates/fullstack/apps/api/src/openapi.ts +32 -0
  145. package/templates/fullstack/apps/api/src/server.ts +29 -0
  146. package/templates/fullstack/apps/api/src/types.ts +15 -0
  147. package/templates/fullstack/apps/api/src/utils/crypto.ts +4 -0
  148. package/templates/fullstack/apps/api/src/utils/http.ts +33 -0
  149. package/templates/fullstack/apps/api/src/utils/sealed-value.ts +37 -0
  150. package/templates/fullstack/apps/api/tests/project.policy.test.ts +26 -0
  151. package/templates/fullstack/apps/api/tests/registration.test.ts +69 -0
  152. package/templates/fullstack/apps/api/tests/security.test.ts +98 -0
  153. package/templates/fullstack/apps/api/tests/validation.test.ts +37 -0
  154. package/templates/fullstack/apps/api/tsconfig.json +16 -0
  155. package/templates/fullstack/apps/api/vitest.config.ts +9 -0
  156. package/templates/fullstack/apps/web/index.html +14 -0
  157. package/templates/fullstack/apps/web/package.json +31 -0
  158. package/templates/fullstack/apps/web/public/authenik8-logo.svg +53 -0
  159. package/templates/fullstack/apps/web/src/App.tsx +45 -0
  160. package/templates/fullstack/apps/web/src/auth/AuthProvider.tsx +53 -0
  161. package/templates/fullstack/apps/web/src/auth/RouteGuards.tsx +16 -0
  162. package/templates/fullstack/apps/web/src/auth/providers.ts +3 -0
  163. package/templates/fullstack/apps/web/src/components/AppShell.tsx +80 -0
  164. package/templates/fullstack/apps/web/src/components/AuthShell.tsx +26 -0
  165. package/templates/fullstack/apps/web/src/components/Page.tsx +30 -0
  166. package/templates/fullstack/apps/web/src/features/projects/ProjectPages.tsx +79 -0
  167. package/templates/fullstack/apps/web/src/main.tsx +24 -0
  168. package/templates/fullstack/apps/web/src/pages/DashboardPage.tsx +48 -0
  169. package/templates/fullstack/apps/web/src/pages/StatusPages.tsx +8 -0
  170. package/templates/fullstack/apps/web/src/pages/admin/AuditPage.tsx +19 -0
  171. package/templates/fullstack/apps/web/src/pages/admin/UsersPage.tsx +24 -0
  172. package/templates/fullstack/apps/web/src/pages/auth/LoginPage.tsx +51 -0
  173. package/templates/fullstack/apps/web/src/pages/auth/OAuthCallbackPage.tsx +26 -0
  174. package/templates/fullstack/apps/web/src/pages/auth/RecoveryPages.tsx +53 -0
  175. package/templates/fullstack/apps/web/src/pages/auth/RegisterPage.tsx +86 -0
  176. package/templates/fullstack/apps/web/src/pages/auth/VerifyEmailPage.tsx +24 -0
  177. package/templates/fullstack/apps/web/src/pages/settings/ProfilePage.tsx +34 -0
  178. package/templates/fullstack/apps/web/src/pages/settings/SecurityPage.tsx +57 -0
  179. package/templates/fullstack/apps/web/src/styles.css +289 -0
  180. package/templates/fullstack/apps/web/src/token-storage.test.ts +12 -0
  181. package/templates/fullstack/apps/web/tsconfig.app.json +19 -0
  182. package/templates/fullstack/apps/web/tsconfig.json +7 -0
  183. package/templates/fullstack/apps/web/tsconfig.node.json +10 -0
  184. package/templates/fullstack/apps/web/vite.config.ts +18 -0
  185. package/templates/fullstack/docker-compose.yml +35 -0
  186. package/templates/fullstack/docs/PRODUCTION.md +30 -0
  187. package/templates/fullstack/gitignore.template +8 -0
  188. package/templates/fullstack/package.json +39 -0
  189. package/templates/fullstack/packages/api-client/package.json +20 -0
  190. package/templates/fullstack/packages/api-client/src/index.ts +203 -0
  191. package/templates/fullstack/packages/api-client/tsconfig.json +14 -0
  192. package/templates/fullstack/packages/contracts/package.json +20 -0
  193. package/templates/fullstack/packages/contracts/src/index.ts +146 -0
  194. package/templates/fullstack/packages/contracts/tsconfig.json +13 -0
  195. package/templates/fullstack/packages/ui/package.json +22 -0
  196. package/templates/fullstack/packages/ui/src/index.tsx +33 -0
  197. package/templates/fullstack/packages/ui/tsconfig.json +14 -0
  198. package/templates/prisma/postgresql/.env.example +6 -2
  199. package/templates/prisma/postgresql/client.ts +17 -0
  200. package/templates/prisma/postgresql/prisma.config.ts +12 -0
  201. package/templates/prisma/postgresql/schema.prisma +0 -1
  202. package/templates/prisma/sqlite/.env.example +6 -2
  203. package/templates/prisma/sqlite/client.ts +16 -0
  204. package/templates/prisma/sqlite/prisma.config.ts +12 -0
  205. package/templates/prisma/sqlite/schema.prisma +0 -1
  206. package/dist/src/tests/templated-routes.test.d.ts +0 -2
  207. package/dist/src/tests/templated-routes.test.d.ts.map +0 -1
  208. package/dist/src/tests/templated-routes.test.js +0 -325
  209. package/dist/src/tests/templated-routes.test.js.map +0 -1
  210. package/dist/templates/THREAT_MODEL.md +0 -138
  211. package/dist/templates/express-auth/src/controllers/protected.controller.d.ts +0 -8
  212. package/dist/templates/express-auth/src/controllers/protected.controller.d.ts.map +0 -1
  213. package/dist/templates/express-auth/src/controllers/protected.controller.js +0 -50
  214. package/dist/templates/express-auth/src/controllers/protected.controller.js.map +0 -1
  215. package/dist/templates/express-auth/src/routes/protected.routes.d.ts +0 -2
  216. package/dist/templates/express-auth/src/routes/protected.routes.d.ts.map +0 -1
  217. package/dist/templates/express-auth/src/routes/protected.routes.js +0 -16
  218. package/dist/templates/express-auth/src/routes/protected.routes.js.map +0 -1
  219. package/dist/templates/express-auth/src/utils/security.d.ts +0 -8
  220. package/dist/templates/express-auth/src/utils/security.d.ts.map +0 -1
  221. package/dist/templates/express-auth/src/utils/security.js +0 -55
  222. package/dist/templates/express-auth/src/utils/security.js.map +0 -1
  223. package/dist/templates/express-base/controllers/base.controller.d.ts +0 -12
  224. package/dist/templates/express-base/controllers/base.controller.d.ts.map +0 -1
  225. package/dist/templates/express-base/controllers/base.controller.js +0 -77
  226. package/dist/templates/express-base/controllers/base.controller.js.map +0 -1
  227. package/dist/templates/express-base/routes/base.routes.d.ts +0 -2
  228. package/dist/templates/express-base/routes/base.routes.d.ts.map +0 -1
  229. package/dist/templates/express-base/routes/base.routes.js +0 -20
  230. package/dist/templates/express-base/routes/base.routes.js.map +0 -1
  231. package/dist/templates/express-base/utils/security.d.ts +0 -5
  232. package/dist/templates/express-base/utils/security.d.ts.map +0 -1
  233. package/dist/templates/express-base/utils/security.js +0 -40
  234. package/dist/templates/express-base/utils/security.js.map +0 -1
  235. package/dist/templates/package.json +0 -27
@@ -1,49 +1,130 @@
1
+ import { z } from "zod";
2
+ import type { AgentIdentityConfig, Authenik8JwkConfig } from "authenik8-core";
3
+
1
4
  const sensitiveKeys = new Set(["token", "accessToken", "refreshToken"]);
2
5
 
3
- export function requiredSecret(name: string): string {
4
- const value = process.env[name];
6
+ const passwordSchema = z.string()
7
+ .min(8, "Password must be between 8 and 1024 characters")
8
+ .max(1024, "Password must be between 8 and 1024 characters")
9
+ .refine((password) => !/[\u0000-\u001f\u007f]/.test(password), "Password contains unsupported control characters");
10
+
11
+ export const credentialsSchema = z.strictObject({
12
+ email: z.string().trim().toLowerCase().email("A valid email is required").max(254),
13
+ password: passwordSchema,
14
+ });
15
+
16
+ const secretSchema = z.string().trim().min(32);
17
+ const environmentValueSchema = z.string().trim().min(1);
18
+ const portSchema = z.coerce.number().int().min(1).max(65535);
19
+ const signingJwkSchema = z.object({
20
+ kty: z.literal("EC"),
21
+ crv: z.literal("P-256"),
22
+ x: z.string().min(1),
23
+ y: z.string().min(1),
24
+ d: z.string().min(1).optional(),
25
+ kid: z.string().min(1),
26
+ alg: z.literal("ES256").optional(),
27
+ use: z.literal("sig").optional(),
28
+ });
29
+ const identifierSchema = z.string()
30
+ .trim()
31
+ .min(1)
32
+ .max(128)
33
+ .regex(/^[A-Za-z0-9._:-]+$/);
34
+ const agentIdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/);
35
+ const agentScopeSchema = z.string().max(128).regex(/^[a-z][a-z0-9._/-]*(?::[a-z][a-z0-9._/-]*)+$/);
36
+ const agentRegistrySchema = z.record(
37
+ agentIdSchema,
38
+ z.array(agentScopeSchema).min(1).max(64),
39
+ );
40
+
41
+ export class InputValidationError extends Error {}
42
+
43
+ function validationMessage(error: z.ZodError, fallback: string): string {
44
+ return error.issues[0]?.message ?? fallback;
45
+ }
5
46
 
6
- if (typeof value !== "string" || value.trim().length < 32) {
7
- throw new Error(`${name} must be set to at least 32 characters`);
47
+ export function requiredSecret(name: string): string {
48
+ const result = secretSchema.safeParse(process.env[name]);
49
+ if (!result.success) {
50
+ throw new InputValidationError(`${name} must be set to at least 32 characters`);
8
51
  }
9
-
10
- return value;
52
+ return result.data;
11
53
  }
12
54
 
13
55
  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
- }
56
+ const result = environmentValueSchema.safeParse(process.env[name]);
57
+ if (!result.success) throw new InputValidationError(`${name} must be set`);
58
+ return result.data;
59
+ }
19
60
 
20
- return value;
61
+ export function requiredPort(): number {
62
+ const result = portSchema.safeParse(process.env.PORT ?? 3000);
63
+ if (!result.success) throw new InputValidationError("PORT must be between 1 and 65535");
64
+ return result.data;
21
65
  }
22
66
 
23
- export function parseCredentials(body: unknown) {
24
- if (!body || typeof body !== "object") {
25
- throw new Error("Email and password are required");
67
+ export function authJwkConfig(): Authenik8JwkConfig {
68
+ let parsed: unknown;
69
+ try {
70
+ parsed = JSON.parse(requiredEnv("AUTHENIK8_SIGNING_JWKS"));
71
+ } catch {
72
+ throw new InputValidationError("AUTHENIK8_SIGNING_JWKS must be a valid JSON array");
26
73
  }
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");
74
+ const result = z.array(signingJwkSchema).min(1).safeParse(parsed);
75
+ if (!result.success) {
76
+ throw new InputValidationError("AUTHENIK8_SIGNING_JWKS must contain ES256 P-256 JWKs");
32
77
  }
78
+ const activeKid = requiredEnv("AUTHENIK8_ACTIVE_KID");
79
+ const activeKey = result.data.find((key) => key.kid === activeKid);
80
+ if (!activeKey?.d) throw new InputValidationError("AUTHENIK8_ACTIVE_KID must select a private signing JWK");
81
+ return {
82
+ keys: result.data,
83
+ activeKid,
84
+ issuer: requiredEnv("AUTHENIK8_ISSUER"),
85
+ audience: requiredEnv("AUTHENIK8_AUDIENCE"),
86
+ };
87
+ }
33
88
 
34
- const normalizedEmail = email.trim().toLowerCase();
35
- const atIndex = normalizedEmail.indexOf("@");
36
- const dotIndex = normalizedEmail.lastIndexOf(".");
89
+ export function agentIdentityConfig(): AgentIdentityConfig | undefined {
90
+ const source = process.env.AUTHENIK8_AGENTS?.trim();
91
+ if (!source) return undefined;
37
92
 
38
- if (atIndex < 1 || atIndex !== normalizedEmail.lastIndexOf("@") || dotIndex < atIndex + 2 || dotIndex === normalizedEmail.length - 1) {
39
- throw new Error("A valid email is required");
93
+ let parsed: unknown;
94
+ try {
95
+ parsed = JSON.parse(source);
96
+ } catch {
97
+ throw new InputValidationError("AUTHENIK8_AGENTS must be a JSON object of agent scope arrays");
40
98
  }
99
+ const result = agentRegistrySchema.safeParse(parsed);
100
+ if (!result.success) {
101
+ throw new InputValidationError("AUTHENIK8_AGENTS must map valid agent IDs to resource:action scopes");
102
+ }
103
+ if (!Object.keys(result.data).length) return undefined;
104
+
105
+ const registry = result.data;
106
+ return {
107
+ resolveAgent: async (agentId) => {
108
+ const scopes = Object.prototype.hasOwnProperty.call(registry, agentId)
109
+ ? registry[agentId]
110
+ : undefined;
111
+ return scopes ? { agentId, scopes, active: true } : null;
112
+ },
113
+ };
114
+ }
41
115
 
42
- if (password.length < 8 || password.length > 1024) {
43
- throw new Error("Password must be between 8 and 1024 characters");
116
+ export function parseCredentials(body: unknown): z.infer<typeof credentialsSchema> {
117
+ const result = credentialsSchema.safeParse(body);
118
+ if (!result.success) {
119
+ throw new InputValidationError(validationMessage(result.error, "Email and password are required"));
44
120
  }
121
+ return result.data;
122
+ }
45
123
 
46
- return { email: normalizedEmail, password };
124
+ export function parseIdentifier(value: unknown, label: string): string {
125
+ const result = identifierSchema.safeParse(value);
126
+ if (!result.success) throw new InputValidationError(`${label} is invalid`);
127
+ return result.data;
47
128
  }
48
129
 
49
130
  export function sanitizeSessionResponse(value: unknown): unknown {
@@ -15,14 +15,18 @@ For SQLite, Postgres in `docker-compose.yml` is optional. Redis is required for
15
15
 
16
16
  ## Environment
17
17
 
18
- Review `.env` before running. The generated secrets are development placeholders and must be replaced before deployment.
18
+ The generator writes a unique signing key and refresh secret to git-ignored `.env`. Move them into your deployment secret manager and never commit the private JWK.
19
19
 
20
20
  Required:
21
21
 
22
22
  ```bash
23
23
  DATABASE_URL=file:./dev.db
24
- JWT_SECRET=dev-jwt-secret-change-before-production-123456
25
- REFRESH_SECRET=dev-refresh-secret-change-before-production-123456
24
+ AUTHENIK8_SIGNING_JWKS='[{"kty":"EC","crv":"P-256","kid":"<generated>","x":"...","y":"...","d":"...","alg":"ES256"}]'
25
+ AUTHENIK8_ACTIVE_KID=<generated>
26
+ AUTHENIK8_ISSUER=http://localhost:3000
27
+ AUTHENIK8_AUDIENCE=your-app-api
28
+ AUTHENIK8_AGENTS={}
29
+ REFRESH_SECRET=<generated-random-secret>
26
30
  REDIS_HOST=127.0.0.1
27
31
  REDIS_PORT=6379
28
32
  ```
@@ -69,7 +73,10 @@ Expected result: the route should reject the request because no access token was
69
73
  ## Environment Variables
70
74
 
71
75
  - `DATABASE_URL`: Prisma database connection. SQLite uses `file:./dev.db`; Postgres uses a `postgresql://...` URL.
72
- - `JWT_SECRET`: signs short-lived access tokens. Use a long random value in production.
76
+ - `AUTHENIK8_SIGNING_JWKS`: JSON array containing the active private ES256 key and any previous public verification keys.
77
+ - `AUTHENIK8_ACTIVE_KID`: selects the private key used to sign new access tokens.
78
+ - `AUTHENIK8_ISSUER` / `AUTHENIK8_AUDIENCE`: required claims checked during verification.
79
+ - `AUTHENIK8_AGENTS`: optional JSON map of agent IDs to exact `resource:action` scopes; keep `{}` to disable agent identity. See `AGENT_IDENTITY.md`.
73
80
  - `REFRESH_SECRET`: signs refresh tokens. Use a different long random value in production.
74
81
  - `REDIS_HOST`: Redis host for refresh-token/session security.
75
82
  - `REDIS_PORT`: Redis port, usually `6379` locally.
@@ -98,9 +105,9 @@ export async function getProtected(accessToken: string) {
98
105
 
99
106
  `Prisma Client did not initialize`: run `npm run prisma:migrate`, then restart `npm run dev`.
100
107
 
101
- `JWT_SECRET must be set to at least 32 characters`: check `.env`; both token secrets must be long strings.
108
+ `AUTHENIK8_SIGNING_JWKS must be a valid JSON array`: restore the generated `.env` value or provide an ES256 P-256 key ring and matching `AUTHENIK8_ACTIVE_KID`.
102
109
 
103
- `Port 3000 already in use`: stop the other process or change the `app.listen(3000)` port in `src/server.ts`.
110
+ `Port 3000 already in use`: stop the other process or set `PORT` in `.env`.
104
111
 
105
112
  `DATABASE_URL is wrong`: for SQLite use `file:./dev.db`; for local Docker Postgres use `postgresql://postgres:postgres@localhost:5432/authenik8?schema=public`.
106
113
 
@@ -9,6 +9,8 @@ export const createApp = (auth: any) => {
9
9
  app.use(auth.helmet);
10
10
  app.use(auth.rateLimit);
11
11
 
12
+ app.get("/.well-known/jwks.json", (_req, res) => res.json(auth.getJwks()));
13
+
12
14
  app.use("/", createBaseRoutes(auth));
13
15
 
14
16
  return app;
@@ -1,5 +1,10 @@
1
1
  import { Request, Response } from "express";
2
- import { getBearerToken, parseRefreshToken, sanitizeSessionResponse } from "../utils/security";
2
+ import {
3
+ InputValidationError,
4
+ parseIdentifier,
5
+ parseRefreshToken,
6
+ sanitizeSessionResponse,
7
+ } from "../utils/security";
3
8
 
4
9
  export const createBaseController = (auth: any) => ({
5
10
  publicRoute(req: Request, res: Response) {
@@ -11,15 +16,8 @@ export const createBaseController = (auth: any) => ({
11
16
  res.json({ token });
12
17
  },
13
18
 
14
- async protected(req: Request, res: Response) {
15
- const token = getBearerToken(req.headers.authorization);
16
-
17
- try {
18
- const decoded = await auth.verifyToken(token);
19
- res.json({ message: "Protected data", user: decoded });
20
- } catch {
21
- res.status(401).json({ error: "Unauthorized" });
22
- }
19
+ protected(req: Request, res: Response) {
20
+ res.json({ message: "Protected data", user: (req as any).user });
23
21
  },
24
22
 
25
23
  async refresh(req: Request, res: Response) {
@@ -43,10 +41,15 @@ export const createBaseController = (auth: any) => ({
43
41
  return res.status(503).json({ success: false, message: "Session management unavailable" });
44
42
  }
45
43
 
46
- const sessions = await actions.listSessions(req.params.userId);
44
+ const userId = parseIdentifier(req.params.userId, "User ID");
45
+ const sessions = await actions.listSessions(userId);
47
46
  res.json({ sessions: sanitizeSessionResponse(sessions) });
48
- } catch {
49
- res.status(500).json({ success: false, message: "Failed to retrieve sessions" });
47
+ } catch (error) {
48
+ const validationError = error instanceof InputValidationError;
49
+ res.status(validationError ? 400 : 500).json({
50
+ success: false,
51
+ message: validationError ? error.message : "Failed to retrieve sessions",
52
+ });
50
53
  }
51
54
  },
52
55
 
@@ -57,10 +60,16 @@ export const createBaseController = (auth: any) => ({
57
60
  return res.status(503).json({ success: false, message: "Session management unavailable" });
58
61
  }
59
62
 
60
- await actions.revokeSession(req.params.userId, req.params.sessionId);
63
+ const userId = parseIdentifier(req.params.userId, "User ID");
64
+ const sessionId = parseIdentifier(req.params.sessionId, "Session ID");
65
+ await actions.revokeSession(userId, sessionId);
61
66
  res.json({ success: true, message: "Session revoked" });
62
- } catch {
63
- res.status(500).json({ success: false, message: "Failed to revoke session" });
67
+ } catch (error) {
68
+ const validationError = error instanceof InputValidationError;
69
+ res.status(validationError ? 400 : 500).json({
70
+ success: false,
71
+ message: validationError ? error.message : "Failed to revoke session",
72
+ });
64
73
  }
65
74
  },
66
75
 
@@ -71,10 +80,15 @@ export const createBaseController = (auth: any) => ({
71
80
  return res.status(503).json({ success: false, message: "Session management unavailable" });
72
81
  }
73
82
 
74
- await actions.revokeAllSessions(req.params.userId);
83
+ const userId = parseIdentifier(req.params.userId, "User ID");
84
+ await actions.revokeAllSessions(userId);
75
85
  res.json({ success: true, message: "All sessions revoked" });
76
- } catch {
77
- res.status(500).json({ success: false, message: "Failed to revoke sessions" });
86
+ } catch (error) {
87
+ const validationError = error instanceof InputValidationError;
88
+ res.status(validationError ? 400 : 500).json({
89
+ success: false,
90
+ message: validationError ? error.message : "Failed to revoke sessions",
91
+ });
78
92
  }
79
93
  },
80
94
  });
@@ -6,6 +6,11 @@ services:
6
6
  command: ["redis-server", "--appendonly", "yes"]
7
7
  volumes:
8
8
  - redis-data:/data
9
+ healthcheck:
10
+ test: ["CMD", "redis-cli", "ping"]
11
+ interval: 5s
12
+ timeout: 3s
13
+ retries: 10
9
14
 
10
15
  postgres:
11
16
  image: postgres:16-alpine
@@ -17,6 +22,11 @@ services:
17
22
  POSTGRES_DB: authenik8
18
23
  volumes:
19
24
  - postgres-data:/var/lib/postgresql/data
25
+ healthcheck:
26
+ test: ["CMD-SHELL", "pg_isready -U postgres -d authenik8"]
27
+ interval: 5s
28
+ timeout: 3s
29
+ retries: 10
20
30
 
21
31
  volumes:
22
32
  redis-data:
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ *.log
5
+ coverage/
6
+ .DS_Store
@@ -3,22 +3,28 @@
3
3
  "scripts": {
4
4
  "dev": "ts-node-dev --respawn --transpile-only ./src/server.ts",
5
5
  "build": "tsc",
6
- "start": "node dist/server.js",
6
+ "start": "node dist/src/server.js",
7
7
  "prisma:migrate": "prisma migrate",
8
- "docker:up": "docker compose up -d",
8
+ "docker:up": "docker compose up -d --wait",
9
9
  "docker:down": "docker compose down"
10
10
  },
11
11
  "dependencies": {
12
- "authenik8-core": "^1.0.38",
12
+ "authenik8-core": "2.0.3",
13
13
  "dotenv": "^16.0.0",
14
14
  "express": "^4.18.2",
15
- "@prisma/client": "5.22.0"
15
+ "@prisma/client": "7.8.0",
16
+ "zod": "^4.4.3"
16
17
  },
17
18
  "devDependencies": {
19
+ "@types/express": "^4.17.21",
20
+ "@types/node": "^22.10.2",
18
21
  "ts-node-dev": "^2.0.0",
19
22
  "typescript": "^5.0.0",
20
- "prisma": "5.22.0"
23
+ "prisma": "7.8.0"
21
24
 
22
25
  },
26
+ "overrides": {
27
+ "@hono/node-server": "1.19.13"
28
+ },
23
29
  "type": "commonjs"
24
30
  }
@@ -7,7 +7,7 @@ export const createBaseRoutes = (auth: any) => {
7
7
 
8
8
  router.get("/public", controller.publicRoute);
9
9
  router.get("/guest", controller.guest);
10
- router.get("/protected", controller.protected);
10
+ router.get("/protected", auth.requireAuth, controller.protected);
11
11
  router.post("/refresh", controller.refresh);
12
12
 
13
13
  router.get("/admin", auth.requireAdmin, controller.admin);
@@ -1,3 +1,8 @@
1
- JWT_SECRET=supersecret
2
- REFRESH_SECRET=refreshsecret
1
+ AUTHENIK8_SIGNING_JWKS=
2
+ AUTHENIK8_ACTIVE_KID=
3
+ AUTHENIK8_ISSUER=http://localhost:3000
4
+ AUTHENIK8_AUDIENCE=authenik8-app-api
5
+ AUTHENIK8_AGENTS={}
6
+ REFRESH_SECRET=replace-with-at-least-32-random-characters
7
+ PORT=3000
3
8
  REDIS_URL=redis://localhost:6379
@@ -1,20 +1,22 @@
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
+ import { agentIdentityConfig, authJwkConfig, requiredPort, requiredSecret } from "../utils/security";
5
5
 
6
6
  dotenv.config();
7
7
 
8
8
  async function start() {
9
9
  const auth = await createAuthenik8({
10
- jwtSecret: requiredSecret("JWT_SECRET"),
10
+ jwt: authJwkConfig(),
11
11
  refreshSecret: requiredSecret("REFRESH_SECRET"),
12
+ agent: agentIdentityConfig(),
12
13
  });
13
14
 
14
15
  const app = createApp(auth);
15
16
 
16
- app.listen(3000, () => {
17
- console.log("🚀 Server running on http://localhost:3000");
17
+ const port = requiredPort();
18
+ app.listen(port, () => {
19
+ console.log(`🚀 Server running on http://localhost:${port}`);
18
20
  });
19
21
  }
20
22
  process.on("uncaughtException", (err) => {
@@ -26,12 +28,4 @@ process.on("unhandledRejection", (err) => {
26
28
  console.error(" Unhandled Rejection:", err);
27
29
  process.exit(1);
28
30
  });
29
- setInterval(() => {
30
- const used = process.memoryUsage().heapUsed / 1024 / 1024;
31
-
32
- if (used > 300) {
33
- console.error(`Memory exceeded: ${used.toFixed(2)} MB`);
34
- process.exit(1);
35
- }
36
- }, 10000);
37
31
  start();
@@ -1,47 +1,14 @@
1
1
  {
2
- // Visit https://aka.ms/tsconfig to read more about this file
3
2
  "compilerOptions": {
4
- // File Layout
5
- "rootDir": "./src",
6
- "outDir": "./dist",
7
-
8
- // Environment Settings
9
- // See also https://aka.ms/tsconfig/module
10
- "module": "nodenext",
11
3
  "target": "ES2020",
12
- "types": [],
13
- // For nodejs:
14
- // "lib": ["esnext"],
15
- // "types": ["node"],
16
- // and npm install -D @types/node
17
-
18
- // Other Outputs
19
- "sourceMap": true,
20
- "declaration": true,
21
- "declarationMap": true,
22
-
23
- // Stricter Typechecking Options
24
- "noUncheckedIndexedAccess": true,
25
- "exactOptionalPropertyTypes": true,
26
-
27
- // Style Options
28
- // "noImplicitReturns": true,
29
- // "noImplicitOverride": true,
30
- // "noUnusedLocals": true,
31
- // "noUnusedParameters": true,
32
- // "noFallthroughCasesInSwitch": true,
33
- // "noPropertyAccessFromIndexSignature": true,
34
-
35
- // Recommended Options
4
+ "module": "commonjs",
5
+ "rootDir": ".",
6
+ "outDir": "./dist",
36
7
  "strict": true,
37
- "jsx": "react-jsx",
38
- "verbatimModuleSyntax": true,
39
- "isolatedModules": true,
40
- "noUncheckedSideEffectImports": true,
41
- "moduleDetection": "force",
8
+ "esModuleInterop": true,
42
9
  "skipLibCheck": true,
43
- "esModuleInterop":true,
44
- //"moduleResolution":Nodenext,
10
+ "forceConsistentCasingInFileNames": true
45
11
  },
46
- "exclude":["dist"]
12
+ "include": ["src", "app.ts", "controllers", "routes", "utils"],
13
+ "exclude": ["node_modules", "dist"]
47
14
  }
@@ -1,32 +1,128 @@
1
+ import { z } from "zod";
2
+ import type { AgentIdentityConfig, Authenik8JwkConfig } from "authenik8-core";
3
+
1
4
  const sensitiveKeys = new Set(["token", "accessToken", "refreshToken"]);
2
5
 
3
- export function requiredSecret(name: string): string {
4
- const value = process.env[name];
6
+ const secretSchema = z.string().trim().min(32);
7
+ const environmentValueSchema = z.string().trim().min(1);
8
+ const portSchema = z.coerce.number().int().min(1).max(65535);
9
+ const signingJwkSchema = z.object({
10
+ kty: z.literal("EC"),
11
+ crv: z.literal("P-256"),
12
+ x: z.string().min(1),
13
+ y: z.string().min(1),
14
+ d: z.string().min(1).optional(),
15
+ kid: z.string().min(1),
16
+ alg: z.literal("ES256").optional(),
17
+ use: z.literal("sig").optional(),
18
+ });
19
+ export const refreshTokenBodySchema = z.strictObject({
20
+ refreshToken: z.string().trim().min(16, "Refresh token is required").max(4096),
21
+ });
22
+ const identifierSchema = z.string()
23
+ .trim()
24
+ .min(1)
25
+ .max(128)
26
+ .regex(/^[A-Za-z0-9._:-]+$/);
27
+ const agentIdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/);
28
+ const agentScopeSchema = z.string().max(128).regex(/^[a-z][a-z0-9._/-]*(?::[a-z][a-z0-9._/-]*)+$/);
29
+ const agentRegistrySchema = z.record(
30
+ agentIdSchema,
31
+ z.array(agentScopeSchema).min(1).max(64),
32
+ );
33
+
34
+ export class InputValidationError extends Error {}
35
+
36
+ function validationMessage(error: z.ZodError, fallback: string): string {
37
+ return error.issues[0]?.message ?? fallback;
38
+ }
5
39
 
6
- if (typeof value !== "string" || value.trim().length < 32) {
7
- throw new Error(`${name} must be set to at least 32 characters`);
40
+ export function requiredSecret(name: string): string {
41
+ const result = secretSchema.safeParse(process.env[name]);
42
+ if (!result.success) {
43
+ throw new InputValidationError(`${name} must be set to at least 32 characters`);
8
44
  }
45
+ return result.data;
46
+ }
9
47
 
10
- return value;
48
+ export function requiredEnv(name: string): string {
49
+ const result = environmentValueSchema.safeParse(process.env[name]);
50
+ if (!result.success) throw new InputValidationError(`${name} must be set`);
51
+ return result.data;
11
52
  }
12
53
 
13
- export function getBearerToken(authorizationHeader: string | undefined): string | undefined {
14
- const [scheme, token] = authorizationHeader?.split(" ") ?? [];
15
- return scheme === "Bearer" && token ? token : undefined;
54
+ export function requiredPort(): number {
55
+ const result = portSchema.safeParse(process.env.PORT ?? 3000);
56
+ if (!result.success) throw new InputValidationError("PORT must be between 1 and 65535");
57
+ return result.data;
16
58
  }
17
59
 
18
- export function parseRefreshToken(body: unknown) {
19
- if (!body || typeof body !== "object") {
20
- throw new Error("Refresh token is required");
60
+ export function authJwkConfig(): Authenik8JwkConfig {
61
+ let parsed: unknown;
62
+ try {
63
+ parsed = JSON.parse(requiredEnv("AUTHENIK8_SIGNING_JWKS"));
64
+ } catch {
65
+ throw new InputValidationError("AUTHENIK8_SIGNING_JWKS must be a valid JSON array");
21
66
  }
67
+ const result = z.array(signingJwkSchema).min(1).safeParse(parsed);
68
+ if (!result.success) {
69
+ throw new InputValidationError("AUTHENIK8_SIGNING_JWKS must contain ES256 P-256 JWKs");
70
+ }
71
+ const activeKid = requiredEnv("AUTHENIK8_ACTIVE_KID");
72
+ const activeKey = result.data.find((key) => key.kid === activeKid);
73
+ if (!activeKey?.d) throw new InputValidationError("AUTHENIK8_ACTIVE_KID must select a private signing JWK");
74
+ return {
75
+ keys: result.data,
76
+ activeKid,
77
+ issuer: requiredEnv("AUTHENIK8_ISSUER"),
78
+ audience: requiredEnv("AUTHENIK8_AUDIENCE"),
79
+ };
80
+ }
22
81
 
23
- const { refreshToken } = body as { refreshToken?: unknown };
82
+ export function agentIdentityConfig(): AgentIdentityConfig | undefined {
83
+ const source = process.env.AUTHENIK8_AGENTS?.trim();
84
+ if (!source) return undefined;
24
85
 
25
- if (typeof refreshToken !== "string" || refreshToken.trim().length < 16) {
26
- throw new Error("Refresh token is required");
86
+ let parsed: unknown;
87
+ try {
88
+ parsed = JSON.parse(source);
89
+ } catch {
90
+ throw new InputValidationError("AUTHENIK8_AGENTS must be a JSON object of agent scope arrays");
27
91
  }
92
+ const result = agentRegistrySchema.safeParse(parsed);
93
+ if (!result.success) {
94
+ throw new InputValidationError("AUTHENIK8_AGENTS must map valid agent IDs to resource:action scopes");
95
+ }
96
+ if (!Object.keys(result.data).length) return undefined;
97
+
98
+ const registry = result.data;
99
+ return {
100
+ resolveAgent: async (agentId) => {
101
+ const scopes = Object.prototype.hasOwnProperty.call(registry, agentId)
102
+ ? registry[agentId]
103
+ : undefined;
104
+ return scopes ? { agentId, scopes, active: true } : null;
105
+ },
106
+ };
107
+ }
108
+
109
+ export function getBearerToken(authorizationHeader: string | undefined): string | undefined {
110
+ const result = z.string().regex(/^Bearer\s+\S+$/).safeParse(authorizationHeader);
111
+ return result.success ? result.data.replace(/^Bearer\s+/, "") : undefined;
112
+ }
113
+
114
+ export function parseRefreshToken(body: unknown): string {
115
+ const result = refreshTokenBodySchema.safeParse(body);
116
+ if (!result.success) {
117
+ throw new InputValidationError(validationMessage(result.error, "Refresh token is required"));
118
+ }
119
+ return result.data.refreshToken;
120
+ }
28
121
 
29
- return refreshToken;
122
+ export function parseIdentifier(value: unknown, label: string): string {
123
+ const result = identifierSchema.safeParse(value);
124
+ if (!result.success) throw new InputValidationError(`${label} is invalid`);
125
+ return result.data;
30
126
  }
31
127
 
32
128
  export function sanitizeSessionResponse(value: unknown): unknown {
@@ -0,0 +1,24 @@
1
+ NODE_ENV=development
2
+ PORT=3000
3
+ WEB_ORIGIN=http://localhost:5173
4
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/authenik8?schema=public
5
+ REDIS_URL=redis://localhost:6379
6
+ AUTHENIK8_SIGNING_JWKS=
7
+ AUTHENIK8_ACTIVE_KID=
8
+ AUTHENIK8_ISSUER=http://localhost:3000
9
+ AUTHENIK8_AUDIENCE=authenik8-fullstack-api
10
+ AUTHENIK8_AGENTS={}
11
+ REFRESH_SECRET=replace-with-another-32-random-characters
12
+ COOKIE_SECURE=false
13
+ TRUST_PROXY=false
14
+ LOG_LEVEL=info
15
+ EMAIL_FROM=Authenik8 <auth@example.com>
16
+ RESEND_API_KEY=
17
+ GOOGLE_CLIENT_ID=
18
+ GOOGLE_CLIENT_SECRET=
19
+ GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/oauth/google/callback
20
+ GITHUB_CLIENT_ID=
21
+ GITHUB_CLIENT_SECRET=
22
+ GITHUB_REDIRECT_URI=http://localhost:3000/api/auth/oauth/github/callback
23
+ SEED_ADMIN_EMAIL=admin@example.com
24
+ SEED_ADMIN_PASSWORD=ChangeMe123!