create-authenik8-app 2.4.12 → 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 -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 +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 +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
@@ -0,0 +1,46 @@
1
+ # Full-stack preset contract
2
+
3
+ This document is the testable contract for the `fullstack` preset.
4
+
5
+ ## Stack and boundaries
6
+
7
+ - Node.js 20.19+, 22.12+, or 24+ and npm workspaces.
8
+ - `apps/web`: React, Vite, React Router, TanStack Query, and the generated UI.
9
+ - `apps/api`: Express, Authenik8, Prisma, PostgreSQL, Redis, policies, and HTTP controllers.
10
+ - `packages/contracts`: shared Zod request schemas and public response types.
11
+ - `packages/api-client`: the only browser HTTP client. It owns the in-memory access token and one-time refresh retry.
12
+ - `packages/ui`: removable application primitives used by the web workspace.
13
+
14
+ ## Commands
15
+
16
+ | Command | Guarantee |
17
+ | --- | --- |
18
+ | `npm run dev` | Builds shared packages and starts API and web together. |
19
+ | `npm run build` | Produces the API and frontend production bundles. |
20
+ | `npm start` | Starts Express, which serves `/api` and the built SPA. |
21
+ | `npm run db:migrate` | Applies the Prisma PostgreSQL development migration. |
22
+ | `npm run db:seed` | Creates the configured administrator and example data. |
23
+ | `npm test` | Runs API policy/security tests and web storage tests. |
24
+
25
+ ## Network contract
26
+
27
+ - Vite: `http://localhost:5173`, proxying `/api` to Express.
28
+ - Express: `http://localhost:3000`.
29
+ - PostgreSQL: `localhost:5432` in local Docker Compose.
30
+ - Redis: `localhost:6379` in local Docker Compose.
31
+ - Production uses one origin. Express serves `apps/web/dist` after `npm run build`.
32
+
33
+ ## Environment contract
34
+
35
+ Required: `DATABASE_URL`, `REDIS_URL`, `AUTHENIK8_SIGNING_JWKS`, `AUTHENIK8_ACTIVE_KID`, `AUTHENIK8_ISSUER`, `AUTHENIK8_AUDIENCE`, `REFRESH_SECRET`, and `WEB_ORIGIN`.
36
+ OAuth variables are required only for an enabled provider. `RESEND_API_KEY` and `EMAIL_FROM` enable production verification and recovery delivery. See `.env.example` for the complete list.
37
+
38
+ ## Security guarantees
39
+
40
+ - Access tokens exist only in module memory and are sent as Bearer tokens.
41
+ - Refresh tokens are AES-256-GCM sealed in an HttpOnly cookie scoped to `/api`, with `SameSite=Strict` and `Secure` in production.
42
+ - Refresh rotation, replay rejection, and server-side refresh revocation use Authenik8 and Redis.
43
+ - Browser mutations enforce an exact allowed `Origin` and a signed double-submit CSRF token.
44
+ - Admin and Project APIs enforce roles or ownership on the server.
45
+ - Credential and recovery failures do not disclose whether an email address exists.
46
+ - Request IDs, security headers, redacted structured logs, Redis-backed Authenik8 rate limiting, readiness, and liveness endpoints are enabled.
@@ -0,0 +1,65 @@
1
+ # Authenik8 Full-stack Starter
2
+
3
+ A connected React and Express application with secure Authenik8 sessions, account settings, administration, and an owned Project resource.
4
+
5
+ Optional agent/service identities are disabled by default. Configure
6
+ `AUTHENIK8_AGENTS` and follow `AGENT_IDENTITY.md` to add scoped M2M or delegated
7
+ agent access without exposing an unsafe public token-minting route.
8
+
9
+ ## Local development
10
+
11
+ Requirements: Node.js 20.19+, 22.12+, or 24+, npm, and Docker with Compose.
12
+
13
+ ```bash
14
+ docker compose up -d
15
+ npm run db:migrate
16
+ npm run db:seed
17
+ npm run dev
18
+ ```
19
+
20
+ Open `http://localhost:5173`. The API runs on `http://localhost:3000/api`. The generated `.env` contains development values; replace every secret before deploying.
21
+
22
+ The seeded administrator uses `SEED_ADMIN_EMAIL` and `SEED_ADMIN_PASSWORD`. Change the default password immediately, including for local environments shared by multiple people.
23
+
24
+ ## Application map
25
+
26
+ - Authentication: registration, login, OAuth, refresh, logout, password recovery, and email verification.
27
+ - Account: profile, password, linked providers, active sessions, and revocation.
28
+ - Administration: users, roles, status, session revocation, and audit events.
29
+ - Projects: list, create, details, edit, archive, and delete with owner policies.
30
+
31
+ The API follows a contract, repository, service, controller, policy, and route convention. Copy `apps/api/src/modules/projects` and `apps/web/src/features/projects` when adding a vertical feature.
32
+
33
+ ## Session model
34
+
35
+ The API returns a short-lived access token to the Auth provider, which holds it only in memory. The browser sends an encrypted, restricted HttpOnly refresh cookie automatically. The API client obtains a signed CSRF token for browser mutations, performs one refresh after a `401`, shares in-flight token requests, and retries once. It never reads from or writes tokens to browser storage.
36
+
37
+ Route guards are user experience controls. API middleware and policies remain the authorization boundary.
38
+
39
+ ## OAuth callbacks
40
+
41
+ Configure either provider with these exact local callback URLs:
42
+
43
+ ```text
44
+ http://localhost:3000/api/auth/oauth/google/callback
45
+ http://localhost:3000/api/auth/oauth/github/callback
46
+ ```
47
+
48
+ OAuth callbacks place a single-use exchange code and an encrypted session payload in Redis, then redirect to the SPA. Tokens are not placed in a URL.
49
+
50
+ ## Production
51
+
52
+ ```bash
53
+ npm run build
54
+ NODE_ENV=production npm start
55
+ ```
56
+
57
+ Read [docs/PRODUCTION.md](docs/PRODUCTION.md) and [THREAT_MODEL.md](THREAT_MODEL.md) before deployment. Production requires HTTPS, strong unique secrets, non-public Redis, database backups, a trusted reverse proxy configuration, an exact `WEB_ORIGIN`, and a working mail delivery integration for recovery and verification links.
58
+
59
+ ## Health and tests
60
+
61
+ - `GET /api/health/live` confirms the process is running.
62
+ - `GET /api/health/ready` checks PostgreSQL and Redis.
63
+ - `GET /api/docs/openapi.json` returns the generated OpenAPI 3.1 contract.
64
+ - `npm test` covers ownership/admin policies, encrypted cookies, origin and CSRF defenses, and the browser storage rule.
65
+ - `npm run typecheck` checks each workspace.
@@ -0,0 +1,30 @@
1
+ # Threat model
2
+
3
+ ## Protected assets
4
+
5
+ Account credentials, OAuth identities, access and refresh tokens, agent grants and M2M sessions, delegated actor chains, administrative actions, user profile data, and owned Project records.
6
+
7
+ ## Trust boundaries
8
+
9
+ The browser is untrusted. React route guards provide navigation behavior only. Express middleware and module policies make every authorization decision. PostgreSQL and Redis must be reachable only from trusted application networks.
10
+
11
+ ## Threats addressed
12
+
13
+ - Refresh-token theft and replay: AES-256-GCM-sealed HttpOnly cookies, Redis-backed rotation, atomic replacement, and replay rejection.
14
+ - Cross-site cookie abuse: exact-origin checks and signed double-submit CSRF tokens on browser mutations.
15
+ - Token exfiltration through browser storage: access tokens remain in module memory; refresh tokens are never exposed to JavaScript.
16
+ - Broken object authorization: every Project lookup or mutation is scoped by owner unless an explicit administrator policy allows it.
17
+ - Privilege escalation: admin APIs require authenticated server-side role checks and record audit events.
18
+ - Account enumeration: login and recovery responses are generic.
19
+ - OAuth login CSRF: provider state is handled by Authenik8; the SPA receives only a single-use, short-lived exchange code.
20
+ - Password database compromise: passwords and recovery/verification tokens are stored as one-way hashes.
21
+ - Basic abuse and injection: Redis-backed Authenik8 rate limiting, request size limits, Zod validation, Prisma parameterization, security headers, and structured logging.
22
+ - Agent identity: human and machine token purposes are separated, exact scopes are checked against the live registry, M2M sessions are revocable, and delegated tokens remain bound to their human session.
23
+
24
+ ## Residual risks
25
+
26
+ An active XSS can make authenticated API calls while the page is open even though it cannot read the refresh cookie. Keep the content security policy strict and audit third-party scripts. Access tokens remain valid until their short expiry after some session revocations. Agent-token issuance is privileged but does not authenticate the workload for you; use mTLS, cloud workload identity, or signed assertions and never expose an unauthenticated mint route. Public JWKS verification cannot observe Redis agent revocation. OAuth provider compromise, malicious dependencies, host compromise, denial of service at volumes beyond one Redis limiter, and mistakes in deployment infrastructure require controls outside this starter.
27
+
28
+ ## Review triggers
29
+
30
+ Repeat the threat review when adding a new identity provider or agent, changing agent scopes or delegation policy, changing cookie scope or origins, introducing file uploads or billing, adding multiple tenants, exposing Redis, changing proxy topology, or adding third-party browser scripts.
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@authenik8/api",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "scripts": {
8
+ "predev": "prisma generate",
9
+ "dev": "tsx watch src/server.ts",
10
+ "prebuild": "prisma generate",
11
+ "build": "tsc -p tsconfig.json",
12
+ "start": "node dist/server.js",
13
+ "test": "vitest run",
14
+ "typecheck": "tsc -p tsconfig.json --noEmit",
15
+ "prisma:generate": "prisma generate",
16
+ "prisma:migrate": "prisma migrate dev && prisma generate",
17
+ "prisma:seed": "prisma generate && prisma db seed",
18
+ "prisma:studio": "prisma studio"
19
+ },
20
+ "dependencies": {
21
+ "@authenik8/contracts": "0.1.0",
22
+ "@prisma/adapter-pg": "7.8.0",
23
+ "@prisma/client": "7.8.0",
24
+ "authenik8-core": "2.0.3",
25
+ "bcryptjs": "^2.4.3",
26
+ "cookie-parser": "^1.4.7",
27
+ "cors": "^2.8.5",
28
+ "dotenv": "^17.3.1",
29
+ "express": "^5.2.1",
30
+ "ioredis": "^5.10.0",
31
+ "pino": "^9.7.0",
32
+ "pino-http": "^10.4.0",
33
+ "zod": "^4.4.3"
34
+ },
35
+ "devDependencies": {
36
+ "@types/bcryptjs": "^2.4.6",
37
+ "@types/cookie-parser": "^1.4.9",
38
+ "@types/cors": "^2.8.19",
39
+ "@types/express": "^5.0.6",
40
+ "@types/node": "^25.0.0",
41
+ "@types/supertest": "^7.2.0",
42
+ "prisma": "7.8.0",
43
+ "supertest": "^7.2.2",
44
+ "tsx": "^4.22.4",
45
+ "typescript": "^6.0.3",
46
+ "vitest": "^4.1.8"
47
+ }
48
+ }
@@ -0,0 +1,124 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ }
8
+
9
+ model User {
10
+ id String @id @default(uuid())
11
+ email String @unique
12
+ passwordHash String?
13
+ name String
14
+ role Role @default(USER)
15
+ status UserStatus @default(ACTIVE)
16
+ emailVerifiedAt DateTime?
17
+ passwordUpdatedAt DateTime?
18
+ createdAt DateTime @default(now())
19
+ updatedAt DateTime @updatedAt
20
+ projects Project[]
21
+ sessions Session[]
22
+ oauthAccounts OAuthAccount[]
23
+ resetTokens PasswordResetToken[]
24
+ verificationTokens EmailVerificationToken[]
25
+ auditEvents AuditEvent[] @relation("AuditActor")
26
+ }
27
+
28
+ model OAuthAccount {
29
+ id String @id @default(uuid())
30
+ userId String
31
+ provider String
32
+ providerAccountId String
33
+ providerEmail String
34
+ createdAt DateTime @default(now())
35
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
36
+
37
+ @@unique([provider, providerAccountId])
38
+ @@unique([userId, provider])
39
+ @@index([userId])
40
+ }
41
+
42
+ model Session {
43
+ id String @id @default(uuid())
44
+ userId String
45
+ coreSessionId String @unique
46
+ refreshHash String @unique
47
+ userAgent String
48
+ ipAddress String
49
+ createdAt DateTime @default(now())
50
+ lastUsedAt DateTime @default(now())
51
+ expiresAt DateTime
52
+ revokedAt DateTime?
53
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
54
+
55
+ @@index([userId, revokedAt])
56
+ }
57
+
58
+ model Project {
59
+ id String @id @default(uuid())
60
+ name String
61
+ description String @default("")
62
+ status ProjectStatus @default(DRAFT)
63
+ ownerId String
64
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
65
+ createdAt DateTime @default(now())
66
+ updatedAt DateTime @updatedAt
67
+
68
+ @@index([ownerId, updatedAt])
69
+ }
70
+
71
+ model PasswordResetToken {
72
+ id String @id @default(uuid())
73
+ tokenHash String @unique
74
+ userId String
75
+ expiresAt DateTime
76
+ usedAt DateTime?
77
+ createdAt DateTime @default(now())
78
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
79
+
80
+ @@index([userId, expiresAt])
81
+ }
82
+
83
+ model EmailVerificationToken {
84
+ id String @id @default(uuid())
85
+ tokenHash String @unique
86
+ userId String
87
+ expiresAt DateTime
88
+ usedAt DateTime?
89
+ createdAt DateTime @default(now())
90
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
91
+
92
+ @@index([userId, expiresAt])
93
+ }
94
+
95
+ model AuditEvent {
96
+ id String @id @default(uuid())
97
+ actorId String?
98
+ action String
99
+ targetType String
100
+ targetId String?
101
+ metadata Json?
102
+ ipAddress String?
103
+ createdAt DateTime @default(now())
104
+ actor User? @relation("AuditActor", fields: [actorId], references: [id], onDelete: SetNull)
105
+
106
+ @@index([createdAt])
107
+ @@index([actorId])
108
+ }
109
+
110
+ enum Role {
111
+ USER
112
+ ADMIN
113
+ }
114
+
115
+ enum UserStatus {
116
+ ACTIVE
117
+ SUSPENDED
118
+ }
119
+
120
+ enum ProjectStatus {
121
+ DRAFT
122
+ ACTIVE
123
+ ARCHIVED
124
+ }
@@ -0,0 +1,36 @@
1
+ import bcrypt from "bcryptjs";
2
+ import { ProjectStatus, Role } from "@prisma/client";
3
+ import { prisma } from "../src/config/prisma.js";
4
+
5
+ async function main() {
6
+ const email = (process.env.SEED_ADMIN_EMAIL ?? "admin@example.com").toLowerCase();
7
+ const password = process.env.SEED_ADMIN_PASSWORD ?? "ChangeMe123!";
8
+ const admin = await prisma.user.upsert({
9
+ where: { email },
10
+ update: { role: Role.ADMIN, status: "ACTIVE" },
11
+ create: {
12
+ email,
13
+ name: "Workspace Admin",
14
+ passwordHash: await bcrypt.hash(password, 12),
15
+ role: Role.ADMIN,
16
+ emailVerifiedAt: new Date(),
17
+ },
18
+ });
19
+
20
+ const existingProject = await prisma.project.findFirst({ where: { ownerId: admin.id } });
21
+ if (!existingProject) {
22
+ await prisma.project.create({
23
+ data: {
24
+ name: "Launch workspace",
25
+ description: "Your first complete Authenik8 feature slice.",
26
+ status: ProjectStatus.ACTIVE,
27
+ ownerId: admin.id,
28
+ },
29
+ });
30
+ }
31
+
32
+ console.log(`Seeded administrator ${email}`);
33
+ }
34
+
35
+ main()
36
+ .finally(() => prisma.$disconnect());
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import dotenv from "dotenv";
4
+ import { defineConfig, env } from "prisma/config";
5
+
6
+ const rootEnv = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.env");
7
+ dotenv.config({ path: process.env.AUTHENIK8_ENV_FILE ?? rootEnv });
8
+
9
+ export default defineConfig({
10
+ schema: "prisma/schema.prisma",
11
+ migrations: {
12
+ path: "prisma/migrations",
13
+ seed: "tsx prisma/seed.ts",
14
+ },
15
+ datasource: {
16
+ url: env("DATABASE_URL"),
17
+ },
18
+ });
@@ -0,0 +1,68 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import cookieParser from "cookie-parser";
4
+ import cors from "cors";
5
+ import express from "express";
6
+ import { pinoHttp } from "pino-http";
7
+ import { getAuthenik8, redis } from "./auth/authenik8.js";
8
+ import { authRoutes } from "./auth/auth.routes.js";
9
+ import { env } from "./config/env.js";
10
+ import { logger } from "./config/logger.js";
11
+ import { isAllowedOrigin } from "./config/origins.js";
12
+ import { prisma } from "./config/prisma.js";
13
+ import { requestId } from "./middleware/request-id.js";
14
+ import { adminRoutes } from "./modules/admin/admin.routes.js";
15
+ import { projectRoutes } from "./modules/projects/project.routes.js";
16
+ import { userRoutes } from "./modules/users/user.routes.js";
17
+ import { AppError, asyncHandler, errorHandler, notFound } from "./utils/http.js";
18
+ import { openApiDocument } from "./openapi.js";
19
+
20
+ export function createApp() {
21
+ const app = express();
22
+ if (env.TRUST_PROXY) app.set("trust proxy", 1);
23
+
24
+ app.use(requestId);
25
+ app.use(pinoHttp({ logger, genReqId: (req) => req.id }));
26
+ app.use(cors({
27
+ credentials: true,
28
+ origin(origin, callback) {
29
+ if (!origin || isAllowedOrigin(origin)) return callback(null, true);
30
+ callback(new AppError(403, "ORIGIN_REJECTED", "Request origin is not allowed"));
31
+ },
32
+ }));
33
+ app.use(getAuthenik8().helmet);
34
+ // authenik8-core provides the Redis-backed limiter for every route below.
35
+ app.use(getAuthenik8().rateLimit);
36
+ app.use(express.json({ limit: "32kb", strict: true }));
37
+ app.use(cookieParser());
38
+
39
+ app.get("/api/health/live", (_req, res) => res.json({ status: "ok" }));
40
+ app.get("/.well-known/jwks.json", (_req, res) => res.json(getAuthenik8().getJwks()));
41
+ app.get("/api/docs/openapi.json", (_req, res) => res.json(openApiDocument));
42
+ app.get("/api/health/ready", asyncHandler(async (_req, res) => {
43
+ await prisma.$queryRaw`SELECT 1`;
44
+ const pong = await redis.ping();
45
+ res.json({ status: "ready", database: "ok", redis: pong === "PONG" ? "ok" : "unavailable" });
46
+ }));
47
+
48
+ app.use("/api/auth", authRoutes);
49
+ app.use("/api/account", userRoutes);
50
+ app.use("/api/projects", projectRoutes);
51
+ app.use("/api/admin", adminRoutes);
52
+
53
+ if (env.NODE_ENV === "production") {
54
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
55
+ const webDist = path.resolve(currentDir, "../../web/dist");
56
+ app.use(express.static(webDist, { index: false, maxAge: "1h" }));
57
+
58
+ // codeql[js/missing-rate-limiting] authenik8-core rate-limits this handler globally.
59
+ app.use((req, res, next) => {
60
+ if (req.method === "GET" && req.accepts("html")) return res.sendFile(path.join(webDist, "index.html"));
61
+ next();
62
+ });
63
+ }
64
+
65
+ app.use(notFound);
66
+ app.use(errorHandler);
67
+ return app;
68
+ }
@@ -0,0 +1,125 @@
1
+ import {
2
+ forgotPasswordSchema,
3
+ loginSchema,
4
+ oauthExchangeSchema,
5
+ oauthLinkQuerySchema,
6
+ oauthProviderSchema,
7
+ registerSchema,
8
+ resetPasswordSchema,
9
+ verificationSchema,
10
+ } from "@authenik8/contracts";
11
+ import { env } from "../config/env.js";
12
+ import { asyncHandler } from "../utils/http.js";
13
+ import { presentUser } from "../modules/users/user.presenter.js";
14
+ import { clearRefreshCookie, readRefreshCookie, setRefreshCookie } from "./cookies.js";
15
+ import { issueCsrfToken } from "../middleware/csrf.js";
16
+ import {
17
+ completeOAuthCallback,
18
+ consumeLinkIntent,
19
+ createLinkIntent,
20
+ exchangeOAuthCode,
21
+ login,
22
+ oauthProvider,
23
+ register,
24
+ requestPasswordReset,
25
+ resendVerification,
26
+ resetPassword,
27
+ revokeRefreshToken,
28
+ rotateSession,
29
+ verifyEmail,
30
+ } from "./auth.service.js";
31
+ import { prisma } from "../config/prisma.js";
32
+
33
+ export const csrfTokenController = asyncHandler(async (req, res) => {
34
+ res.set("Cache-Control", "no-store");
35
+ res.json({ csrfToken: issueCsrfToken(req, res) });
36
+ });
37
+
38
+ export const registerController = asyncHandler(async (req, res) => {
39
+ res.status(201).json(await register(registerSchema.parse(req.body)));
40
+ });
41
+
42
+ export const loginController = asyncHandler(async (req, res) => {
43
+ const session = await login(loginSchema.parse(req.body), req);
44
+ setRefreshCookie(res, session.refreshToken);
45
+ res.json({ accessToken: session.accessToken, user: session.user });
46
+ });
47
+
48
+ export const refreshController = asyncHandler(async (req, res) => {
49
+ const session = await rotateSession(readRefreshCookie(req));
50
+ setRefreshCookie(res, session.refreshToken);
51
+ res.json({ accessToken: session.accessToken, user: session.user });
52
+ });
53
+
54
+ export const logoutController = asyncHandler(async (req, res) => {
55
+ await revokeRefreshToken(readRefreshCookie(req));
56
+ clearRefreshCookie(res);
57
+ res.json({ message: "Signed out" });
58
+ });
59
+
60
+ export const meController = asyncHandler(async (req, res) => {
61
+ const user = await prisma.user.findUniqueOrThrow({ where: { id: req.user!.userId } });
62
+ res.json({ user: presentUser(user) });
63
+ });
64
+
65
+ export const forgotPasswordController = asyncHandler(async (req, res) => {
66
+ const { email } = forgotPasswordSchema.parse(req.body);
67
+ res.json(await requestPasswordReset(email));
68
+ });
69
+
70
+ export const resetPasswordController = asyncHandler(async (req, res) => {
71
+ const input = resetPasswordSchema.parse(req.body);
72
+ await resetPassword(input.token, input.password);
73
+ clearRefreshCookie(res);
74
+ res.json({ message: "Password updated" });
75
+ });
76
+
77
+ export const verifyEmailController = asyncHandler(async (req, res) => {
78
+ const { token } = verificationSchema.parse(req.body);
79
+ await verifyEmail(token);
80
+ res.json({ message: "Email verified" });
81
+ });
82
+
83
+ export const resendVerificationController = asyncHandler(async (req, res) => {
84
+ res.json(await resendVerification(req.user!.userId));
85
+ });
86
+
87
+ export const oauthRedirectController = asyncHandler(async (req, res) => {
88
+ await oauthProvider(oauthProviderSchema.parse(req.params.provider)).provider.redirect(req, res);
89
+ });
90
+
91
+ export const oauthLinkIntentController = asyncHandler(async (req, res) => {
92
+ const url = await createLinkIntent(req.user!.userId, oauthProviderSchema.parse(req.params.provider));
93
+ res.json({ url });
94
+ });
95
+
96
+ export const oauthLinkRedirectController = asyncHandler(async (req, res) => {
97
+ const { ticket } = oauthLinkQuerySchema.parse(req.query);
98
+ const userId = await consumeLinkIntent(ticket);
99
+ req.user = { userId, email: "", name: "", role: "USER" };
100
+ await oauthProvider(oauthProviderSchema.parse(req.params.provider)).provider.redirect(req, res, "link");
101
+ });
102
+
103
+ export const oauthCallbackController = asyncHandler(async (req, res) => {
104
+ try {
105
+ const providerName = oauthProviderSchema.parse(req.params.provider);
106
+ const { provider } = oauthProvider(providerName);
107
+ const result = await provider.handleCallback(req);
108
+ const completed = await completeOAuthCallback(providerName, result, req);
109
+ if (completed.linked) {
110
+ res.redirect(`${env.WEB_ORIGIN}/settings/security?linked=${completed.provider}`);
111
+ return;
112
+ }
113
+ res.redirect(`${env.WEB_ORIGIN}/auth/callback?code=${completed.code}`);
114
+ } catch (error) {
115
+ req.log?.warn({ err: error }, "OAuth callback rejected");
116
+ res.redirect(`${env.WEB_ORIGIN}/login?oauthError=1`);
117
+ }
118
+ });
119
+
120
+ export const oauthExchangeController = asyncHandler(async (req, res) => {
121
+ const { code } = oauthExchangeSchema.parse(req.body);
122
+ const session = await exchangeOAuthCode(code);
123
+ setRefreshCookie(res, session.refreshToken);
124
+ res.json({ accessToken: session.accessToken, user: session.user });
125
+ });
@@ -0,0 +1,50 @@
1
+ import { Router } from "express";
2
+ import { authenticate } from "../middleware/authenticate.js";
3
+ import { requireCsrf } from "../middleware/csrf.js";
4
+ import { requireAllowedOrigin } from "../middleware/origin.js";
5
+ import {
6
+ csrfTokenController,
7
+ forgotPasswordController,
8
+ loginController,
9
+ logoutController,
10
+ meController,
11
+ oauthCallbackController,
12
+ oauthExchangeController,
13
+ oauthLinkIntentController,
14
+ oauthLinkRedirectController,
15
+ oauthRedirectController,
16
+ refreshController,
17
+ registerController,
18
+ resendVerificationController,
19
+ resetPasswordController,
20
+ verifyEmailController,
21
+ } from "./auth.controller.js";
22
+
23
+ export const authRoutes = Router();
24
+
25
+ authRoutes.get("/csrf", csrfTokenController);
26
+ authRoutes.post("/register", requireAllowedOrigin, requireCsrf, registerController);
27
+ // These routes are mounted after authenik8-core's global Redis-backed limiter.
28
+ // codeql[js/missing-rate-limiting]
29
+ authRoutes.post("/login", requireAllowedOrigin, requireCsrf, loginController);
30
+ authRoutes.post("/refresh", requireAllowedOrigin, requireCsrf, refreshController);
31
+ authRoutes.post("/logout", requireAllowedOrigin, requireCsrf, logoutController);
32
+ // codeql[js/missing-rate-limiting]
33
+ authRoutes.get("/me", authenticate, meController);
34
+ authRoutes.post("/forgot-password", requireAllowedOrigin, requireCsrf, forgotPasswordController);
35
+ authRoutes.post("/reset-password", requireAllowedOrigin, requireCsrf, resetPasswordController);
36
+ // codeql[js/missing-rate-limiting]
37
+ authRoutes.post("/verify-email", requireAllowedOrigin, requireCsrf, verifyEmailController);
38
+ // codeql[js/missing-rate-limiting]
39
+ authRoutes.post("/resend-verification", requireAllowedOrigin, requireCsrf, authenticate, resendVerificationController);
40
+
41
+ // codeql[js/missing-rate-limiting]
42
+ authRoutes.get("/oauth/:provider", oauthRedirectController);
43
+ // codeql[js/missing-rate-limiting]
44
+ authRoutes.post("/oauth/:provider/link-intent", requireAllowedOrigin, requireCsrf, authenticate, oauthLinkIntentController);
45
+ // codeql[js/missing-rate-limiting]
46
+ authRoutes.get("/oauth/:provider/link", oauthLinkRedirectController);
47
+ // codeql[js/missing-rate-limiting]
48
+ authRoutes.get("/oauth/:provider/callback", oauthCallbackController);
49
+ // codeql[js/missing-rate-limiting]
50
+ authRoutes.post("/oauth/exchange", requireAllowedOrigin, requireCsrf, oauthExchangeController);