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.
- package/README.md +160 -57
- package/assets/logo.svg +53 -0
- package/dist/src/bin/index.js +199 -125
- package/dist/src/bin/index.js.map +1 -1
- package/dist/src/lib/args.d.ts +18 -0
- package/dist/src/lib/args.d.ts.map +1 -0
- package/dist/src/lib/args.js +82 -0
- package/dist/src/lib/args.js.map +1 -0
- package/dist/src/lib/constants.d.ts +1 -0
- package/dist/src/lib/constants.d.ts.map +1 -1
- package/dist/src/lib/constants.js +10 -11
- package/dist/src/lib/constants.js.map +1 -1
- package/dist/src/lib/preflight.d.ts +3 -0
- package/dist/src/lib/preflight.d.ts.map +1 -0
- package/dist/src/lib/preflight.js +21 -0
- package/dist/src/lib/preflight.js.map +1 -0
- package/dist/src/lib/process.d.ts +6 -2
- package/dist/src/lib/process.d.ts.map +1 -1
- package/dist/src/lib/process.js +66 -7
- package/dist/src/lib/process.js.map +1 -1
- package/dist/src/lib/projectName.d.ts +2 -0
- package/dist/src/lib/projectName.d.ts.map +1 -0
- package/dist/src/lib/projectName.js +6 -0
- package/dist/src/lib/projectName.js.map +1 -0
- package/dist/src/lib/schemas.d.ts +192 -0
- package/dist/src/lib/schemas.d.ts.map +1 -0
- package/dist/src/lib/schemas.js +171 -0
- package/dist/src/lib/schemas.js.map +1 -0
- package/dist/src/lib/state.d.ts.map +1 -1
- package/dist/src/lib/state.js +20 -3
- package/dist/src/lib/state.js.map +1 -1
- package/dist/src/lib/types.d.ts +6 -13
- package/dist/src/lib/types.d.ts.map +1 -1
- package/dist/src/lib/ui.d.ts +8 -3
- package/dist/src/lib/ui.d.ts.map +1 -1
- package/dist/src/lib/ui.js +76 -112
- package/dist/src/lib/ui.js.map +1 -1
- package/dist/src/steps/configurePrisma.d.ts.map +1 -1
- package/dist/src/steps/configurePrisma.js +83 -9
- package/dist/src/steps/configurePrisma.js.map +1 -1
- package/dist/src/steps/createProject.d.ts +2 -1
- package/dist/src/steps/createProject.d.ts.map +1 -1
- package/dist/src/steps/createProject.js +149 -28
- package/dist/src/steps/createProject.js.map +1 -1
- package/dist/src/steps/finalSetup.d.ts +3 -3
- package/dist/src/steps/finalSetup.d.ts.map +1 -1
- package/dist/src/steps/finalSetup.js +24 -28
- package/dist/src/steps/finalSetup.js.map +1 -1
- package/dist/src/steps/installAuth.d.ts +1 -1
- package/dist/src/steps/installAuth.d.ts.map +1 -1
- package/dist/src/steps/installAuth.js +5 -4
- package/dist/src/steps/installAuth.js.map +1 -1
- package/dist/src/steps/installDeps.d.ts +11 -4
- package/dist/src/steps/installDeps.d.ts.map +1 -1
- package/dist/src/steps/installDeps.js +45 -77
- package/dist/src/steps/installDeps.js.map +1 -1
- package/dist/src/steps/prompts.d.ts +1 -1
- package/dist/src/steps/prompts.d.ts.map +1 -1
- package/dist/src/steps/prompts.js +42 -30
- package/dist/src/steps/prompts.js.map +1 -1
- package/dist/src/utils/jwk.d.ts +2 -0
- package/dist/src/utils/jwk.d.ts.map +1 -0
- package/dist/src/utils/jwk.js +44 -0
- package/dist/src/utils/jwk.js.map +1 -0
- package/dist/src/utils/output.d.ts +1 -1
- package/dist/src/utils/output.d.ts.map +1 -1
- package/dist/src/utils/output.js +75 -68
- package/dist/src/utils/output.js.map +1 -1
- package/package.json +17 -9
- package/templates/AGENT_IDENTITY.md +112 -0
- package/templates/THREAT_MODEL.md +27 -5
- package/templates/express-auth/README.md +13 -10
- package/templates/express-auth/docker-compose.yml +10 -0
- package/templates/express-auth/gitignore.template +6 -0
- package/templates/express-auth/package.json +13 -6
- package/templates/express-auth/src/app.ts +2 -0
- package/templates/express-auth/src/controllers/auth.controller.ts +2 -6
- package/templates/express-auth/src/controllers/protected.controller.ts +25 -10
- package/templates/express-auth/src/middleware/auth.middleware.ts +1 -0
- package/templates/express-auth/src/routes/protected.routes.ts +3 -1
- package/templates/express-auth/src/server.ts +6 -12
- package/templates/express-auth/src/utils/security.ts +119 -32
- package/templates/express-auth+/README.md +13 -10
- package/templates/express-auth+/docker-compose.yml +10 -0
- package/templates/express-auth+/gitignore.template +6 -0
- package/templates/express-auth+/package.json +13 -6
- package/templates/express-auth+/src/auth/auth.ts +3 -2
- package/templates/express-auth+/src/auth/controllers/password.controller.ts +3 -7
- package/templates/express-auth+/src/auth/controllers/protected.controller.ts +26 -11
- package/templates/express-auth+/src/auth/identity.adapter.ts +90 -0
- package/templates/express-auth+/src/auth/middleware/auth.middleware.ts +3 -4
- package/templates/express-auth+/src/auth/routes/protected.routes.ts +2 -2
- package/templates/express-auth+/src/oauth-providers/github/src/auth/auth.ts +3 -2
- package/templates/express-auth+/src/oauth-providers/google/src/auth/auth.ts +3 -2
- package/templates/express-auth+/src/oauth-providers/google-github/src/auth/auth.ts +3 -2
- package/templates/express-auth+/src/server.ts +5 -10
- package/templates/express-auth+/src/utils/security.ts +109 -28
- package/templates/express-base/README.md +13 -6
- package/templates/express-base/app.ts +2 -0
- package/templates/express-base/controllers/base.controller.ts +33 -19
- package/templates/express-base/docker-compose.yml +10 -0
- package/templates/express-base/gitignore.template +6 -0
- package/templates/express-base/package.json +11 -5
- package/templates/express-base/routes/base.routes.ts +1 -1
- package/templates/express-base/src/.env.example +7 -2
- package/templates/express-base/src/server.ts +6 -12
- package/templates/express-base/tsconfig.json +7 -40
- package/templates/express-base/utils/security.ts +111 -15
- package/templates/fullstack/.env.example +24 -0
- package/templates/fullstack/PRESET_CONTRACT.md +46 -0
- package/templates/fullstack/README.md +65 -0
- package/templates/fullstack/THREAT_MODEL.md +30 -0
- package/templates/fullstack/apps/api/package.json +48 -0
- package/templates/fullstack/apps/api/prisma/schema.prisma +124 -0
- package/templates/fullstack/apps/api/prisma/seed.ts +36 -0
- package/templates/fullstack/apps/api/prisma.config.ts +18 -0
- package/templates/fullstack/apps/api/src/app.ts +68 -0
- package/templates/fullstack/apps/api/src/auth/auth.controller.ts +125 -0
- package/templates/fullstack/apps/api/src/auth/auth.routes.ts +50 -0
- package/templates/fullstack/apps/api/src/auth/auth.service.ts +257 -0
- package/templates/fullstack/apps/api/src/auth/authenik8.ts +67 -0
- package/templates/fullstack/apps/api/src/auth/cookies.ts +36 -0
- package/templates/fullstack/apps/api/src/config/env.ts +72 -0
- package/templates/fullstack/apps/api/src/config/logger.ts +17 -0
- package/templates/fullstack/apps/api/src/config/mailer.ts +32 -0
- package/templates/fullstack/apps/api/src/config/origins.ts +27 -0
- package/templates/fullstack/apps/api/src/config/prisma.ts +9 -0
- package/templates/fullstack/apps/api/src/middleware/authenticate.ts +35 -0
- package/templates/fullstack/apps/api/src/middleware/csrf.ts +60 -0
- package/templates/fullstack/apps/api/src/middleware/origin.ts +12 -0
- package/templates/fullstack/apps/api/src/middleware/request-id.ts +9 -0
- package/templates/fullstack/apps/api/src/modules/admin/admin.controller.ts +17 -0
- package/templates/fullstack/apps/api/src/modules/admin/admin.routes.ts +14 -0
- package/templates/fullstack/apps/api/src/modules/admin/admin.service.ts +54 -0
- package/templates/fullstack/apps/api/src/modules/projects/project.controller.ts +22 -0
- package/templates/fullstack/apps/api/src/modules/projects/project.policy.ts +14 -0
- package/templates/fullstack/apps/api/src/modules/projects/project.repository.ts +20 -0
- package/templates/fullstack/apps/api/src/modules/projects/project.routes.ts +21 -0
- package/templates/fullstack/apps/api/src/modules/projects/project.service.ts +41 -0
- package/templates/fullstack/apps/api/src/modules/users/user.controller.ts +26 -0
- package/templates/fullstack/apps/api/src/modules/users/user.presenter.ts +13 -0
- package/templates/fullstack/apps/api/src/modules/users/user.routes.ts +21 -0
- package/templates/fullstack/apps/api/src/modules/users/user.service.ts +71 -0
- package/templates/fullstack/apps/api/src/openapi.ts +32 -0
- package/templates/fullstack/apps/api/src/server.ts +29 -0
- package/templates/fullstack/apps/api/src/types.ts +15 -0
- package/templates/fullstack/apps/api/src/utils/crypto.ts +4 -0
- package/templates/fullstack/apps/api/src/utils/http.ts +33 -0
- package/templates/fullstack/apps/api/src/utils/sealed-value.ts +37 -0
- package/templates/fullstack/apps/api/tests/project.policy.test.ts +26 -0
- package/templates/fullstack/apps/api/tests/registration.test.ts +69 -0
- package/templates/fullstack/apps/api/tests/security.test.ts +98 -0
- package/templates/fullstack/apps/api/tests/validation.test.ts +37 -0
- package/templates/fullstack/apps/api/tsconfig.json +16 -0
- package/templates/fullstack/apps/api/vitest.config.ts +9 -0
- package/templates/fullstack/apps/web/index.html +14 -0
- package/templates/fullstack/apps/web/package.json +31 -0
- package/templates/fullstack/apps/web/public/authenik8-logo.svg +53 -0
- package/templates/fullstack/apps/web/src/App.tsx +45 -0
- package/templates/fullstack/apps/web/src/auth/AuthProvider.tsx +53 -0
- package/templates/fullstack/apps/web/src/auth/RouteGuards.tsx +16 -0
- package/templates/fullstack/apps/web/src/auth/providers.ts +3 -0
- package/templates/fullstack/apps/web/src/components/AppShell.tsx +80 -0
- package/templates/fullstack/apps/web/src/components/AuthShell.tsx +26 -0
- package/templates/fullstack/apps/web/src/components/Page.tsx +30 -0
- package/templates/fullstack/apps/web/src/features/projects/ProjectPages.tsx +79 -0
- package/templates/fullstack/apps/web/src/main.tsx +24 -0
- package/templates/fullstack/apps/web/src/pages/DashboardPage.tsx +48 -0
- package/templates/fullstack/apps/web/src/pages/StatusPages.tsx +8 -0
- package/templates/fullstack/apps/web/src/pages/admin/AuditPage.tsx +19 -0
- package/templates/fullstack/apps/web/src/pages/admin/UsersPage.tsx +24 -0
- package/templates/fullstack/apps/web/src/pages/auth/LoginPage.tsx +51 -0
- package/templates/fullstack/apps/web/src/pages/auth/OAuthCallbackPage.tsx +26 -0
- package/templates/fullstack/apps/web/src/pages/auth/RecoveryPages.tsx +53 -0
- package/templates/fullstack/apps/web/src/pages/auth/RegisterPage.tsx +86 -0
- package/templates/fullstack/apps/web/src/pages/auth/VerifyEmailPage.tsx +24 -0
- package/templates/fullstack/apps/web/src/pages/settings/ProfilePage.tsx +34 -0
- package/templates/fullstack/apps/web/src/pages/settings/SecurityPage.tsx +57 -0
- package/templates/fullstack/apps/web/src/styles.css +289 -0
- package/templates/fullstack/apps/web/src/token-storage.test.ts +12 -0
- package/templates/fullstack/apps/web/tsconfig.app.json +19 -0
- package/templates/fullstack/apps/web/tsconfig.json +7 -0
- package/templates/fullstack/apps/web/tsconfig.node.json +10 -0
- package/templates/fullstack/apps/web/vite.config.ts +18 -0
- package/templates/fullstack/docker-compose.yml +35 -0
- package/templates/fullstack/docs/PRODUCTION.md +30 -0
- package/templates/fullstack/gitignore.template +8 -0
- package/templates/fullstack/package.json +39 -0
- package/templates/fullstack/packages/api-client/package.json +20 -0
- package/templates/fullstack/packages/api-client/src/index.ts +203 -0
- package/templates/fullstack/packages/api-client/tsconfig.json +14 -0
- package/templates/fullstack/packages/contracts/package.json +20 -0
- package/templates/fullstack/packages/contracts/src/index.ts +146 -0
- package/templates/fullstack/packages/contracts/tsconfig.json +13 -0
- package/templates/fullstack/packages/ui/package.json +22 -0
- package/templates/fullstack/packages/ui/src/index.tsx +33 -0
- package/templates/fullstack/packages/ui/tsconfig.json +14 -0
- package/templates/prisma/postgresql/.env.example +6 -2
- package/templates/prisma/postgresql/client.ts +17 -0
- package/templates/prisma/postgresql/prisma.config.ts +12 -0
- package/templates/prisma/postgresql/schema.prisma +0 -1
- package/templates/prisma/sqlite/.env.example +6 -2
- package/templates/prisma/sqlite/client.ts +16 -0
- package/templates/prisma/sqlite/prisma.config.ts +12 -0
- package/templates/prisma/sqlite/schema.prisma +0 -1
- package/dist/src/tests/templated-routes.test.d.ts +0 -2
- package/dist/src/tests/templated-routes.test.d.ts.map +0 -1
- package/dist/src/tests/templated-routes.test.js +0 -325
- package/dist/src/tests/templated-routes.test.js.map +0 -1
- package/dist/templates/THREAT_MODEL.md +0 -138
- package/dist/templates/express-auth/src/controllers/protected.controller.d.ts +0 -8
- package/dist/templates/express-auth/src/controllers/protected.controller.d.ts.map +0 -1
- package/dist/templates/express-auth/src/controllers/protected.controller.js +0 -50
- package/dist/templates/express-auth/src/controllers/protected.controller.js.map +0 -1
- package/dist/templates/express-auth/src/routes/protected.routes.d.ts +0 -2
- package/dist/templates/express-auth/src/routes/protected.routes.d.ts.map +0 -1
- package/dist/templates/express-auth/src/routes/protected.routes.js +0 -16
- package/dist/templates/express-auth/src/routes/protected.routes.js.map +0 -1
- package/dist/templates/express-auth/src/utils/security.d.ts +0 -8
- package/dist/templates/express-auth/src/utils/security.d.ts.map +0 -1
- package/dist/templates/express-auth/src/utils/security.js +0 -55
- package/dist/templates/express-auth/src/utils/security.js.map +0 -1
- package/dist/templates/express-base/controllers/base.controller.d.ts +0 -12
- package/dist/templates/express-base/controllers/base.controller.d.ts.map +0 -1
- package/dist/templates/express-base/controllers/base.controller.js +0 -77
- package/dist/templates/express-base/controllers/base.controller.js.map +0 -1
- package/dist/templates/express-base/routes/base.routes.d.ts +0 -2
- package/dist/templates/express-base/routes/base.routes.d.ts.map +0 -1
- package/dist/templates/express-base/routes/base.routes.js +0 -20
- package/dist/templates/express-base/routes/base.routes.js.map +0 -1
- package/dist/templates/express-base/utils/security.d.ts +0 -5
- package/dist/templates/express-base/utils/security.d.ts.map +0 -1
- package/dist/templates/express-base/utils/security.js +0 -40
- package/dist/templates/express-base/utils/security.js.map +0 -1
- 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);
|