create-authenik8-app 2.4.3 → 2.4.5
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 +151 -4
- package/package.json +10 -8
- package/templates/THREAT_MODEL.md +138 -0
- package/templates/express-auth/README.md +196 -0
- package/templates/express-auth/docker-compose.yml +23 -0
- package/templates/express-auth/package.json +4 -2
- package/templates/express-auth/src/app.ts +1 -1
- package/templates/express-auth/src/server.ts +1 -1
- package/templates/express-auth+/README.md +247 -0
- package/templates/express-auth+/docker-compose.yml +23 -0
- package/templates/express-auth+/package.json +5 -3
- package/templates/express-auth+/src/auth/auth.ts +6 -16
- package/templates/express-auth+/src/auth/controllers/oauth.controller.ts +1 -0
- package/templates/express-auth+/src/auth/{password.controller.ts → controllers/password.controller.ts} +4 -4
- package/templates/express-auth+/src/auth/{protected.controller.ts → controllers/protected.controller.ts} +2 -2
- package/templates/express-auth+/src/auth/{auth.middleware.ts → middleware/auth.middleware.ts} +1 -1
- package/templates/express-auth+/src/auth/routes/oauth.routes.ts +5 -0
- package/templates/express-auth+/src/auth/{password.route.ts → routes/password.route.ts} +1 -1
- package/templates/express-auth+/src/auth/{protected.routes.ts → routes/protected.routes.ts} +2 -2
- package/templates/express-auth+/src/oauth-providers/github/src/auth/auth.ts +42 -0
- package/templates/express-auth+/src/oauth-providers/github/src/auth/oauth.controller.ts +37 -0
- package/templates/express-auth+/src/oauth-providers/github/src/auth/oauth.routes.ts +11 -0
- package/templates/express-auth+/src/oauth-providers/google/src/auth/auth.ts +42 -0
- package/templates/express-auth+/src/oauth-providers/google/src/auth/oauth.controller.ts +37 -0
- package/templates/express-auth+/src/oauth-providers/google/src/auth/oauth.routes.ts +11 -0
- package/templates/express-auth+/src/oauth-providers/google-github/src/auth/auth.ts +47 -0
- package/templates/express-auth+/src/oauth-providers/google-github/src/auth/oauth.controller.ts +57 -0
- package/templates/express-auth+/src/server.ts +3 -3
- package/templates/express-base/README.md +113 -0
- package/templates/express-base/app.ts +1 -1
- package/templates/express-base/docker-compose.yml +23 -0
- package/templates/express-base/package.json +4 -2
- package/templates/express-base/src/package-lock.json +0 -1
- package/templates/express-base/src/server.ts +1 -1
- package/templates/prisma/postgresql/.env.example +11 -0
- package/templates/prisma/sqlite/.env.example +11 -0
- package/templates/express-auth+/src/auth/oauth.controller.ts +0 -38
- package/templates/express-auth+/src/{auth → oauth-providers/google-github/src/auth}/oauth.routes.ts +1 -1
package/README.md
CHANGED
|
@@ -34,12 +34,13 @@
|
|
|
34
34
|
|
|
35
35
|
Create a new project:
|
|
36
36
|
|
|
37
|
-
```
|
|
38
|
-
bash
|
|
37
|
+
```bash
|
|
39
38
|
npx create-authenik8-app my-app
|
|
40
39
|
|
|
41
40
|
cd my-app
|
|
42
41
|
|
|
42
|
+
npm run prisma:migrate
|
|
43
|
+
|
|
43
44
|
redis-server --daemonize yes
|
|
44
45
|
|
|
45
46
|
npm run dev
|
|
@@ -112,12 +113,24 @@ Generated automatically:
|
|
|
112
113
|
The CLI generates these automatically:
|
|
113
114
|
|
|
114
115
|
```
|
|
116
|
+
DATABASE_URL=file:./dev.db
|
|
115
117
|
JWT_SECRET=your-secret
|
|
116
118
|
REFRESH_SECRET=your-refresh-secret
|
|
117
119
|
REDIS_HOST=127.0.0.1
|
|
118
120
|
REDIS_PORT=6379
|
|
119
121
|
```
|
|
120
122
|
|
|
123
|
+
For Full Auth (Password + OAuth), also set:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
GOOGLE_CLIENT_ID=your-google-client-id
|
|
127
|
+
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
|
128
|
+
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
|
|
129
|
+
GITHUB_CLIENT_ID=your-github-client-id
|
|
130
|
+
GITHUB_CLIENT_SECRET=your-github-client-secret
|
|
131
|
+
GITHUB_REDIRECT_URI=http://localhost:3000/auth/github/callback
|
|
132
|
+
```
|
|
133
|
+
|
|
121
134
|
|
|
122
135
|
---
|
|
123
136
|
|
|
@@ -162,7 +175,142 @@ This design makes future additions (MFA, WebAuthn, etc.) much cleaner.
|
|
|
162
175
|
---
|
|
163
176
|
## Powered by
|
|
164
177
|
|
|
165
|
-
authenik8-core (v1.0.
|
|
178
|
+
authenik8-core (v1.0.33) identity & token engine(beta)
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## How authenik8-core works in generated apps
|
|
183
|
+
|
|
184
|
+
Generated projects call:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const auth = await createAuthenik8({
|
|
188
|
+
jwtSecret: requiredSecret("JWT_SECRET"),
|
|
189
|
+
refreshSecret: requiredSecret("REFRESH_SECRET"),
|
|
190
|
+
oauth: {
|
|
191
|
+
google: {
|
|
192
|
+
clientId: requiredEnv("GOOGLE_CLIENT_ID"),
|
|
193
|
+
clientSecret: requiredEnv("GOOGLE_CLIENT_SECRET"),
|
|
194
|
+
redirectUri: requiredEnv("GOOGLE_REDIRECT_URI"),
|
|
195
|
+
},
|
|
196
|
+
github: {
|
|
197
|
+
clientId: requiredEnv("GITHUB_CLIENT_ID"),
|
|
198
|
+
clientSecret: requiredEnv("GITHUB_CLIENT_SECRET"),
|
|
199
|
+
redirectUri: requiredEnv("GITHUB_REDIRECT_URI"),
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
That factory returns one auth object used by the generated routes:
|
|
206
|
+
|
|
207
|
+
• `signToken(payload)` creates access tokens.
|
|
208
|
+
|
|
209
|
+
• `verifyToken(token)` verifies access tokens.
|
|
210
|
+
|
|
211
|
+
• `generateRefreshToken(payload)` creates stateful refresh tokens.
|
|
212
|
+
|
|
213
|
+
• `refreshToken(refreshToken)` rotates refresh tokens and returns a new access/refresh pair.
|
|
214
|
+
|
|
215
|
+
• `helmet`, `rateLimit`, and `ipWhitelist` are Express middleware.
|
|
216
|
+
|
|
217
|
+
• `requireAdmin` protects admin-only routes by checking `role: "admin"`.
|
|
218
|
+
|
|
219
|
+
• `oauth.google` and `oauth.github` provide redirect and callback handlers.
|
|
220
|
+
|
|
221
|
+
• `issueTokensFromProfile(profile)` turns a verified OAuth profile into app tokens through the Identity Engine.
|
|
222
|
+
|
|
223
|
+
### Redis-backed token lifecycle
|
|
224
|
+
|
|
225
|
+
Authenik8-core intentionally makes JWT auth stateful:
|
|
226
|
+
|
|
227
|
+
1. Access tokens are signed with `JWT_SECRET`.
|
|
228
|
+
2. Refresh tokens are signed with `REFRESH_SECRET` and include a unique `jti`.
|
|
229
|
+
3. The current valid refresh token is stored in Redis under `refresh:<userId>`.
|
|
230
|
+
4. Refresh calls acquire a Redis lock with `lock:<userId>`.
|
|
231
|
+
5. The submitted refresh token must match the Redis value.
|
|
232
|
+
6. A new access token and refresh token are issued.
|
|
233
|
+
7. The new refresh token atomically replaces the old one.
|
|
234
|
+
8. Reusing the old refresh token fails.
|
|
235
|
+
|
|
236
|
+
This is why Redis is required. It enables refresh-token replay protection, concurrent refresh protection, and server-side session control.
|
|
237
|
+
|
|
238
|
+
### OAuth identity resolution
|
|
239
|
+
|
|
240
|
+
OAuth is not handled as separate unrelated Passport-style strategies. Provider callbacks are normalized into this profile shape:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
{
|
|
244
|
+
email: "user@example.com",
|
|
245
|
+
name: "User Name",
|
|
246
|
+
provider: "google",
|
|
247
|
+
providerId: "provider-user-id",
|
|
248
|
+
email_verified: true
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The Identity Engine then decides:
|
|
253
|
+
|
|
254
|
+
• Existing provider login: provider is already linked, so tokens are issued.
|
|
255
|
+
|
|
256
|
+
• New user creation: no matching identity exists, so a new user identity is created.
|
|
257
|
+
|
|
258
|
+
• Link required: an email match exists but policy requires explicit account linking.
|
|
259
|
+
|
|
260
|
+
• Link provider: an authenticated user links Google or GitHub to their existing account.
|
|
261
|
+
|
|
262
|
+
OAuth state is stored in Redis for five minutes under `oauth:state:<state>`, and Redis-backed identity records use:
|
|
263
|
+
|
|
264
|
+
```text
|
|
265
|
+
oauth:v1:user:<userId>
|
|
266
|
+
oauth:v1:email:<email>
|
|
267
|
+
oauth:v1:provider:<provider>:<providerId>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Security middleware
|
|
271
|
+
|
|
272
|
+
Generated apps use the middleware returned by core:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
app.use(auth.helmet);
|
|
276
|
+
app.use(auth.rateLimit);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`helmet` applies secure HTTP headers. `rateLimit` is Redis-backed and defaults to 100 requests per 60 seconds with a 300-second block. `ipWhitelist` is available for stricter APIs and allows localhost by default.
|
|
280
|
+
|
|
281
|
+
### Common core errors
|
|
282
|
+
|
|
283
|
+
• `MissingTokenError`: no refresh token was sent.
|
|
284
|
+
|
|
285
|
+
• `InvalidTokenError`: refresh token is invalid, expired, reused, or replaced.
|
|
286
|
+
|
|
287
|
+
• `Concurrent refresh detected`: two refresh requests tried to rotate the same token at once.
|
|
288
|
+
|
|
289
|
+
• `OAuthError:Invalid or expired state`: OAuth callback state is missing from Redis.
|
|
290
|
+
|
|
291
|
+
• `OAuth profile email must be verified before issuing tokens`: provider email was not verified.
|
|
292
|
+
|
|
293
|
+
• `Provider already linked to another user`: account linking tried to attach an already-owned provider.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Threat Model
|
|
298
|
+
|
|
299
|
+
Generated apps include a `THREAT_MODEL.md` file. It explains:
|
|
300
|
+
|
|
301
|
+
• what the generated app protects,
|
|
302
|
+
|
|
303
|
+
• what `authenik8-core` handles with Redis-backed token state,
|
|
304
|
+
|
|
305
|
+
• what threats remain your responsibility,
|
|
306
|
+
|
|
307
|
+
• and what must be configured before production.
|
|
308
|
+
|
|
309
|
+
Key protections include refresh-token replay detection, concurrent refresh locking, OAuth state validation, verified-email OAuth token issuance, Redis-backed rate limiting, secure headers, session tracking, and admin-route checks.
|
|
310
|
+
|
|
311
|
+
Key non-goals include frontend XSS protection, CSRF for cookie-based auth, object-level authorization, MFA/WebAuthn, password reset, provider dashboard security, and protection from leaked secrets.
|
|
312
|
+
|
|
313
|
+
Before production, replace generated secrets, keep Redis private, use HTTPS, review CORS, configure exact OAuth callback URLs, and add business-level authorization checks to your own routes.
|
|
166
314
|
|
|
167
315
|
---
|
|
168
316
|
|
|
@@ -289,4 +437,3 @@ The Identity Engine is what makes Authenik8 feel like a coherent **authenticatio
|
|
|
289
437
|
• MFA
|
|
290
438
|
|
|
291
439
|
• Production presets
|
|
292
|
-
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-authenik8-app",
|
|
3
|
-
"version": "2.4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.4.5",
|
|
4
|
+
"description": " Fast Express + TypeScript auth starter with secure JWT, refresh rotation, Redis, RBAC, OAuth & Prisma.\nPowered by the Authenik8 Identity Engine.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"create-authenik8-app": "dist/index.js"
|
|
6
|
+
"create-authenik8-app": "dist/src/bin/index.js"
|
|
7
7
|
},
|
|
8
8
|
"keywords": [
|
|
9
9
|
"cli",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"fs-extra": "^11.3.4",
|
|
36
36
|
"inquirer": "^13.3.2",
|
|
37
37
|
"ora": "^9.3.0",
|
|
38
|
-
"tsx": "^4.
|
|
38
|
+
"tsx": "^4.22.4",
|
|
39
|
+
"typescript": "^6.0.3"
|
|
39
40
|
},
|
|
40
41
|
"files": [
|
|
41
42
|
"dist",
|
|
@@ -45,22 +46,23 @@
|
|
|
45
46
|
"test": "vitest run",
|
|
46
47
|
"test:watch": "vitest",
|
|
47
48
|
"test:coverage": "vitest run --coverage",
|
|
48
|
-
"test:templates": "node --import tsx --test tests/template-servers.test.mjs"
|
|
49
|
+
"test:templates": "node --import tsx --test tests/template-servers.test.mjs",
|
|
50
|
+
"build": "tsc && cp -r ./templates/express-auth/package.json ./templates/THREAT_MODEL.md dist/templates"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
53
|
"@types/express": "^5.0.6",
|
|
52
54
|
"@types/fs-extra": "^11.0.4",
|
|
53
55
|
"@types/node": "^25.5.2",
|
|
54
56
|
"@types/supertest": "^7.2.0",
|
|
55
|
-
"@vitest/coverage-v8": "^
|
|
57
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
56
58
|
"supertest": "^7.2.2",
|
|
57
|
-
"vitest": "^
|
|
59
|
+
"vitest": "^4.1.8"
|
|
58
60
|
},
|
|
59
61
|
"repository": {
|
|
60
62
|
"type": "git",
|
|
61
63
|
"url": "https://github.com/COD434/create-authenik8-app.git"
|
|
62
64
|
},
|
|
63
65
|
"overrides": {
|
|
64
|
-
"brace-expansion": "
|
|
66
|
+
"brace-expansion": "2.0.2"
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Authenik8 Generated App Threat Model
|
|
2
|
+
|
|
3
|
+
This document describes what a generated Authenik8 app is designed to help with, what it does not solve for you, and what you must configure before production use.
|
|
4
|
+
|
|
5
|
+
## System Boundary
|
|
6
|
+
|
|
7
|
+
A generated app includes:
|
|
8
|
+
|
|
9
|
+
- Express API routes.
|
|
10
|
+
- JWT access tokens.
|
|
11
|
+
- Redis-backed refresh-token rotation.
|
|
12
|
+
- Prisma database schema.
|
|
13
|
+
- Optional Google/GitHub OAuth.
|
|
14
|
+
- Authenik8 security middleware: Helmet, rate limiting, and optional IP whitelist.
|
|
15
|
+
- Optional PM2 production process config.
|
|
16
|
+
|
|
17
|
+
External systems are outside the generated app boundary:
|
|
18
|
+
|
|
19
|
+
- Browser or mobile frontend.
|
|
20
|
+
- OAuth provider dashboards.
|
|
21
|
+
- Redis hosting.
|
|
22
|
+
- SQL database hosting.
|
|
23
|
+
- TLS termination and reverse proxy.
|
|
24
|
+
- Secret management.
|
|
25
|
+
- Email delivery, MFA, and password reset flows unless you add them.
|
|
26
|
+
|
|
27
|
+
## Assets Protected
|
|
28
|
+
|
|
29
|
+
- Access tokens.
|
|
30
|
+
- Refresh tokens.
|
|
31
|
+
- OAuth authorization state.
|
|
32
|
+
- OAuth identity records.
|
|
33
|
+
- User IDs, emails, roles, and provider links.
|
|
34
|
+
- Redis-backed session state.
|
|
35
|
+
- Admin-only routes.
|
|
36
|
+
|
|
37
|
+
## Trust Assumptions
|
|
38
|
+
|
|
39
|
+
- `JWT_SECRET` and `REFRESH_SECRET` are long, random, private values.
|
|
40
|
+
- Redis is private to the app and not exposed to the public internet.
|
|
41
|
+
- Database credentials are private.
|
|
42
|
+
- OAuth callback URLs match provider dashboard settings exactly.
|
|
43
|
+
- Production traffic uses HTTPS.
|
|
44
|
+
- Reverse proxy headers are trusted only when you control the proxy.
|
|
45
|
+
- Developers validate and authorize their own business-domain routes.
|
|
46
|
+
|
|
47
|
+
## Threats Addressed
|
|
48
|
+
|
|
49
|
+
### Refresh-token replay
|
|
50
|
+
|
|
51
|
+
Refresh tokens are stateful. The currently valid refresh token is stored in Redis under `refresh:<userId>`. When a refresh succeeds, Authenik8-core rotates the refresh token and replaces the Redis value. Reusing an old refresh token fails.
|
|
52
|
+
|
|
53
|
+
### Concurrent refresh abuse
|
|
54
|
+
|
|
55
|
+
Refresh requests acquire a Redis lock under `lock:<userId>`. Two simultaneous refresh attempts for the same user should not both rotate successfully.
|
|
56
|
+
|
|
57
|
+
### Stateless JWT logout limitations
|
|
58
|
+
|
|
59
|
+
Access-token sessions are persisted in Redis under `sessions:<userId>`. Admin helpers can list sessions and revoke one or all sessions for a user.
|
|
60
|
+
|
|
61
|
+
### Basic request flooding
|
|
62
|
+
|
|
63
|
+
`auth.rateLimit` uses Redis-backed rate limiting. Generated apps apply it globally by default.
|
|
64
|
+
|
|
65
|
+
### Common HTTP header risks
|
|
66
|
+
|
|
67
|
+
`auth.helmet` applies secure HTTP headers through Helmet.
|
|
68
|
+
|
|
69
|
+
### OAuth CSRF/state tampering
|
|
70
|
+
|
|
71
|
+
OAuth redirects generate random state and store it in Redis for five minutes under `oauth:state:<state>`. Callback handlers reject missing, invalid, or expired state.
|
|
72
|
+
|
|
73
|
+
### Duplicate OAuth identities
|
|
74
|
+
|
|
75
|
+
OAuth profiles are normalized into provider, provider ID, email, name, and email verification status. The Identity Engine checks provider and email records before creating a new identity.
|
|
76
|
+
|
|
77
|
+
### Unverified OAuth email token issuance
|
|
78
|
+
|
|
79
|
+
`issueTokensFromProfile` rejects OAuth profiles whose email is not verified.
|
|
80
|
+
|
|
81
|
+
### Admin-route access
|
|
82
|
+
|
|
83
|
+
`auth.requireAdmin` checks for a valid JWT with `role: "admin"`.
|
|
84
|
+
|
|
85
|
+
## Threats Not Fully Addressed
|
|
86
|
+
|
|
87
|
+
### XSS in your frontend
|
|
88
|
+
|
|
89
|
+
If frontend JavaScript is compromised, tokens stored in memory or browser storage can be stolen. Use a strong frontend CSP, avoid unsafe HTML rendering, and choose token storage deliberately.
|
|
90
|
+
|
|
91
|
+
### CSRF for cookie-based auth
|
|
92
|
+
|
|
93
|
+
Generated examples use bearer tokens. If you move tokens into cookies, add CSRF protection and strict cookie settings.
|
|
94
|
+
|
|
95
|
+
### Weak or leaked secrets
|
|
96
|
+
|
|
97
|
+
Authenik8 cannot protect tokens if `JWT_SECRET` or `REFRESH_SECRET` is short, reused, committed, logged, or leaked.
|
|
98
|
+
|
|
99
|
+
### Public Redis exposure
|
|
100
|
+
|
|
101
|
+
Redis must not be reachable from the public internet. Use private networking, authentication, TLS where available, and provider-level access controls.
|
|
102
|
+
|
|
103
|
+
### Database authorization bugs
|
|
104
|
+
|
|
105
|
+
Authenik8 authenticates users and provides route middleware. Your application must still enforce object-level authorization, ownership checks, and tenant isolation.
|
|
106
|
+
|
|
107
|
+
### Password reset, email verification, MFA, and WebAuthn
|
|
108
|
+
|
|
109
|
+
These are not included unless you add them.
|
|
110
|
+
|
|
111
|
+
### OAuth provider compromise or misconfiguration
|
|
112
|
+
|
|
113
|
+
Provider dashboard settings, app approval screens, callback URLs, and provider secrets must be managed outside the generated app.
|
|
114
|
+
|
|
115
|
+
### Brute-force protection per credential
|
|
116
|
+
|
|
117
|
+
Global rate limiting is included. Add stricter per-email or per-account login throttling for high-risk apps.
|
|
118
|
+
|
|
119
|
+
### Token theft before expiry
|
|
120
|
+
|
|
121
|
+
Short-lived access tokens reduce exposure, but a stolen access token can be used until it expires or is rejected by your session policy.
|
|
122
|
+
|
|
123
|
+
## Production Checklist
|
|
124
|
+
|
|
125
|
+
- Replace generated development secrets with long random values.
|
|
126
|
+
- Run Redis on private networking.
|
|
127
|
+
- Run Postgres or your database on private networking.
|
|
128
|
+
- Use HTTPS only.
|
|
129
|
+
- Set exact OAuth callback URLs in Google/GitHub dashboards.
|
|
130
|
+
- Use `npx prisma migrate deploy` in production.
|
|
131
|
+
- Review CORS policy before connecting a frontend.
|
|
132
|
+
- Add business-level authorization checks to every protected resource route.
|
|
133
|
+
- Add logging and alerting for refresh failures, OAuth failures, and admin actions.
|
|
134
|
+
- Keep `authenik8-core` and generated dependencies updated.
|
|
135
|
+
|
|
136
|
+
## Security Reporting
|
|
137
|
+
|
|
138
|
+
If you find a vulnerability in the generated app or Authenik8-core integration, do not publish exploit details publicly first. Open a private security report or contact the maintainer through the repository security policy.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Authenik8 Express Password API
|
|
2
|
+
|
|
3
|
+
Generated by `create-authenik8-app`.
|
|
4
|
+
|
|
5
|
+
## Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run docker:up
|
|
10
|
+
npm run prisma:migrate
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
For SQLite, Postgres in `docker-compose.yml` is optional. Redis is required for refresh-token rotation and replay protection.
|
|
15
|
+
|
|
16
|
+
## Environment
|
|
17
|
+
|
|
18
|
+
Review `.env` before running. The generated secrets are development placeholders and must be replaced before deployment.
|
|
19
|
+
|
|
20
|
+
Required:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
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
|
|
26
|
+
REDIS_HOST=127.0.0.1
|
|
27
|
+
REDIS_PORT=6379
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## API Contract
|
|
31
|
+
|
|
32
|
+
Register:
|
|
33
|
+
|
|
34
|
+
```http
|
|
35
|
+
POST /auth/register
|
|
36
|
+
Content-Type: application/json
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
"email": "dev@example.com",
|
|
40
|
+
"password": "password123"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Login:
|
|
45
|
+
|
|
46
|
+
```http
|
|
47
|
+
POST /auth/login
|
|
48
|
+
Content-Type: application/json
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
"email": "dev@example.com",
|
|
52
|
+
"password": "password123"
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Refresh:
|
|
57
|
+
|
|
58
|
+
```http
|
|
59
|
+
POST /auth/refresh
|
|
60
|
+
Content-Type: application/json
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
"refreshToken": "<refreshToken>"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Protected:
|
|
68
|
+
|
|
69
|
+
```http
|
|
70
|
+
GET /protected
|
|
71
|
+
Authorization: Bearer <accessToken>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 3-Minute Verification
|
|
75
|
+
|
|
76
|
+
Start the API in one terminal:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm run docker:up
|
|
80
|
+
npm run prisma:migrate
|
|
81
|
+
npm run dev
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Register a user:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
curl -s -X POST http://localhost:3000/auth/register \
|
|
88
|
+
-H "Content-Type: application/json" \
|
|
89
|
+
-d '{"email":"dev@example.com","password":"password123"}'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Login and save the response:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
curl -s -X POST http://localhost:3000/auth/login \
|
|
96
|
+
-H "Content-Type: application/json" \
|
|
97
|
+
-d '{"email":"dev@example.com","password":"password123"}'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Expected shape:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"user": {
|
|
105
|
+
"id": "user-id",
|
|
106
|
+
"email": "dev@example.com"
|
|
107
|
+
},
|
|
108
|
+
"accessToken": "access-token",
|
|
109
|
+
"refreshToken": "refresh-token"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Call a protected route:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
curl http://localhost:3000/protected \
|
|
117
|
+
-H "Authorization: Bearer <accessToken>"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Refresh an access token:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
curl -s -X POST http://localhost:3000/auth/refresh \
|
|
124
|
+
-H "Content-Type: application/json" \
|
|
125
|
+
-d '{"refreshToken":"<refreshToken>"}'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Environment Variables
|
|
129
|
+
|
|
130
|
+
- `DATABASE_URL`: Prisma database connection. SQLite uses `file:./dev.db`; Postgres uses a `postgresql://...` URL.
|
|
131
|
+
- `JWT_SECRET`: signs short-lived access tokens. Use a long random value in production.
|
|
132
|
+
- `REFRESH_SECRET`: signs refresh tokens. Use a different long random value in production.
|
|
133
|
+
- `REDIS_HOST`: Redis host for refresh-token/session security.
|
|
134
|
+
- `REDIS_PORT`: Redis port, usually `6379` locally.
|
|
135
|
+
|
|
136
|
+
## Frontend Use
|
|
137
|
+
|
|
138
|
+
Store the access token in memory and use the refresh token only through your chosen secure storage strategy. Add the access token to API requests with the `Authorization` header.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
let accessToken = "";
|
|
142
|
+
let refreshToken = "";
|
|
143
|
+
|
|
144
|
+
export async function login(email: string, password: string) {
|
|
145
|
+
const response = await fetch("http://localhost:3000/auth/login", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
body: JSON.stringify({ email, password }),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(`Login failed: ${response.status}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const session = await response.json();
|
|
156
|
+
accessToken = session.accessToken;
|
|
157
|
+
refreshToken = session.refreshToken;
|
|
158
|
+
return session;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function getProtected() {
|
|
162
|
+
const response = await fetch("http://localhost:3000/protected", {
|
|
163
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw new Error(`Protected request failed: ${response.status}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return response.json();
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Troubleshooting
|
|
175
|
+
|
|
176
|
+
`Redis connection refused`: run `npm run docker:up` or start Redis locally with `redis-server --daemonize yes`.
|
|
177
|
+
|
|
178
|
+
`Prisma Client did not initialize`: run `npm run prisma:migrate`, then restart `npm run dev`.
|
|
179
|
+
|
|
180
|
+
`JWT_SECRET must be set to at least 32 characters`: check `.env`; both token secrets must be long strings.
|
|
181
|
+
|
|
182
|
+
`Cannot POST /auth/login`: confirm the server is running and you generated the password auth template, not the JWT-only base template.
|
|
183
|
+
|
|
184
|
+
`Invalid email or password`: register the user first, then login with the same email/password.
|
|
185
|
+
|
|
186
|
+
`Port 3000 already in use`: stop the other process or change the `app.listen(3000)` port in `src/server.ts`.
|
|
187
|
+
|
|
188
|
+
`DATABASE_URL is wrong`: for SQLite use `file:./dev.db`; for local Docker Postgres use `postgresql://postgres:postgres@localhost:5432/authenik8?schema=public`.
|
|
189
|
+
|
|
190
|
+
## Threat Model
|
|
191
|
+
|
|
192
|
+
Read `THREAT_MODEL.md` before deploying. It explains what Authenik8 protects, what Redis-backed token state handles, and what remains your responsibility.
|
|
193
|
+
|
|
194
|
+
## Deploy
|
|
195
|
+
|
|
196
|
+
Use `npm run build`, run `npx prisma migrate deploy` for production databases, set real secrets in your host, and point Redis/Postgres env vars at managed services.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
services:
|
|
2
|
+
redis:
|
|
3
|
+
image: redis:7-alpine
|
|
4
|
+
ports:
|
|
5
|
+
- "6379:6379"
|
|
6
|
+
command: ["redis-server", "--appendonly", "yes"]
|
|
7
|
+
volumes:
|
|
8
|
+
- redis-data:/data
|
|
9
|
+
|
|
10
|
+
postgres:
|
|
11
|
+
image: postgres:16-alpine
|
|
12
|
+
ports:
|
|
13
|
+
- "5432:5432"
|
|
14
|
+
environment:
|
|
15
|
+
POSTGRES_USER: postgres
|
|
16
|
+
POSTGRES_PASSWORD: postgres
|
|
17
|
+
POSTGRES_DB: authenik8
|
|
18
|
+
volumes:
|
|
19
|
+
- postgres-data:/var/lib/postgresql/data
|
|
20
|
+
|
|
21
|
+
volumes:
|
|
22
|
+
redis-data:
|
|
23
|
+
postgres-data:
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"start": "node dist/server.js",
|
|
10
|
-
"prisma:migrate": "prisma migrate dev"
|
|
10
|
+
"prisma:migrate": "prisma migrate dev",
|
|
11
|
+
"docker:up": "docker compose up -d",
|
|
12
|
+
"docker:down": "docker compose down"
|
|
11
13
|
},
|
|
12
14
|
"dependencies": {
|
|
13
|
-
"authenik8-core": "^1.0.
|
|
15
|
+
"authenik8-core": "^1.0.33",
|
|
14
16
|
"dotenv": "^16.0.0",
|
|
15
17
|
"express": "^4.19.2",
|
|
16
18
|
"@prisma/client": "5.22.0"
|