create-authenik8-app 2.4.11 → 2.4.13

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 -7
  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 +149 -28
  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 +13 -6
  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 +13 -6
  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,54 +1,141 @@
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
+ export const refreshTokenBodySchema = z.strictObject({
17
+ refreshToken: z.string().trim().min(16, "Refresh token is required").max(4096),
18
+ });
19
+ const secretSchema = z.string().trim().min(32);
20
+ const environmentValueSchema = z.string().trim().min(1);
21
+ const portSchema = z.coerce.number().int().min(1).max(65535);
22
+ const signingJwkSchema = z.object({
23
+ kty: z.literal("EC"),
24
+ crv: z.literal("P-256"),
25
+ x: z.string().min(1),
26
+ y: z.string().min(1),
27
+ d: z.string().min(1).optional(),
28
+ kid: z.string().min(1),
29
+ alg: z.literal("ES256").optional(),
30
+ use: z.literal("sig").optional(),
31
+ });
32
+ const identifierSchema = z.string()
33
+ .trim()
34
+ .min(1)
35
+ .max(128)
36
+ .regex(/^[A-Za-z0-9._:-]+$/);
37
+ const agentIdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/);
38
+ const agentScopeSchema = z.string().max(128).regex(/^[a-z][a-z0-9._/-]*(?::[a-z][a-z0-9._/-]*)+$/);
39
+ const agentRegistrySchema = z.record(
40
+ agentIdSchema,
41
+ z.array(agentScopeSchema).min(1).max(64),
42
+ );
43
+
44
+ export class InputValidationError extends Error {}
45
+
46
+ function validationMessage(error: z.ZodError, fallback: string): string {
47
+ return error.issues[0]?.message ?? fallback;
48
+ }
5
49
 
