@appweaver/create-weaver-app 1.2.1 → 1.3.1
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/package.json +2 -2
- package/skill/SKILL.md +6 -6
- package/skill/references/client.md +40 -6
- package/skill/references/configuration.md +72 -5
- package/skill/references/security.md +67 -133
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appweaver/create-weaver-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Appweaver - the backend framework for AI-first development (@create-weaver-app)",
|
|
5
5
|
"author": "Luka Matosevic",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
28
|
+
"node": ">= 22"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"create-weaver-app": "node ./dist/create-weaver-app.js"
|
package/skill/SKILL.md
CHANGED
|
@@ -35,7 +35,7 @@ The basic file structure of the Appweaver project:
|
|
|
35
35
|
- `test/e2e/` - the end-to-end tests root directory
|
|
36
36
|
- `test/unit/` - the unit tests root directory
|
|
37
37
|
- `.env` - override the central configuration (optional)
|
|
38
|
-
- `.env.{env}` - override the central configuration for specific
|
|
38
|
+
- `.env.{env}` - override the central configuration for a specific environment (optional)
|
|
39
39
|
- `appweaver.json` - central library configuration file
|
|
40
40
|
- `appweaver.{env}.json` - environment specific configuration files that override the central configuration
|
|
41
41
|
- `Dockerfile` - the dockerfile used for building a docker image for deploying the application
|
|
@@ -414,8 +414,8 @@ await injectService('Post').query({}, 1, 50, {
|
|
|
414
414
|
|
|
415
415
|
A hidden, virtual, or array scalar field, a field of a to-many relation, or a relation the action does not include is
|
|
416
416
|
rejected with a `400` error. Sort inputs are typed by `QuerySort<T>` from `@appweaver/common`, with a `<Model>Sort`
|
|
417
|
-
alias emitted per model
|
|
418
|
-
`-createdAt,id`.
|
|
417
|
+
alias emitted per model and validated over HTTP against a generated `<Model>QuerySort` JSON schema. The default is
|
|
418
|
+
`-createdAt,id`.
|
|
419
419
|
|
|
420
420
|
### Aggregating
|
|
421
421
|
|
|
@@ -436,9 +436,9 @@ additional queries, skipped for the periods holding no record.
|
|
|
436
436
|
|
|
437
437
|
Any other field (string, boolean, enum, JSON, array, hidden, virtual, or a relation), an operator its type does not
|
|
438
438
|
support, an empty selection, or a `dateField` that is not a date field is rejected with a `400` error. Selections are
|
|
439
|
-
typed by `AggregateSelect<T>` from `@appweaver/common`, with a `<Model>Aggregate` alias emitted per model, and
|
|
440
|
-
|
|
441
|
-
|
|
439
|
+
typed by `AggregateSelect<T>` from `@appweaver/common`, with a `<Model>Aggregate` alias emitted per model, and validated
|
|
440
|
+
over HTTP against a generated `<Model>AggregateSelect` JSON schema. The response stays untyped JSON, since its shape
|
|
441
|
+
follows the selection.
|
|
442
442
|
|
|
443
443
|
### Registering a custom route
|
|
444
444
|
|
|
@@ -73,13 +73,18 @@ Reads an OpenAPI v3 schema and generates TypeScript types and a typed client cla
|
|
|
73
73
|
`@maxLength`, `@minimum`, `@maximum`, `@pattern`, `@format`).
|
|
74
74
|
3. Deduplicates union types and extracts inline schemas to named exported types, including the ones carrying a
|
|
75
75
|
description (i.e. `PostQuerySort`).
|
|
76
|
-
4. Hoists the
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
4. Hoists the schemas the document repeats inline into a shared definition each, so the generated types declare them
|
|
77
|
+
once and reference them everywhere else. This covers the enums (every sortable field of every resource shares one
|
|
78
|
+
`SortDirection` rather than declaring an `asc | desc` enum of its own) and the value a filterable field accepts
|
|
79
|
+
(`QueryFilterValue`, built from the plain `QueryFilterScalar`), while every property keeps its own description.
|
|
80
|
+
5. Emits every enum as a constant object plus a type alias of its values, so both the member (`SortDirection.asc`) and
|
|
81
|
+
the plain literal (`'asc'`) are accepted wherever the enum is used. When the types are written to a declaration file
|
|
82
|
+
(`.d.ts`), the constant is declared rather than initialized, since such a file carries no runtime values.
|
|
83
|
+
6. Classifies all API paths into route groups: resources, auth, account, health, files, and custom.
|
|
84
|
+
7. Emits a typed client class extending `FetchClient<Paths>` with a getter for each route group. Resources with
|
|
80
85
|
unsupported operations are excluded at compile time using `Omit`.
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
8. Formats all output with Prettier.
|
|
87
|
+
9. Writes files with an autogenerated header comment.
|
|
83
88
|
|
|
84
89
|
**Examples:**
|
|
85
90
|
|
|
@@ -146,6 +151,35 @@ const sort: PostQuerySort = { createdAt: SortDirection.desc, title: SortDirectio
|
|
|
146
151
|
const posts = await client.post.query({ sort, page: 1, size: 20 });
|
|
147
152
|
```
|
|
148
153
|
|
|
154
|
+
Enums are generated as a constant object together with a type alias of its values, so the members and the raw literals
|
|
155
|
+
are interchangeable and no import is needed for the literal form:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
export const SortDirection = { asc: 'asc', desc: 'desc' } as const;
|
|
159
|
+
export type SortDirection = (typeof SortDirection)[keyof typeof SortDirection];
|
|
160
|
+
|
|
161
|
+
// Both are valid and equally type safe
|
|
162
|
+
const byMember: PostQuerySort = { createdAt: SortDirection.desc };
|
|
163
|
+
const byLiteral: PostQuerySort = { createdAt: 'desc' };
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The value a filterable field accepts is declared once as well, rather than being spelled out again for every field of
|
|
167
|
+
every resource. A scalar field references `QueryFilterValue`, and a relation field adds the filter of the related
|
|
168
|
+
resource to the plain values it accepts:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
export type QueryFilterScalar = string | number | boolean | null;
|
|
172
|
+
export type QueryFilterValue = QueryFilterScalar | QueryFilterScalar[] | QueryCondition;
|
|
173
|
+
|
|
174
|
+
export type PostQueryFilter = {
|
|
175
|
+
/** @description Filter by the title field */
|
|
176
|
+
title?: QueryFilterValue;
|
|
177
|
+
/** @description Filter by the author relation, matching an id, a list of ids, or a nested User filter */
|
|
178
|
+
author?: QueryFilterScalar | QueryFilterScalar[] | UserQueryFilter | UserQueryFilter[];
|
|
179
|
+
// ...
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
149
183
|
### Client file
|
|
150
184
|
|
|
151
185
|
```ts
|
|
@@ -256,11 +256,12 @@ The config object is frozen with `Object.freeze()` after loading to prevent runt
|
|
|
256
256
|
|
|
257
257
|
#### OAuth2 general
|
|
258
258
|
|
|
259
|
-
| Property
|
|
260
|
-
|
|
261
|
-
| `SECURITY_OAUTH2_STATE_TTL`
|
|
262
|
-
| `SECURITY_OAUTH2_REGISTRATION_ENABLED`
|
|
263
|
-
| `SECURITY_OAUTH2_FETCH_AVATAR_ENABLED`
|
|
259
|
+
| Property | Type | Default | Description |
|
|
260
|
+
|----------------------------------------------------------|---------|----------|------------------------------------------------------------------------------------------------------------------------------------|
|
|
261
|
+
| `SECURITY_OAUTH2_STATE_TTL` | integer | `600000` | OAuth2 state parameter TTL in milliseconds (default 10 min). |
|
|
262
|
+
| `SECURITY_OAUTH2_REGISTRATION_ENABLED` | boolean | `true` | Allow registering new users via OAuth2 login. When `false`, only already existing users (matched by email) can log in via OAuth2. |
|
|
263
|
+
| `SECURITY_OAUTH2_FETCH_AVATAR_ENABLED` | boolean | `false` | Download the user's avatar from the OAuth2 provider during registration and pass it as `avatarFile` to `registrationData`. |
|
|
264
|
+
| `SECURITY_OAUTH2_CONNECTED_ACCOUNTS_KEEP_DATABASE_TABLE` | boolean | `false` | Keep the `ConnectedAccount` table even when every OAuth2 provider is disabled, so the links are not dropped by the next migration. |
|
|
264
265
|
|
|
265
266
|
#### OAuth2 Google
|
|
266
267
|
|
|
@@ -280,6 +281,72 @@ The config object is frozen with `Object.freeze()` after loading to prevent runt
|
|
|
280
281
|
| `SECURITY_OAUTH2_FACEBOOK_CLIENT_SECRET` | string? | - | Facebook OAuth2 client secret. |
|
|
281
282
|
| `SECURITY_OAUTH2_FACEBOOK_USER_INFO_URL` | string | `'https://graph.facebook.com/me'` | Facebook user info endpoint. |
|
|
282
283
|
|
|
284
|
+
#### OAuth2 X (Twitter)
|
|
285
|
+
|
|
286
|
+
The app must be granted the email permission in the X developer portal, otherwise X refuses to return the
|
|
287
|
+
`confirmed_email` field and every login is rejected.
|
|
288
|
+
|
|
289
|
+
| Property | Type | Default | Description |
|
|
290
|
+
|-----------------------------------|---------|----------------------------------|------------------------------------------------------------------------|
|
|
291
|
+
| `SECURITY_OAUTH2_X_ENABLED` | boolean | `false` | Enable X OAuth2 provider. The flow always uses PKCE, which X requires. |
|
|
292
|
+
| `SECURITY_OAUTH2_X_CLIENT_ID` | string? | - | X OAuth2 client ID. |
|
|
293
|
+
| `SECURITY_OAUTH2_X_CLIENT_SECRET` | string? | - | X OAuth2 client secret. |
|
|
294
|
+
| `SECURITY_OAUTH2_X_USER_INFO_URL` | string | `'https://api.x.com/2/users/me'` | X user info endpoint. |
|
|
295
|
+
|
|
296
|
+
#### OAuth2 GitHub
|
|
297
|
+
|
|
298
|
+
| Property | Type | Default | Description |
|
|
299
|
+
|----------------------------------------|---------|---------------------------------|--------------------------------|
|
|
300
|
+
| `SECURITY_OAUTH2_GITHUB_ENABLED` | boolean | `false` | Enable GitHub OAuth2 provider. |
|
|
301
|
+
| `SECURITY_OAUTH2_GITHUB_CLIENT_ID` | string? | - | GitHub OAuth2 client ID. |
|
|
302
|
+
| `SECURITY_OAUTH2_GITHUB_CLIENT_SECRET` | string? | - | GitHub OAuth2 client secret. |
|
|
303
|
+
| `SECURITY_OAUTH2_GITHUB_USER_INFO_URL` | string | `'https://api.github.com/user'` | GitHub user info endpoint. |
|
|
304
|
+
|
|
305
|
+
#### OAuth2 GitLab
|
|
306
|
+
|
|
307
|
+
| Property | Type | Default | Description |
|
|
308
|
+
|----------------------------------------|---------|------------------------------------|---------------------------------------------------------------------------------------------|
|
|
309
|
+
| `SECURITY_OAUTH2_GITLAB_ENABLED` | boolean | `false` | Enable GitLab OAuth2 provider. |
|
|
310
|
+
| `SECURITY_OAUTH2_GITLAB_CLIENT_ID` | string? | - | GitLab OAuth2 client ID. |
|
|
311
|
+
| `SECURITY_OAUTH2_GITLAB_CLIENT_SECRET` | string? | - | GitLab OAuth2 client secret. |
|
|
312
|
+
| `SECURITY_OAUTH2_GITLAB_BASE_URL` | string | `'https://gitlab.com'` | Instance base URL for the authorize and token endpoints. Change it for self-managed GitLab. |
|
|
313
|
+
| `SECURITY_OAUTH2_GITLAB_USER_INFO_URL` | string | `'https://gitlab.com/api/v4/user'` | GitLab user info endpoint. |
|
|
314
|
+
|
|
315
|
+
#### OAuth2 LinkedIn
|
|
316
|
+
|
|
317
|
+
| Property | Type | Default | Description |
|
|
318
|
+
|------------------------------------------|---------|------------------------------------------|----------------------------------|
|
|
319
|
+
| `SECURITY_OAUTH2_LINKEDIN_ENABLED` | boolean | `false` | Enable LinkedIn OAuth2 provider. |
|
|
320
|
+
| `SECURITY_OAUTH2_LINKEDIN_CLIENT_ID` | string? | - | LinkedIn OAuth2 client ID. |
|
|
321
|
+
| `SECURITY_OAUTH2_LINKEDIN_CLIENT_SECRET` | string? | - | LinkedIn OAuth2 client secret. |
|
|
322
|
+
| `SECURITY_OAUTH2_LINKEDIN_USER_INFO_URL` | string | `'https://api.linkedin.com/v2/userinfo'` | LinkedIn user info endpoint. |
|
|
323
|
+
|
|
324
|
+
#### OAuth2 Apple
|
|
325
|
+
|
|
326
|
+
Apple expects the client secret to be a short-lived ES256 JWT: either set `SECURITY_OAUTH2_APPLE_CLIENT_SECRET` to one
|
|
327
|
+
you generated yourself, or provide the team ID, key ID and `.p8` private key and let the framework sign one at startup.
|
|
328
|
+
|
|
329
|
+
| Property | Type | Default | Description |
|
|
330
|
+
|--------------------------------------------------|---------|------------|--------------------------------------------------------------------------------------------------|
|
|
331
|
+
| `SECURITY_OAUTH2_APPLE_ENABLED` | boolean | `false` | Enable Apple OAuth2 provider. |
|
|
332
|
+
| `SECURITY_OAUTH2_APPLE_CLIENT_ID` | string? | - | Apple OAuth2 client ID (the Services ID identifier, e.g. `com.example.service`). |
|
|
333
|
+
| `SECURITY_OAUTH2_APPLE_CLIENT_SECRET` | string? | - | Pre-generated client secret JWT. Takes precedence over the signing key settings below. |
|
|
334
|
+
| `SECURITY_OAUTH2_APPLE_TEAM_ID` | string? | - | Apple developer team ID. |
|
|
335
|
+
| `SECURITY_OAUTH2_APPLE_KEY_ID` | string? | - | Identifier of the `.p8` signing key. |
|
|
336
|
+
| `SECURITY_OAUTH2_APPLE_PRIVATE_KEY` | string? | - | Contents of the `.p8` private key (PEM). Escaped `\n` sequences are restored. |
|
|
337
|
+
| `SECURITY_OAUTH2_APPLE_PRIVATE_KEY_PATH` | string? | - | Path to the `.p8` private key file. Used when the inline key is not set. |
|
|
338
|
+
| `SECURITY_OAUTH2_APPLE_CLIENT_SECRET_EXPIRES_IN` | integer | `15552000` | Lifetime in seconds of the generated secret. Apple rejects anything above `15777000` (6 months). |
|
|
339
|
+
|
|
340
|
+
#### OAuth2 Microsoft
|
|
341
|
+
|
|
342
|
+
| Property | Type | Default | Description |
|
|
343
|
+
|-------------------------------------------|---------|-----------------------------------------|----------------------------------------------------------------------------------------------------------|
|
|
344
|
+
| `SECURITY_OAUTH2_MICROSOFT_ENABLED` | boolean | `false` | Enable Microsoft OAuth2 provider. |
|
|
345
|
+
| `SECURITY_OAUTH2_MICROSOFT_CLIENT_ID` | string? | - | Microsoft OAuth2 client ID (the Entra ID application ID). |
|
|
346
|
+
| `SECURITY_OAUTH2_MICROSOFT_CLIENT_SECRET` | string? | - | Microsoft OAuth2 client secret. |
|
|
347
|
+
| `SECURITY_OAUTH2_MICROSOFT_TENANT` | string | `'common'` | Tenant used in the authorize and token endpoints: a tenant ID, `common`, `organizations` or `consumers`. |
|
|
348
|
+
| `SECURITY_OAUTH2_MICROSOFT_USER_INFO_URL` | string | `'https://graph.microsoft.com/v1.0/me'` | Microsoft Graph user info endpoint. |
|
|
349
|
+
|
|
283
350
|
#### OAuth2 Custom (OpenID Connect)
|
|
284
351
|
|
|
285
352
|
| Property | Type | Default | Description |
|
|
@@ -73,7 +73,7 @@ if `SECURITY_JWT_SECRET` is set.
|
|
|
73
73
|
```json
|
|
74
74
|
{
|
|
75
75
|
"scope": "auth | refresh | 2fa",
|
|
76
|
-
"source": "password | oauth2Google | oauth2Facebook |
|
|
76
|
+
"source": "password | apiKey | basic | oauth2Google | oauth2Facebook | oauth2X | oauth2Github | oauth2Gitlab | oauth2Linkedin | oauth2Apple | oauth2Microsoft | oauth2Custom",
|
|
77
77
|
"username": "User email (e.g. admin@example.com)",
|
|
78
78
|
"sub": "User ID (e.g. 123)",
|
|
79
79
|
"iat": "Issued at timestamp (e.g. 1774623924234)"
|
|
@@ -187,8 +187,8 @@ Default delimiter is `AK`, so a key looks like: `42AKa1b2c3d4e5f6...`
|
|
|
187
187
|
|
|
188
188
|
## OAuth2 authentication
|
|
189
189
|
|
|
190
|
-
Appweaver supports OAuth2 login with Google, Facebook,
|
|
191
|
-
the same flow
|
|
190
|
+
Appweaver supports OAuth2 login with Google, Facebook, X, GitHub, GitLab, LinkedIn, Apple, Microsoft, and a custom
|
|
191
|
+
OpenID Connect provider. All of them are built from the same `createOAuth2Plugin` factory and follow the same flow.
|
|
192
192
|
|
|
193
193
|
### OAuth2 flow
|
|
194
194
|
|
|
@@ -217,140 +217,74 @@ the same flow pattern.
|
|
|
217
217
|
9. Server finds the user by email and invokes the optional checkOAuth2User callback
|
|
218
218
|
(aborts with an error when the callback returns a string or an Error).
|
|
219
219
|
New users are registered unless SECURITY_OAUTH2_REGISTRATION_ENABLED=false
|
|
220
|
-
10. Server generates an authentication OTT
|
|
220
|
+
10. Server generates an authentication OTT, flagging it when the password has to be confirmed
|
|
221
221
|
11. Server redirects to the original URL with the token:
|
|
222
|
-
-> https://myapp.com/dashboard?token={ott}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
POST /auth/exchange-token { token: "{ott}" }
|
|
222
|
+
-> https://myapp.com/dashboard?token={ott}[&passwordRequired=true]
|
|
223
|
+
12. Client exchanges the OTT for JWT tokens, adding the password when it was asked for:
|
|
224
|
+
POST /auth/exchange-token { token: "{ott}", password: "..." }
|
|
226
225
|
-> { accessToken, refreshToken }
|
|
226
|
+
13. Server records the provider account in the ConnectedAccount table
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
-
###
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
**
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Or via environment variables:
|
|
295
|
-
|
|
296
|
-
```env
|
|
297
|
-
SECURITY_OAUTH2_FACEBOOK_ENABLED=true
|
|
298
|
-
SECURITY_OAUTH2_FACEBOOK_CLIENT_ID=your-facebook-app-id
|
|
299
|
-
SECURITY_OAUTH2_FACEBOOK_CLIENT_SECRET=your-facebook-app-secret
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
**Routes:**
|
|
303
|
-
|
|
304
|
-
| Method | Path | Description |
|
|
305
|
-
|--------|---------------------------------|----------------------------------------------------------------------------|
|
|
306
|
-
| `GET` | `/auth/login/facebook` | Redirect to Facebook login. Query: `redirectToUrl`. |
|
|
307
|
-
| `GET` | `/auth/login/facebook/callback` | Facebook callback. Exchanges code, creates/finds user, redirects with OTT. |
|
|
308
|
-
|
|
309
|
-
**Scopes**: `public_profile`, `email`
|
|
310
|
-
|
|
311
|
-
**User info extracted**: `email`, `name` (split into firstName/lastName), `picture` (avatarUrl)
|
|
312
|
-
|
|
313
|
-
**Facebook Developer Console setup:**
|
|
314
|
-
|
|
315
|
-
1. Create an app in the Facebook Developer Console
|
|
316
|
-
2. Add Facebook Login product
|
|
317
|
-
3. Set the valid OAuth redirect URI to: `{APP_HOSTNAME}{SERVER_API_PREFIX}/auth/login/facebook/callback`
|
|
318
|
-
|
|
319
|
-
### Custom OAuth2 (OpenID Connect)
|
|
320
|
-
|
|
321
|
-
For any OpenID Connect-compatible provider (Keycloak, Auth0, etc.).
|
|
322
|
-
|
|
323
|
-
**Configuration:**
|
|
324
|
-
|
|
325
|
-
```json
|
|
326
|
-
{
|
|
327
|
-
"config": {
|
|
328
|
-
"security": {
|
|
329
|
-
"oauth2": {
|
|
330
|
-
"custom": {
|
|
331
|
-
"enabled": true,
|
|
332
|
-
"clientId": "your-client-id",
|
|
333
|
-
"clientSecret": "your-client-secret",
|
|
334
|
-
"issuer": "https://keycloak.example.com/realms/myrealm"
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
**Routes:**
|
|
343
|
-
|
|
344
|
-
| Method | Path | Description |
|
|
345
|
-
|--------|-------------------------------|------------------------------------------------------|
|
|
346
|
-
| `GET` | `/auth/login/custom` | Redirect to custom provider. Query: `redirectToUrl`. |
|
|
347
|
-
| `GET` | `/auth/login/custom/callback` | Custom provider callback. |
|
|
348
|
-
|
|
349
|
-
**Scopes**: `openid`, `profile`, `email`
|
|
350
|
-
|
|
351
|
-
**User info endpoint**: `{issuer}/protocol/openid-connect/userinfo`
|
|
352
|
-
|
|
353
|
-
**Standard claims expected**: `sub`, `email`, `given_name`, `family_name`, `picture` (optional, avatarUrl)
|
|
229
|
+
### Providers
|
|
230
|
+
|
|
231
|
+
Every provider is disabled by default and enabled with `SECURITY_OAUTH2_<NAME>_ENABLED` plus `_CLIENT_ID` and
|
|
232
|
+
`_CLIENT_SECRET` (JSON: `security.oauth2.<name>`). Enabling one registers `GET /auth/login/<name>` and
|
|
233
|
+
`/auth/login/<name>/callback`. Register the callback URL `{APP_HOSTNAME}{SERVER_API_PREFIX}/auth/login/<name>/callback`
|
|
234
|
+
with the provider.
|
|
235
|
+
|
|
236
|
+
| `<name>` | Scopes | User info source |
|
|
237
|
+
|-------------|-------------------------------------------|---------------------------------------------|
|
|
238
|
+
| `google` | `profile`, `email` | `googleapis.com/oauth2/v2/userinfo` |
|
|
239
|
+
| `facebook` | `public_profile`, `email` | `graph.facebook.com/me` |
|
|
240
|
+
| `x` | `users.read`, `tweet.read` | `api.x.com/2/users/me` |
|
|
241
|
+
| `github` | `read:user`, `user:email` | `api.github.com/user` |
|
|
242
|
+
| `gitlab` | `read_user` | `gitlab.com/api/v4/user` |
|
|
243
|
+
| `linkedin` | `openid`, `profile`, `email` | `api.linkedin.com/v2/userinfo` |
|
|
244
|
+
| `apple` | `name`, `email` | the `id_token` (no endpoint) |
|
|
245
|
+
| `microsoft` | `openid`, `profile`, `email`, `User.Read` | `graph.microsoft.com/v1.0/me` |
|
|
246
|
+
| `custom` | `openid`, `profile`, `email` | `{issuer}/protocol/openid-connect/userinfo` |
|
|
247
|
+
|
|
248
|
+
### Provider anomalies
|
|
249
|
+
|
|
250
|
+
- **X only releases the email address to approved apps.** The `confirmed_email` field is requested explicitly, but X
|
|
251
|
+
serves it solely to apps granted the email permission in the developer portal — without it the call fails and the
|
|
252
|
+
login is rejected with a 403.
|
|
253
|
+
- **Apple's callback is a `POST`** (`response_mode=form_post`), the identity comes from the `id_token`, and the client
|
|
254
|
+
secret is an ES256 JWT signed at startup from `teamId`/`keyId`/the `.p8` key — expiring after
|
|
255
|
+
`clientSecretExpiresIn` seconds, so a longer-running process needs a restart. Set `clientSecret` directly to use your
|
|
256
|
+
own. The name is sent **only on the first authorization**; later logins yield empty `firstName`/`lastName`.
|
|
257
|
+
- **GitHub** falls back to `/user/emails` for the primary-verified address when the profile email is private and rejects
|
|
258
|
+
accounts with no verified address.
|
|
259
|
+
- **GitLab** self-managed instances need `SECURITY_OAUTH2_GITLAB_BASE_URL` and `_USER_INFO_URL`.
|
|
260
|
+
- **Microsoft** uses `SECURITY_OAUTH2_MICROSOFT_TENANT` (default `common`) in its endpoints, and its Graph photo is an
|
|
261
|
+
authenticated binary, so it arrives as `avatarFile` instead of `avatarUrl`.
|
|
262
|
+
- **Custom** targets any OpenID Connect provider (Keycloak, Auth0, …) and needs `issuer` instead of preset endpoints.
|
|
263
|
+
|
|
264
|
+
### Account takeover protection
|
|
265
|
+
|
|
266
|
+
An OAuth2 sign-in matches an existing user by email, so a provider account carrying someone else's address must not by
|
|
267
|
+
itself unlock a password-protected account. The first time a provider is linked to a user that has a `passwordHash`, the
|
|
268
|
+
redirect carries `&passwordRequired=true` and `POST /auth/exchange-token` rejects the token unless
|
|
269
|
+
`{ token, password }` is sent. The one-time token is spent either way, so a wrong password means restarting the flow.
|
|
270
|
+
|
|
271
|
+
Confirmation happens once: the pairing is stored in `ConnectedAccount` and later sign-ins pass through. Users without a
|
|
272
|
+
password, and users being registered by the current sign-in, are never asked. There is no flag to switch this off — the
|
|
273
|
+
check only ever fires where skipping it would hand the account over. Without a `ConnectedAccount` table there is nowhere
|
|
274
|
+
to record the confirmation, so the password is asked on every sign-in instead.
|
|
275
|
+
|
|
276
|
+
A local `verifiedEmail: false` does not block the sign-in — the provider vouches for the address and the password
|
|
277
|
+
confirms the account, which together prove more than local verification would. The address is marked verified once the
|
|
278
|
+
exchange succeeds.
|
|
279
|
+
|
|
280
|
+
### Connected accounts
|
|
281
|
+
|
|
282
|
+
`ConnectedAccount` pairs a provider account with a local user: `provider`, `providerAccountId`, `scope`, `lastLoginAt`
|
|
283
|
+
(`createdAt` holds the link date) and a relation to the auth model, indexed on `[provider, providerAccountId]`. A
|
|
284
|
+
provider account belongs to one user only — relinking it elsewhere fails with a 403.
|
|
285
|
+
|
|
286
|
+
The table exists when any OAuth2 provider is enabled; `SECURITY_OAUTH2_CONNECTED_ACCOUNTS_KEEP_DATABASE_TABLE=true`
|
|
287
|
+
keeps it after disabling OAuth2, like `SECURITY_API_KEY_KEEP_DATABASE_TABLE` does for API keys.
|
|
354
288
|
|
|
355
289
|
### OAuth2 registration control and hooks
|
|
356
290
|
|