6
- if (typeof value !== "string" || value.trim().length < 32) {
7
- throw new Error(`${name} must be set to at least 32 characters`);
50
+ export function requiredSecret(name: string): string {
51
+ const result = secretSchema.safeParse(process.env[name]);
52
+ if (!result.success) {
53
+ throw new InputValidationError(`${name} must be set to at least 32 characters`);
8
54
  }
9
-
10
- return value;
55
+ return result.data;
11
56
  }
12
57
 
13
- export function parseCredentials(body: unknown) {
14
- if (!body || typeof body !== "object") {
15
- throw new Error("Email and password are required");
16
- }
58
+ export function requiredEnv(name: string): string {
59
+ const result = environmentValueSchema.safeParse(process.env[name]);
60
+ if (!result.success) throw new InputValidationError(`${name} must be set`);
61
+ return result.data;
62
+ }
17
63
 
18
- const { email, password } = body as { email?: unknown; password?: unknown };
64
+ export function requiredPort(): number {
65
+ const result = portSchema.safeParse(process.env.PORT ?? 3000);
66
+ if (!result.success) throw new InputValidationError("PORT must be between 1 and 65535");
67
+ return result.data;
68
+ }
19
69
 
20
- if (typeof email !== "string" || typeof password !== "string") {
21
- throw new Error("Email and password are required");
70
+ export function authJwkConfig(): Authenik8JwkConfig {
71
+ let parsed: unknown;
72
+ try {
73
+ parsed = JSON.parse(requiredEnv("AUTHENIK8_SIGNING_JWKS"));
74
+ } catch {
75
+ throw new InputValidationError("AUTHENIK8_SIGNING_JWKS must be a valid JSON array");
22
76
  }
77
+ const result = z.array(signingJwkSchema).min(1).safeParse(parsed);
78
+ if (!result.success) {
79
+ throw new InputValidationError("AUTHENIK8_SIGNING_JWKS must contain ES256 P-256 JWKs");
80
+ }
81
+ const activeKid = requiredEnv("AUTHENIK8_ACTIVE_KID");
82
+ const activeKey = result.data.find((key) => key.kid === activeKid);
83
+ if (!activeKey?.d) throw new InputValidationError("AUTHENIK8_ACTIVE_KID must select a private signing JWK");
84
+ return {
85
+ keys: result.data,
86
+ activeKid,
87
+ issuer: requiredEnv("AUTHENIK8_ISSUER"),
88
+ audience: requiredEnv("AUTHENIK8_AUDIENCE"),
89
+ };
90
+ }
23
91
 
24
- const normalizedEmail = email.trim().toLowerCase();
25
- const atIndex = normalizedEmail.indexOf("@");
26
- const dotIndex = normalizedEmail.lastIndexOf(".");
27
-
92
+ export function agentIdentityConfig(): AgentIdentityConfig | undefined {
93
+ const source = process.env.AUTHENIK8_AGENTS?.trim();
94
+ if (!source) return undefined;
28
95
 
29
- if (atIndex < 1 || atIndex !== normalizedEmail.lastIndexOf("@") || dotIndex < atIndex + 2 ||dotIndex === normalizedEmail.length - 1 ) {
30
- throw new Error("A valid email is required");
96
+ let parsed: unknown;
97
+ try {
98
+ parsed = JSON.parse(source);
99
+ } catch {
100
+ throw new InputValidationError("AUTHENIK8_AGENTS must be a JSON object of agent scope arrays");
31
101
  }
32
-
33
- if (password.length < 8 || password.length > 1024) {
34
- throw new Error("Password must be between 8 and 1024 characters");
102
+ const result = agentRegistrySchema.safeParse(parsed);
103
+ if (!result.success) {
104
+ throw new InputValidationError("AUTHENIK8_AGENTS must map valid agent IDs to resource:action scopes");
35
105
  }
36
-
37
- return { email: normalizedEmail, password };
106
+ if (!Object.keys(result.data).length) return undefined;
107
+
108
+ const registry = result.data;
109
+ return {
110
+ resolveAgent: async (agentId) => {
111
+ const scopes = Object.prototype.hasOwnProperty.call(registry, agentId)
112
+ ? registry[agentId]
113
+ : undefined;
114
+ return scopes ? { agentId, scopes, active: true } : null;
115
+ },
116
+ };
38
117
  }
39
118
 
40
- export function parseRefreshToken(body: unknown) {
41
- if (!body || typeof body !== "object") {
42
- throw new Error("Refresh token is required");
119
+ export function parseCredentials(body: unknown): z.infer<typeof credentialsSchema> {
120
+ const result = credentialsSchema.safeParse(body);
121
+ if (!result.success) {
122
+ throw new InputValidationError(validationMessage(result.error, "Email and password are required"));
43
123
  }
124
+ return result.data;
125
+ }
44
126
 
45
- const { refreshToken } = body as { refreshToken?: unknown };
46
-
47
- if (typeof refreshToken !== "string" || refreshToken.trim().length < 16) {
48
- throw new Error("Refresh token is required");
127
+ export function parseRefreshToken(body: unknown): string {
128
+ const result = refreshTokenBodySchema.safeParse(body);
129
+ if (!result.success) {
130
+ throw new InputValidationError(validationMessage(result.error, "Refresh token is required"));
49
131
  }
132
+ return result.data.refreshToken;
133
+ }
50
134
 
51
- return refreshToken;
135
+ export function parseIdentifier(value: unknown, label: string): string {
136
+ const result = identifierSchema.safeParse(value);
137
+ if (!result.success) throw new InputValidationError(`${label} is invalid`);
138
+ return result.data;
52
139
  }
53
140
 
54
141
  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
  AUTHENIK8_OAUTH_PROVIDERS=google,github
@@ -124,10 +128,6 @@ Expected shape:
124
128
 
125
129
  ```json
126
130
  {
127
- "user": {
128
- "id": "user-id",
129
- "email": "dev@example.com"
130
- },
131
131
  "accessToken": "access-token",
132
132
  "refreshToken": "refresh-token"
133
133
  }
@@ -152,7 +152,10 @@ If a provider is not listed in `AUTHENIK8_OAUTH_PROVIDERS`, that provider route
152
152
  ## Environment Variables
153
153
 
154
154
  - `DATABASE_URL`: Prisma database connection. SQLite uses `file:./dev.db`; Postgres uses a `postgresql://...` URL.
155
- - `JWT_SECRET`: signs short-lived access tokens. Use a long random value in production.
155
+ - `AUTHENIK8_SIGNING_JWKS`: JSON array containing the active private ES256 key and any previous public verification keys.
156
+ - `AUTHENIK8_ACTIVE_KID`: selects the private key used to sign new access tokens.
157
+ - `AUTHENIK8_ISSUER` / `AUTHENIK8_AUDIENCE`: required claims checked during verification.
158
+ - `AUTHENIK8_AGENTS`: optional JSON map of agent IDs to exact `resource:action` scopes; keep `{}` to disable agent identity. See `AGENT_IDENTITY.md`.
156
159
  - `REFRESH_SECRET`: signs refresh tokens. Use a different long random value in production.
157
160
  - `REDIS_HOST`: Redis host for refresh-token/session security.
158
161
  - `REDIS_PORT`: Redis port, usually `6379` locally.
@@ -224,7 +227,7 @@ The callback URL in `.env` must match the provider dashboard exactly, including
224
227
 
225
228
  `Prisma Client did not initialize`: run `npm run prisma:migrate`, then restart `npm run dev`.
226
229
 
227
- `JWT_SECRET must be set to at least 32 characters`: check `.env`; both token secrets must be long strings.
230
+ `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`.
228
231
 
229
232
  `Cannot POST /auth/login`: confirm the server is running and you generated the password or OAuth auth template.
230
233
 
@@ -234,7 +237,7 @@ The callback URL in `.env` must match the provider dashboard exactly, including
234
237
 
235
238
  `google OAuth is not configured`: add `google` to `AUTHENIK8_OAUTH_PROVIDERS` and set the Google env vars, or use only the enabled provider route.
236
239
 
237
- `Port 3000 already in use`: stop the other process or change the `app.listen(3000)` port in `src/server.ts`.
240
+ `Port 3000 already in use`: stop the other process or set `PORT` in `.env`.
238
241
 
239
242
  `DATABASE_URL is wrong`: for SQLite use `file:./dev.db`; for local Docker Postgres use `postgresql://postgres:postgres@localhost:5432/authenik8?schema=public`.
240
243
 
@@ -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,25 +3,32 @@
3
3
  "description": "Authenik8 generated Express auth app",
4
4
  "main": "dist/server.js",
5
5
  "type": "commonjs",
6
+ "engines": {
7
+ "node": "^20.19 || ^22.12 || >=24"
8
+ },
6
9
  "scripts": {
7
10
  "dev": "ts-node-dev --respawn --transpile-only src/server.ts",
8
11
  "build": "tsc",
9
12
  "start": "node dist/server.js",
10
- "prisma:migrate": "prisma migrate dev",
11
- "docker:up": "docker compose up -d",
13
+ "prisma:migrate": "prisma migrate dev && prisma generate",
14
+ "docker:up": "docker compose up -d --wait",
12
15
  "docker:down": "docker compose down"
13
16
  },
14
17
  "dependencies": {
15
- "authenik8-core": "^1.0.38",
18
+ "authenik8-core": "2.0.3",
16
19
  "dotenv": "^16.0.0",
17
20
  "express": "^4.19.2",
18
- "@prisma/client": "5.22.0"
21
+ "@prisma/client": "7.8.0",
22
+ "zod": "^4.4.3"
19
23
  },
20
24
  "devDependencies": {
21
25
  "@types/express": "^4.17.21",
22
- "@types/node": "^25.9.1",
26
+ "@types/node": "^26.0.1",
23
27
  "ts-node-dev": "^2.0.0",
24
28
  "typescript": "^5.0.0",
25
- "prisma": "5.22.0"
29
+ "prisma": "7.8.0"
30
+ },
31
+ "overrides": {
32
+ "@hono/node-server": "1.19.13"
26
33
  }
27
34
  }
@@ -1,6 +1,6 @@
1
1
  import { createAuthenik8 } from "authenik8-core";
2
2
  import dotenv from "dotenv";
3
- import { requiredSecret } from "../utils/security";
3
+ import { agentIdentityConfig, authJwkConfig, requiredSecret } from "../utils/security";
4
4
 
5
5
  dotenv.config();
6
6
 
@@ -12,8 +12,9 @@ function oauthConfig() {
12
12
 
13
13
  export async function initAuth() {
14
14
  authInstance= await createAuthenik8({
15
- jwtSecret: requiredSecret("JWT_SECRET"),
15
+ jwt: authJwkConfig(),
16
16
  refreshSecret: requiredSecret("REFRESH_SECRET"),
17
+ agent: agentIdentityConfig(),
17
18
  oauth: oauthConfig(),
18
19
  });
19
20
 
@@ -32,19 +32,15 @@ export const passwordController = {
32
32
  const { email, password } = parseCredentials(req.body);
33
33
  const user = await prisma.user.findUnique({ where: { email } });
34
34
 
35
- if (!user || !(await comparePassword(password, user.password))) {
35
+ if (!user?.password || !(await comparePassword(password, user.password))) {
36
36
  return res.status(401).json({ error: "Invalid credentials" });
37
37
  }
38
38
 
39
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({
40
+ const { accessToken, refreshToken } = await auth.issueTokens({
46
41
  userId: user.id,
47
42
  email: user.email,
43
+ role: String(user.role).toLowerCase(),
48
44
  });
49
45
 
50
46
  res.json({ accessToken, refreshToken });
@@ -1,8 +1,19 @@
1
1
  import { Request, Response } from "express";
2
- import { sanitizeSessionResponse } from "../../utils/security";
2
+ import {
3
+ InputValidationError,
4
+ parseIdentifier,
5
+ sanitizeSessionResponse,
6
+ } from "../../utils/security";
7
+
8
+ function sessionError(res: Response, error: unknown, fallback: string) {
9
+ if (error instanceof InputValidationError) {
10
+ return res.status(400).json({ success: false, message: error.message });
11
+ }
12
+ return res.status(500).json({ success: false, message: fallback });
13
+ }
3
14
 
4
15
  export const protectedController = {
5
- protected( res: Response) {
16
+ protected(_req: Request, res: Response) {
6
17
  res.json({ message: "Protected route" });
7
18
  },
8
19
 
@@ -13,10 +24,11 @@ export const protectedController = {
13
24
  return res.status(503).json({ success: false, message: "Session management unavailable" });
14
25
  }
15
26
 
16
- const sessions = await actions.listSessions(req.params.userId);
27
+ const userId = parseIdentifier(req.params.userId, "User ID");
28
+ const sessions = await actions.listSessions(userId);
17
29
  res.json({ sessions: sanitizeSessionResponse(sessions) });
18
- } catch {
19
- res.status(500).json({ success: false, message: "Failed to retrieve sessions" });
30
+ } catch (error) {
31
+ sessionError(res, error, "Failed to retrieve sessions");
20
32
  }
21
33
  },
22
34
 
@@ -27,10 +39,12 @@ export const protectedController = {
27
39
  return res.status(503).json({ success: false, message: "Session management unavailable" });
28
40
  }
29
41
 
30
- await actions.revokeSession(req.params.userId, req.params.sessionId);
42
+ const userId = parseIdentifier(req.params.userId, "User ID");
43
+ const sessionId = parseIdentifier(req.params.sessionId, "Session ID");
44
+ await actions.revokeSession(userId, sessionId);
31
45
  res.json({ success: true, message: "Session revoked" });
32
- } catch {
33
- res.status(500).json({ success: false, message: "Failed to revoke session" });
46
+ } catch (error) {
47
+ sessionError(res, error, "Failed to revoke session");
34
48
  }
35
49
  },
36
50
 
@@ -41,10 +55,11 @@ export const protectedController = {
41
55
  return res.status(503).json({ success: false, message: "Session management unavailable" });
42
56
  }
43
57
 
44
- await actions.revokeAllSessions(req.params.userId);
58
+ const userId = parseIdentifier(req.params.userId, "User ID");
59
+ await actions.revokeAllSessions(userId);
45
60
  res.json({ success: true, message: "All sessions revoked" });
46
- } catch {
47
- res.status(500).json({ success: false, message: "Failed to revoke sessions" });
61
+ } catch (error) {
62
+ sessionError(res, error, "Failed to revoke sessions");
48
63
  }
49
64
  },
50
65
  };
@@ -0,0 +1,90 @@
1
+ import { prisma } from "../prisma/client";
2
+
3
+ type ProviderIdentity = {
4
+ provider: string;
5
+ providerId: string;
6
+ };
7
+
8
+ type IdentityUser = {
9
+ id: string;
10
+ email: string;
11
+ role?: string;
12
+ providers: ProviderIdentity[];
13
+ };
14
+
15
+ const db = prisma as any;
16
+
17
+ function toIdentityUser(user: any): IdentityUser | null {
18
+ if (!user) return null;
19
+
20
+ return {
21
+ id: user.id,
22
+ email: user.email,
23
+ role: typeof user.role === "string" ? user.role.toLowerCase() : undefined,
24
+ providers: (user.identityProviders ?? []).map((identity: ProviderIdentity) => ({
25
+ provider: identity.provider,
26
+ providerId: identity.providerId,
27
+ })),
28
+ };
29
+ }
30
+
31
+ const includeProviders = { identityProviders: true };
32
+
33
+ export const identityAdapter = {
34
+ async findUserByEmail(email: string) {
35
+ const user = await db.user.findUnique({
36
+ where: { email: email.trim().toLowerCase() },
37
+ include: includeProviders,
38
+ });
39
+ return toIdentityUser(user);
40
+ },
41
+
42
+ async findUserByProvider(provider: string, providerId: string) {
43
+ const identity = await db.identityProvider.findUnique({
44
+ where: { provider_providerId: { provider, providerId } },
45
+ include: { user: { include: includeProviders } },
46
+ });
47
+ return toIdentityUser(identity?.user);
48
+ },
49
+
50
+ async createUser(data: { email: string; provider: string; providerId: string }) {
51
+ const email = data.email.trim().toLowerCase();
52
+ const existing = await this.findUserByEmail(email);
53
+ if (existing) return existing;
54
+
55
+ const user = await db.user.create({
56
+ data: {
57
+ email,
58
+ password: null,
59
+ verified: true,
60
+ identityProviders: {
61
+ create: { provider: data.provider, providerId: data.providerId },
62
+ },
63
+ },
64
+ include: includeProviders,
65
+ });
66
+
67
+ return toIdentityUser(user) as IdentityUser;
68
+ },
69
+
70
+ async linkProvider(userId: string, provider: string, providerId: string) {
71
+ const existing = await db.identityProvider.findUnique({
72
+ where: { provider_providerId: { provider, providerId } },
73
+ });
74
+
75
+ if (existing && existing.userId !== userId) {
76
+ throw new Error("Provider is already linked to another user");
77
+ }
78
+
79
+ const user = await db.user.findUnique({ where: { id: userId } });
80
+ if (!user) {
81
+ throw new Error(`User not found: ${userId}`);
82
+ }
83
+
84
+ await db.identityProvider.upsert({
85
+ where: { userId_provider: { userId, provider } },
86
+ create: { userId, provider, providerId },
87
+ update: { providerId },
88
+ });
89
+ },
90
+ };
@@ -2,9 +2,8 @@ import { Request, Response, NextFunction } from "express";
2
2
  import { getAuth } from "../auth";
3
3
 
4
4
  export const adminMiddleware = (req: Request, res: Response, next: NextFunction) => {
5
- getAuth().requireAdmin(req, res, next);
5
+ return getAuth().requireAdmin(req, res, next);
6
6
  };
7
7
 
8
- export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
9
- getAuth().authenticateJWT(req, res, next);
10
- };
8
+ export const authMiddleware = (req: Request, res: Response, next: NextFunction) =>
9
+ getAuth().requireAuth(req, res, next);
@@ -1,10 +1,10 @@
1
1
  import express from "express";
2
- import { adminMiddleware } from "../middleware/auth.middleware";
2
+ import { adminMiddleware, authMiddleware } from "../middleware/auth.middleware";
3
3
  import { protectedController } from "../controllers/protected.controller";
4
4
 
5
5
  const router = express.Router();
6
6
 
7
- router.get("/protected", adminMiddleware, protectedController.protected);
7
+ router.get("/protected", authMiddleware, protectedController.protected);
8
8
  router.get("/admin/sessions/:userId", adminMiddleware, protectedController.listSessions);
9
9
  router.delete("/admin/sessions/:userId/:sessionId", adminMiddleware, protectedController.revokeSession);
10
10
  router.delete("/admin/sessions/:userId", adminMiddleware, protectedController.revokeAllSessions);
@@ -1,6 +1,6 @@
1
1
  import { createAuthenik8 } from "authenik8-core";
2
2
  import dotenv from "dotenv";
3
- import { requiredEnv, requiredSecret } from "../../../../src/utils/security";
3
+ import { agentIdentityConfig, authJwkConfig, requiredEnv, requiredSecret } from "../../../../src/utils/security";
4
4
 
5
5
  dotenv.config();
6
6
 
@@ -18,8 +18,9 @@ function oauthConfig() {
18
18
 
19
19
  export async function initAuth() {
20
20
  authInstance= await createAuthenik8({
21
- jwtSecret: requiredSecret("JWT_SECRET"),
21
+ jwt: authJwkConfig(),
22
22
  refreshSecret: requiredSecret("REFRESH_SECRET"),
23
+ agent: agentIdentityConfig(),
23
24
  oauth: oauthConfig(),
24
25
  });
25
26
 
@@ -1,6 +1,6 @@
1
1
  import { createAuthenik8 } from "authenik8-core";
2
2
  import dotenv from "dotenv";
3
- import { requiredEnv, requiredSecret } from "../../../../src/utils/security";
3
+ import { agentIdentityConfig, authJwkConfig, requiredEnv, requiredSecret } from "../../../../src/utils/security";
4
4
 
5
5
  dotenv.config();
6
6
 
@@ -18,8 +18,9 @@ function oauthConfig() {
18
18
 
19
19
  export async function initAuth() {
20
20
  authInstance= await createAuthenik8({
21
- jwtSecret: requiredSecret("JWT_SECRET"),
21
+ jwt: authJwkConfig(),
22
22
  refreshSecret: requiredSecret("REFRESH_SECRET"),
23
+ agent: agentIdentityConfig(),
23
24
  oauth: oauthConfig(),
24
25
  });
25
26
 
@@ -1,6 +1,6 @@
1
1
  import { createAuthenik8 } from "authenik8-core";
2
2
  import dotenv from "dotenv";
3
- import { requiredEnv, requiredSecret } from "../utils/security";
3
+ import { agentIdentityConfig, authJwkConfig, requiredEnv, requiredSecret } from "../utils/security";
4
4
 
5
5
  dotenv.config();
6
6
 
@@ -23,8 +23,9 @@ function oauthConfig() {
23
23
 
24
24
  export async function initAuth() {
25
25
  authInstance= await createAuthenik8({
26
- jwtSecret: requiredSecret("JWT_SECRET"),
26
+ jwt: authJwkConfig(),
27
27
  refreshSecret: requiredSecret("REFRESH_SECRET"),
28
+ agent: agentIdentityConfig(),
28
29
  oauth: oauthConfig(),
29
30
  });
30
31
 
@@ -3,6 +3,7 @@ import passwordRoutes from "./auth/routes/password.route";
3
3
  import oauthRoutes from "./auth/routes/oauth.routes";
4
4
  import protectedRoutes from "./auth/routes/protected.routes";
5
5
  import { getAuth, initAuth } from "./auth/auth";
6
+ import { requiredPort } from "./utils/security";
6
7
 
7
8
  async function start(){
8
9
  await initAuth();
@@ -12,13 +13,15 @@ async function start(){
12
13
  app.use(express.json({ limit: "16kb", strict: true }));
13
14
  app.use(auth.helmet);
14
15
  app.use(auth.rateLimit);
16
+ app.get("/.well-known/jwks.json", (_req, res) => res.json(auth.getJwks()));
15
17
 
16
18
  app.use("/auth", passwordRoutes);
17
19
  app.use("/auth", oauthRoutes);
18
20
  app.use("/", protectedRoutes);
19
21
 
20
- app.listen(3000, () => {
21
- console.log("Auth system running on http://localhost:3000");
22
+ const port = requiredPort();
23
+ app.listen(port, () => {
24
+ console.log(`Auth system running on http://localhost:${port}`);
22
25
  });
23
26
  }
24
27
 
@@ -32,11 +35,3 @@ process.on("unhandledRejection", (err) => {
32
35
  console.error(" Unhandled Rejection:", err);
33
36
  process.exit(1);
34
37
  });
35
- setInterval(() => {
36
- const used = process.memoryUsage().heapUsed / 1024 / 1024;
37
-
38
- if (used > 300) {
39
- console.error(` Memory exceeded: ${used.toFixed(2)} MB`);
40
- process.exit(1);
41
- }
42
- }, 10000);