@appweaver/core 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/app/load-providers.js +1 -0
  2. package/factory/create-service.js +5 -5
  3. package/package.json +2 -2
  4. package/prisma/client/browser.d.ts +5 -0
  5. package/prisma/client/client.d.ts +5 -0
  6. package/prisma/client/internal/class.d.ts +11 -0
  7. package/prisma/client/internal/class.js +4 -4
  8. package/prisma/client/internal/prismaNamespace.d.ts +87 -1
  9. package/prisma/client/internal/prismaNamespace.js +11 -1
  10. package/prisma/client/internal/prismaNamespaceBrowser.d.ts +11 -0
  11. package/prisma/client/internal/prismaNamespaceBrowser.js +11 -1
  12. package/prisma/client/models/ConnectedAccount.d.ts +1089 -0
  13. package/prisma/client/models/ConnectedAccount.js +2 -0
  14. package/prisma/client/models.d.ts +1 -0
  15. package/resources.d.ts +2 -0
  16. package/resources.js +15 -11
  17. package/security/auth-routes.js +2 -2
  18. package/security/auth-schema.d.ts +2 -0
  19. package/security/auth-schema.js +5 -1
  20. package/security/auth-service.d.ts +7 -19
  21. package/security/auth-service.js +37 -32
  22. package/security/auth.js +15 -2
  23. package/security/create-auth-resources.js +15 -0
  24. package/security/helper.d.ts +8 -0
  25. package/security/helper.js +13 -0
  26. package/security/index.d.ts +2 -0
  27. package/security/index.js +2 -0
  28. package/security/oauth2/create-oauth2-plugin.d.ts +19 -4
  29. package/security/oauth2/create-oauth2-plugin.js +66 -23
  30. package/security/oauth2/index.d.ts +6 -0
  31. package/security/oauth2/index.js +6 -0
  32. package/security/oauth2/oauth2-apple.d.ts +10 -0
  33. package/security/oauth2/oauth2-apple.js +56 -0
  34. package/security/oauth2/oauth2-custom.d.ts +6 -4
  35. package/security/oauth2/oauth2-custom.js +6 -8
  36. package/security/oauth2/oauth2-facebook.d.ts +3 -1
  37. package/security/oauth2/oauth2-facebook.js +5 -11
  38. package/security/oauth2/oauth2-github.d.ts +3 -0
  39. package/security/oauth2/oauth2-github.js +47 -0
  40. package/security/oauth2/oauth2-gitlab.d.ts +3 -0
  41. package/security/oauth2/oauth2-gitlab.js +33 -0
  42. package/security/oauth2/oauth2-google.d.ts +3 -1
  43. package/security/oauth2/oauth2-google.js +6 -11
  44. package/security/oauth2/oauth2-linkedin.d.ts +3 -0
  45. package/security/oauth2/oauth2-linkedin.js +28 -0
  46. package/security/oauth2/oauth2-microsoft.d.ts +3 -0
  47. package/security/oauth2/oauth2-microsoft.js +36 -0
  48. package/security/oauth2/oauth2-schema.d.ts +9 -2
  49. package/security/oauth2/oauth2-schema.js +20 -6
  50. package/security/oauth2/oauth2-service.d.ts +58 -0
  51. package/security/oauth2/oauth2-service.js +142 -0
  52. package/security/oauth2/oauth2-util.d.ts +60 -0
  53. package/security/oauth2/oauth2-util.js +206 -0
  54. package/security/oauth2/oauth2-x.d.ts +3 -0
  55. package/security/oauth2/oauth2-x.js +33 -0
  56. package/security/resources/connected-account/model.d.ts +2 -0
  57. package/security/resources/connected-account/model.js +53 -0
  58. package/security/resources/connected-account/service.d.ts +2 -0
  59. package/security/resources/connected-account/service.js +7 -0
  60. package/types/auth.d.ts +23 -5
  61. package/types/generated.d.ts +62 -0
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.oauth2Apple = void 0;
4
+ exports.extractAppleUser = extractAppleUser;
5
+ const common_1 = require("@appweaver/common");
6
+ const create_oauth2_plugin_1 = require("./create-oauth2-plugin");
7
+ const oauth2_util_1 = require("./oauth2-util");
8
+ exports.oauth2Apple = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1.AuthSource.OAuth2Apple, {
9
+ enabled: common_1.config.SECURITY_OAUTH2_APPLE_ENABLED,
10
+ clientId: common_1.config.SECURITY_OAUTH2_APPLE_CLIENT_ID,
11
+ // Resolved lazily so the signing key is only read when the provider is actually enabled.
12
+ clientSecret: oauth2_util_1.createAppleClientSecret,
13
+ scope: ['name', 'email'],
14
+ // Requesting the name or email scope makes Apple post the authorization response as a form body.
15
+ formPostCallback: true,
16
+ // Apple's token endpoint rejects HTTP Basic credentials.
17
+ authorizationMethod: 'body',
18
+ extractUserInfo: (_accessToken, context) => extractAppleUser(context)
19
+ });
20
+ /**
21
+ * Builds the user info from Apple's token response. Apple has no user info endpoint: the identity is carried by the
22
+ * `id_token`, and the display name is posted alongside the authorization code on the very first authorization only.
23
+ *
24
+ * @param {OAuth2UserInfoContext} context - The token set and callback request from the authorization code flow.
25
+ * @return {Promise<UserInfo>} A promise resolving to the extracted user info.
26
+ */
27
+ async function extractAppleUser({ token, request }) {
28
+ const claims = (0, oauth2_util_1.decodeJwtPayload)('Apple', token.id_token);
29
+ const name = parseAppleFormUser(request.body)?.name;
30
+ return {
31
+ id: claims.sub,
32
+ email: (0, oauth2_util_1.requireEmail)('Apple', claims.email),
33
+ firstName: name?.firstName ?? '',
34
+ lastName: name?.lastName ?? ''
35
+ };
36
+ }
37
+ /**
38
+ * Parses the JSON encoded `user` field Apple posts to the callback on the first authorization. Malformed values are
39
+ * logged and ignored, since the rest of the identity comes from the identity token.
40
+ *
41
+ * @param {unknown} body - The parsed form body of the callback request.
42
+ * @return {AppleFormUser | undefined} The decoded profile, or `undefined` when it is absent or unreadable.
43
+ */
44
+ function parseAppleFormUser(body) {
45
+ const raw = body?.user;
46
+ if (!raw) {
47
+ return undefined;
48
+ }
49
+ try {
50
+ return JSON.parse(raw);
51
+ }
52
+ catch (e) {
53
+ common_1.logger.debug({ err: e }, 'Apple OAuth2 user payload is malformed');
54
+ return undefined;
55
+ }
56
+ }
@@ -1,8 +1,10 @@
1
+ import { UserInfo } from '../../types';
1
2
  export type OAuth2UserInfo = {
2
3
  sub: string;
3
- email: string;
4
- given_name: string;
5
- family_name: string;
4
+ email?: string;
5
+ given_name?: string;
6
+ family_name?: string;
6
7
  picture?: string;
7
8
  };
8
- export declare const oauth2Custom: (server: import("../..").Server) => Promise<void>;
9
+ export declare const oauth2Custom: (server: import("../../types").Server) => Promise<void>;
10
+ export declare function fetchCustomUser(accessToken: string): Promise<UserInfo>;
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.oauth2Custom = void 0;
4
+ exports.fetchCustomUser = fetchCustomUser;
4
5
  const common_1 = require("@appweaver/common");
5
6
  const context_1 = require("../../context");
6
7
  const errors_1 = require("../../errors");
7
8
  const create_oauth2_plugin_1 = require("./create-oauth2-plugin");
9
+ const oauth2_util_1 = require("./oauth2-util");
8
10
  exports.oauth2Custom = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1.AuthSource.OAuth2Custom, {
9
11
  enabled: common_1.config.SECURITY_OAUTH2_CUSTOM_ENABLED,
10
12
  clientId: common_1.config.SECURITY_OAUTH2_CUSTOM_CLIENT_ID,
@@ -25,17 +27,13 @@ async function fetchCustomUser(accessToken) {
25
27
  }
26
28
  else {
27
29
  // Fallback to direct API call if the plugin is not initialized
28
- const resp = await fetch(`${common_1.config.SECURITY_OAUTH2_CUSTOM_ISSUER}/protocol/openid-connect/userinfo`, { method: 'GET', headers: { authorization: `Bearer ${accessToken}` } });
29
- if (!resp.ok) {
30
- throw new errors_1.HttpError(`Custom OAuth2 API error: ${resp.status} ${resp.statusText}`, 500);
31
- }
32
- data = await resp.json();
30
+ data = await (0, oauth2_util_1.fetchUserInfo)('Custom OAuth2', `${common_1.config.SECURITY_OAUTH2_CUSTOM_ISSUER}/protocol/openid-connect/userinfo`, accessToken);
33
31
  }
34
32
  return {
35
33
  id: data.sub,
36
- email: data.email,
37
- firstName: data.given_name,
38
- lastName: data.family_name,
34
+ email: (0, oauth2_util_1.requireEmail)('Custom OAuth2', data.email),
35
+ firstName: data.given_name ?? '',
36
+ lastName: data.family_name ?? '',
39
37
  avatarUrl: data.picture
40
38
  };
41
39
  }
@@ -1 +1,3 @@
1
- export declare const oauth2Facebook: (server: import("../..").Server) => Promise<void>;
1
+ import { UserInfo } from '../../types';
2
+ export declare const oauth2Facebook: (server: import("../../types").Server) => Promise<void>;
3
+ export declare function fetchFacebookUser(accessToken: string): Promise<UserInfo>;
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.oauth2Facebook = void 0;
4
+ exports.fetchFacebookUser = fetchFacebookUser;
4
5
  const common_1 = require("@appweaver/common");
5
- const errors_1 = require("../../errors");
6
6
  const create_oauth2_plugin_1 = require("./create-oauth2-plugin");
7
+ const oauth2_util_1 = require("./oauth2-util");
7
8
  exports.oauth2Facebook = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1.AuthSource.OAuth2Facebook, {
8
9
  enabled: common_1.config.SECURITY_OAUTH2_FACEBOOK_ENABLED,
9
10
  clientId: common_1.config.SECURITY_OAUTH2_FACEBOOK_CLIENT_ID,
@@ -14,18 +15,11 @@ exports.oauth2Facebook = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1
14
15
  async function fetchFacebookUser(accessToken) {
15
16
  const params = new URLSearchParams();
16
17
  params.append('fields', 'id,name,email,picture.width(512)');
17
- params.append('access_token', accessToken);
18
- const resp = await fetch(`${common_1.config.SECURITY_OAUTH2_FACEBOOK_USER_INFO_URL}?${params}`, { method: 'GET' });
19
- if (!resp.ok) {
20
- throw new errors_1.HttpError(`Facebook Graph API error: ${resp.status} ${resp.statusText}`, 500);
21
- }
22
- const data = await resp.json();
23
- const [firstName, lastName] = data.name.split(' ');
18
+ const data = await (0, oauth2_util_1.fetchUserInfo)('Facebook Graph', `${common_1.config.SECURITY_OAUTH2_FACEBOOK_USER_INFO_URL}?${params}`, accessToken);
24
19
  return {
25
20
  id: data.id,
26
- email: data.email,
27
- firstName,
28
- lastName,
21
+ email: (0, oauth2_util_1.requireEmail)('Facebook', data.email),
22
+ ...(0, oauth2_util_1.splitFullName)(data.name),
29
23
  avatarUrl: data.picture?.data?.url
30
24
  };
31
25
  }
@@ -0,0 +1,3 @@
1
+ import { UserInfo } from '../../types';
2
+ export declare const oauth2Github: (server: import("../../types").Server) => Promise<void>;
3
+ export declare function fetchGithubUser(accessToken: string): Promise<UserInfo>;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.oauth2Github = void 0;
4
+ exports.fetchGithubUser = fetchGithubUser;
5
+ const common_1 = require("@appweaver/common");
6
+ const errors_1 = require("../../errors");
7
+ const create_oauth2_plugin_1 = require("./create-oauth2-plugin");
8
+ const oauth2_util_1 = require("./oauth2-util");
9
+ const GITHUB_HEADERS = {
10
+ accept: 'application/vnd.github+json',
11
+ 'x-github-api-version': '2022-11-28',
12
+ 'user-agent': 'appweaver'
13
+ };
14
+ exports.oauth2Github = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1.AuthSource.OAuth2Github, {
15
+ enabled: common_1.config.SECURITY_OAUTH2_GITHUB_ENABLED,
16
+ clientId: common_1.config.SECURITY_OAUTH2_GITHUB_CLIENT_ID,
17
+ clientSecret: common_1.config.SECURITY_OAUTH2_GITHUB_CLIENT_SECRET,
18
+ displayName: 'GitHub',
19
+ scope: ['read:user', 'user:email'],
20
+ extractUserInfo: (accessToken) => fetchGithubUser(accessToken)
21
+ });
22
+ async function fetchGithubUser(accessToken) {
23
+ const data = await (0, oauth2_util_1.fetchUserInfo)('GitHub', common_1.config.SECURITY_OAUTH2_GITHUB_USER_INFO_URL, accessToken, GITHUB_HEADERS);
24
+ return {
25
+ id: String(data.id),
26
+ email: data.email ?? (await fetchGithubEmail(accessToken)),
27
+ ...(0, oauth2_util_1.splitFullName)(data.name ?? data.login),
28
+ avatarUrl: data.avatar_url
29
+ };
30
+ }
31
+ /**
32
+ * Resolves the user's email address from the dedicated emails endpoint, needed because GitHub omits it from the user
33
+ * profile whenever the address is kept private.
34
+ *
35
+ * @param {string} accessToken - The access token obtained from the authorization code flow.
36
+ * @return {Promise<string>} A promise resolving to the primary verified address, or the first verified one.
37
+ * @throws {HttpError} If the account has no verified email address.
38
+ */
39
+ async function fetchGithubEmail(accessToken) {
40
+ const emails = await (0, oauth2_util_1.fetchUserInfo)('GitHub', `${common_1.config.SECURITY_OAUTH2_GITHUB_USER_INFO_URL}/emails`, accessToken, GITHUB_HEADERS);
41
+ const verified = emails.filter((email) => email.verified);
42
+ const email = verified.find((email) => email.primary) ?? verified[0];
43
+ if (!email) {
44
+ throw new errors_1.HttpError('GitHub account has no verified email address', 403);
45
+ }
46
+ return email.email;
47
+ }
@@ -0,0 +1,3 @@
1
+ import { UserInfo } from '../../types';
2
+ export declare const oauth2Gitlab: (server: import("../../types").Server) => Promise<void>;
3
+ export declare function fetchGitlabUser(accessToken: string): Promise<UserInfo>;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.oauth2Gitlab = void 0;
4
+ exports.fetchGitlabUser = fetchGitlabUser;
5
+ const common_1 = require("@appweaver/common");
6
+ const create_oauth2_plugin_1 = require("./create-oauth2-plugin");
7
+ const oauth2_util_1 = require("./oauth2-util");
8
+ const baseUrl = common_1.config.SECURITY_OAUTH2_GITLAB_BASE_URL.replace(/\/$/, '');
9
+ exports.oauth2Gitlab = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1.AuthSource.OAuth2Gitlab, {
10
+ enabled: common_1.config.SECURITY_OAUTH2_GITLAB_ENABLED,
11
+ clientId: common_1.config.SECURITY_OAUTH2_GITLAB_CLIENT_ID,
12
+ clientSecret: common_1.config.SECURITY_OAUTH2_GITLAB_CLIENT_SECRET,
13
+ displayName: 'GitLab',
14
+ scope: ['read_user'],
15
+ // Built from the base URL rather than the bundled preset, so self-managed instances work too.
16
+ auth: {
17
+ authorizeHost: baseUrl,
18
+ authorizePath: '/oauth/authorize',
19
+ tokenHost: baseUrl,
20
+ tokenPath: '/oauth/token',
21
+ revokePath: '/oauth/revoke'
22
+ },
23
+ extractUserInfo: (accessToken) => fetchGitlabUser(accessToken)
24
+ });
25
+ async function fetchGitlabUser(accessToken) {
26
+ const data = await (0, oauth2_util_1.fetchUserInfo)('GitLab', common_1.config.SECURITY_OAUTH2_GITLAB_USER_INFO_URL, accessToken);
27
+ return {
28
+ id: String(data.id),
29
+ email: (0, oauth2_util_1.requireEmail)('GitLab', data.email),
30
+ ...(0, oauth2_util_1.splitFullName)(data.name ?? data.username),
31
+ avatarUrl: data.avatar_url
32
+ };
33
+ }
@@ -1 +1,3 @@
1
- export declare const oauth2Google: (server: import("../..").Server) => Promise<void>;
1
+ import { UserInfo } from '../../types';
2
+ export declare const oauth2Google: (server: import("../../types").Server) => Promise<void>;
3
+ export declare function fetchGoogleUser(accessToken: string): Promise<UserInfo>;
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.oauth2Google = void 0;
4
+ exports.fetchGoogleUser = fetchGoogleUser;
4
5
  const common_1 = require("@appweaver/common");
5
- const errors_1 = require("../../errors");
6
6
  const create_oauth2_plugin_1 = require("./create-oauth2-plugin");
7
+ const oauth2_util_1 = require("./oauth2-util");
7
8
  exports.oauth2Google = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1.AuthSource.OAuth2Google, {
8
9
  enabled: common_1.config.SECURITY_OAUTH2_GOOGLE_ENABLED,
9
10
  clientId: common_1.config.SECURITY_OAUTH2_GOOGLE_CLIENT_ID,
@@ -12,18 +13,12 @@ exports.oauth2Google = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1.A
12
13
  extractUserInfo: (accessToken) => fetchGoogleUser(accessToken)
13
14
  });
14
15
  async function fetchGoogleUser(accessToken) {
15
- const params = new URLSearchParams();
16
- params.append('access_token', accessToken);
17
- const resp = await fetch(`${common_1.config.SECURITY_OAUTH2_GOOGLE_USER_INFO_URL}?${params}`, { method: 'GET' });
18
- if (!resp.ok) {
19
- throw new errors_1.HttpError(`Google API error: ${resp.status} ${resp.statusText}`, 500);
20
- }
21
- const data = await resp.json();
16
+ const data = await (0, oauth2_util_1.fetchUserInfo)('Google', common_1.config.SECURITY_OAUTH2_GOOGLE_USER_INFO_URL, accessToken);
22
17
  return {
23
18
  id: data.id,
24
- email: data.email,
25
- firstName: data.given_name,
26
- lastName: data.family_name,
19
+ email: (0, oauth2_util_1.requireEmail)('Google', data.email),
20
+ firstName: data.given_name ?? '',
21
+ lastName: data.family_name ?? '',
27
22
  avatarUrl: data.picture
28
23
  };
29
24
  }
@@ -0,0 +1,3 @@
1
+ import { UserInfo } from '../../types';
2
+ export declare const oauth2Linkedin: (server: import("../../types").Server) => Promise<void>;
3
+ export declare function fetchLinkedinUser(accessToken: string): Promise<UserInfo>;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.oauth2Linkedin = void 0;
4
+ exports.fetchLinkedinUser = fetchLinkedinUser;
5
+ const common_1 = require("@appweaver/common");
6
+ const create_oauth2_plugin_1 = require("./create-oauth2-plugin");
7
+ const oauth2_util_1 = require("./oauth2-util");
8
+ exports.oauth2Linkedin = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1.AuthSource.OAuth2Linkedin, {
9
+ enabled: common_1.config.SECURITY_OAUTH2_LINKEDIN_ENABLED,
10
+ clientId: common_1.config.SECURITY_OAUTH2_LINKEDIN_CLIENT_ID,
11
+ clientSecret: common_1.config.SECURITY_OAUTH2_LINKEDIN_CLIENT_SECRET,
12
+ displayName: 'LinkedIn',
13
+ scope: ['openid', 'profile', 'email'],
14
+ // LinkedIn's token endpoint rejects HTTP Basic credentials.
15
+ authorizationMethod: 'body',
16
+ extractUserInfo: (accessToken) => fetchLinkedinUser(accessToken)
17
+ });
18
+ async function fetchLinkedinUser(accessToken) {
19
+ const data = await (0, oauth2_util_1.fetchUserInfo)('LinkedIn', common_1.config.SECURITY_OAUTH2_LINKEDIN_USER_INFO_URL, accessToken);
20
+ const { firstName, lastName } = (0, oauth2_util_1.splitFullName)(data.name);
21
+ return {
22
+ id: data.sub,
23
+ email: (0, oauth2_util_1.requireEmail)('LinkedIn', data.email),
24
+ firstName: data.given_name ?? firstName,
25
+ lastName: data.family_name ?? lastName,
26
+ avatarUrl: data.picture
27
+ };
28
+ }
@@ -0,0 +1,3 @@
1
+ import { UserInfo } from '../../types';
2
+ export declare const oauth2Microsoft: (server: import("../../types").Server) => Promise<void>;
3
+ export declare function fetchMicrosoftUser(accessToken: string): Promise<UserInfo>;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.oauth2Microsoft = void 0;
4
+ exports.fetchMicrosoftUser = fetchMicrosoftUser;
5
+ const common_1 = require("@appweaver/common");
6
+ const create_oauth2_plugin_1 = require("./create-oauth2-plugin");
7
+ const oauth2_util_1 = require("./oauth2-util");
8
+ const tenant = common_1.config.SECURITY_OAUTH2_MICROSOFT_TENANT;
9
+ exports.oauth2Microsoft = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1.AuthSource.OAuth2Microsoft, {
10
+ enabled: common_1.config.SECURITY_OAUTH2_MICROSOFT_ENABLED,
11
+ clientId: common_1.config.SECURITY_OAUTH2_MICROSOFT_CLIENT_ID,
12
+ clientSecret: common_1.config.SECURITY_OAUTH2_MICROSOFT_CLIENT_SECRET,
13
+ scope: ['openid', 'profile', 'email', 'User.Read'],
14
+ // Built from the configured tenant rather than the bundled preset, which is hardcoded to `common`.
15
+ auth: {
16
+ authorizeHost: 'https://login.microsoftonline.com',
17
+ authorizePath: `/${tenant}/oauth2/v2.0/authorize`,
18
+ tokenHost: 'https://login.microsoftonline.com',
19
+ tokenPath: `/${tenant}/oauth2/v2.0/token`
20
+ },
21
+ extractUserInfo: (accessToken) => fetchMicrosoftUser(accessToken)
22
+ });
23
+ async function fetchMicrosoftUser(accessToken) {
24
+ const userInfoUrl = common_1.config.SECURITY_OAUTH2_MICROSOFT_USER_INFO_URL;
25
+ const data = await (0, oauth2_util_1.fetchUserInfo)('Microsoft', userInfoUrl, accessToken);
26
+ const { firstName, lastName } = (0, oauth2_util_1.splitFullName)(data.displayName);
27
+ return {
28
+ id: data.id,
29
+ // Work accounts expose `mail`, personal ones only the principal name.
30
+ email: (0, oauth2_util_1.requireEmail)('Microsoft', data.mail ?? data.userPrincipalName),
31
+ firstName: data.givenName ?? firstName,
32
+ lastName: data.surname ?? lastName,
33
+ // Microsoft Graph serves the photo as authenticated binary content, so it has to be downloaded here.
34
+ avatarFile: await (0, oauth2_util_1.fetchAuthenticatedAvatar)(`${userInfoUrl}/photo/$value`, accessToken, data.id)
35
+ };
36
+ }
@@ -2,9 +2,16 @@ import { RouteSchema } from '@appweaver/common';
2
2
  export declare const OAuth2RedirectQuery: import("@sinclair/typebox").TObject<{
3
3
  redirectToUrl: import("@sinclair/typebox").TString;
4
4
  }>;
5
- export declare const OAuth2CallbackQuery: import("@sinclair/typebox").TObject<{
5
+ export declare const OAuth2CallbackRequest: import("@sinclair/typebox").TObject<{
6
6
  code: import("@sinclair/typebox").TString;
7
7
  state: import("@sinclair/typebox").TString;
8
+ id_token: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
9
+ user: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
8
10
  }>;
9
11
  export declare function createOAuth2RedirectSchema(providerName: string): RouteSchema;
10
- export declare function createOAuth2CallbackSchema(providerName: string): RouteSchema;
12
+ /**
13
+ * @param {string} providerName - Human-readable provider name used in the summary and description.
14
+ * @param {boolean} [formPost] - Whether the provider returns the authorization response as a form body
15
+ * (`response_mode=form_post`) instead of query parameters.
16
+ */
17
+ export declare function createOAuth2CallbackSchema(providerName: string, formPost?: boolean): RouteSchema;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OAuth2CallbackQuery = exports.OAuth2RedirectQuery = void 0;
3
+ exports.OAuth2CallbackRequest = exports.OAuth2RedirectQuery = void 0;
4
4
  exports.createOAuth2RedirectSchema = createOAuth2RedirectSchema;
5
5
  exports.createOAuth2CallbackSchema = createOAuth2CallbackSchema;
6
6
  const typebox_1 = require("@sinclair/typebox");
@@ -14,7 +14,7 @@ exports.OAuth2RedirectQuery = typebox_1.Type.Object({
14
14
  example: 'https://example.com/login/handler'
15
15
  })
16
16
  });
17
- exports.OAuth2CallbackQuery = typebox_1.Type.Object({
17
+ exports.OAuth2CallbackRequest = typebox_1.Type.Object({
18
18
  code: typebox_1.Type.String({
19
19
  description: 'Authorization code from OAuth2 provider',
20
20
  example: '3b7bdc9982feac0e20cf4ddc9be52a1a027142e25b4d14c1b5a280595bc20'
@@ -22,8 +22,15 @@ exports.OAuth2CallbackQuery = typebox_1.Type.Object({
22
22
  state: typebox_1.Type.String({
23
23
  description: 'Authorization state returned from OAuth2 provider',
24
24
  example: '89bbb34d76801fcf8251193a02a1d62c7c87a'
25
- })
26
- });
25
+ }),
26
+ id_token: typebox_1.Type.Optional(typebox_1.Type.String({
27
+ description: 'OpenID Connect identity token carrying the user claims'
28
+ })),
29
+ user: typebox_1.Type.Optional(typebox_1.Type.String({
30
+ description: 'JSON encoded user profile, sent by Apple only on the first authorization',
31
+ example: '{"name":{"firstName":"Ada","lastName":"Lovelace"}}'
32
+ }))
33
+ }, { additionalProperties: true });
27
34
  function createOAuth2RedirectSchema(providerName) {
28
35
  return {
29
36
  tags: ['Auth'],
@@ -37,12 +44,19 @@ function createOAuth2RedirectSchema(providerName) {
37
44
  }
38
45
  };
39
46
  }
40
- function createOAuth2CallbackSchema(providerName) {
47
+ /**
48
+ * @param {string} providerName - Human-readable provider name used in the summary and description.
49
+ * @param {boolean} [formPost] - Whether the provider returns the authorization response as a form body
50
+ * (`response_mode=form_post`) instead of query parameters.
51
+ */
52
+ function createOAuth2CallbackSchema(providerName, formPost = false) {
41
53
  return {
42
54
  tags: ['Auth'],
43
55
  summary: `Authenticate identity from ${providerName} callback`,
44
56
  description: `Authenticate identity from ${providerName} callback`,
45
- querystring: exports.OAuth2CallbackQuery,
57
+ ...(formPost
58
+ ? { body: exports.OAuth2CallbackRequest }
59
+ : { querystring: exports.OAuth2CallbackRequest }),
46
60
  response: {
47
61
  302: {
48
62
  description: `Redirect to 'redirectToUrl' provided when initiating OAuth2 authentication`
@@ -0,0 +1,58 @@
1
+ import { AuthSource, AuthUser } from '@appweaver/common';
2
+ import { ConnectedAccount, UserInfo } from '../../types';
3
+ /**
4
+ * Handles the parts of authentication that only apply to OAuth2 sign-ins: the application's own admission check, the
5
+ * links between provider accounts and local users, and the rule deciding when a sign-in has to be confirmed with the
6
+ * account password.
7
+ */
8
+ export declare class OAuth2Service {
9
+ /**
10
+ * Checks whether a user is allowed to be registered and/or authenticated via OAuth2 by invoking the optional
11
+ * `checkOAuth2User` callback configured on the auth service. When the callback returns nothing, the OAuth2 flow
12
+ * proceeds normally (registration of a new user or login of an existing one). When it returns a string or an error,
13
+ * the flow is aborted by throwing an `HttpError`.
14
+ *
15
+ * @param {AuthSource} source - The OAuth2 authentication source, e.g., oauth2Google, oauth2Facebook, oauth2Custom.
16
+ * @param {UserInfo} userInfo - The user info extracted from the OAuth2 provider.
17
+ * @param {AuthUser | null} authUser - The existing authenticated user matched by email, or null when the user does
18
+ * not exist yet (i.e., a new user would be registered).
19
+ * @return {Promise<void>} A promise that resolves when the user is allowed to proceed.
20
+ * @throws {HttpError} If the configured callback returns a string or an error (status 403 unless an `HttpError` is
21
+ * returned, in which case it is thrown as-is).
22
+ */
23
+ checkUser(source: AuthSource, userInfo: UserInfo, authUser: AuthUser | null): Promise<void>;
24
+ /**
25
+ * Decides whether an OAuth2 sign-in has to be confirmed with the account password before it is honored. This is the
26
+ * case the first time a provider account is linked to an existing user that already has a password, since anyone
27
+ * able to create a provider account carrying that email address could otherwise take the account over. The rule is
28
+ * not configurable: turning it off would hand over every password-protected account to whoever can present a
29
+ * matching address.
30
+ *
31
+ * @param {AuthUser} authUser - The local user matched by email address.
32
+ * @param {AuthSource} source - The OAuth2 authentication source.
33
+ * @param {string} providerAccountId - The user identifier reported by the provider.
34
+ * @return {Promise<boolean>} A promise resolving to true when the password has to be confirmed.
35
+ */
36
+ requiresPasswordConfirmation(authUser: AuthUser, source: AuthSource, providerAccountId: string): Promise<boolean>;
37
+ /**
38
+ * Finds the connected account linking an OAuth2 provider account to a local user.
39
+ *
40
+ * @param {AuthSource} source - The OAuth2 authentication source.
41
+ * @param {string} providerAccountId - The user identifier reported by the provider.
42
+ * @return {Promise<ConnectedAccount | null>} A promise resolving to the link, or null when the provider account is
43
+ * unknown or connected accounts are not being tracked.
44
+ */
45
+ findConnectedAccount(source: AuthSource, providerAccountId: string): Promise<ConnectedAccount | null>;
46
+ /**
47
+ * Records a successful OAuth2 sign-in, creating the link on the first one and refreshing it afterward. Does nothing
48
+ * when connected accounts are not being tracked.
49
+ *
50
+ * @param {AuthUser} authUser - The local user the provider account belongs to.
51
+ * @param {AuthSource} source - The OAuth2 authentication source.
52
+ * @param {string} providerAccountId - The user identifier reported by the provider.
53
+ * @param {string} [scope] - The scopes granted by the provider.
54
+ * @return {Promise<void>} A promise that resolves once the link is stored.
55
+ * @throws {HttpError} If the provider account is already linked to a different user.
56
+ */
57
+ linkConnectedAccount(authUser: AuthUser, source: AuthSource, providerAccountId: string, scope?: string): Promise<void>;
58
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuth2Service = void 0;
4
+ const common_1 = require("@appweaver/common");
5
+ const context_1 = require("../../context");
6
+ const errors_1 = require("../../errors");
7
+ const helper_1 = require("../helper");
8
+ /**
9
+ * Handles the parts of authentication that only apply to OAuth2 sign-ins: the application's own admission check, the
10
+ * links between provider accounts and local users, and the rule deciding when a sign-in has to be confirmed with the
11
+ * account password.
12
+ */
13
+ class OAuth2Service {
14
+ /** @internal */
15
+ _authUserService = (0, helper_1.resourceAuthService)();
16
+ /** Optional, since an application with no OAuth2 provider never registers it.
17
+ * @internal */
18
+ _connectedAccountService = (0, context_1.injectService)('ConnectedAccount', false);
19
+ /**
20
+ * Checks whether a user is allowed to be registered and/or authenticated via OAuth2 by invoking the optional
21
+ * `checkOAuth2User` callback configured on the auth service. When the callback returns nothing, the OAuth2 flow
22
+ * proceeds normally (registration of a new user or login of an existing one). When it returns a string or an error,
23
+ * the flow is aborted by throwing an `HttpError`.
24
+ *
25
+ * @param {AuthSource} source - The OAuth2 authentication source, e.g., oauth2Google, oauth2Facebook, oauth2Custom.
26
+ * @param {UserInfo} userInfo - The user info extracted from the OAuth2 provider.
27
+ * @param {AuthUser | null} authUser - The existing authenticated user matched by email, or null when the user does
28
+ * not exist yet (i.e., a new user would be registered).
29
+ * @return {Promise<void>} A promise that resolves when the user is allowed to proceed.
30
+ * @throws {HttpError} If the configured callback returns a string or an error (status 403 unless an `HttpError` is
31
+ * returned, in which case it is thrown as-is).
32
+ */
33
+ async checkUser(source, userInfo, authUser) {
34
+ const serviceConfig = this._authUserService[common_1.CONFIG];
35
+ if (!serviceConfig.checkOAuth2User) {
36
+ return;
37
+ }
38
+ const result = await serviceConfig.checkOAuth2User(source, userInfo, authUser);
39
+ if (!result) {
40
+ return;
41
+ }
42
+ if (result instanceof errors_1.HttpError) {
43
+ throw result;
44
+ }
45
+ throw new errors_1.HttpError(result instanceof Error ? result.message : result, 403, result instanceof Error ? result : undefined);
46
+ }
47
+ /**
48
+ * Decides whether an OAuth2 sign-in has to be confirmed with the account password before it is honored. This is the
49
+ * case the first time a provider account is linked to an existing user that already has a password, since anyone
50
+ * able to create a provider account carrying that email address could otherwise take the account over. The rule is
51
+ * not configurable: turning it off would hand over every password-protected account to whoever can present a
52
+ * matching address.
53
+ *
54
+ * @param {AuthUser} authUser - The local user matched by email address.
55
+ * @param {AuthSource} source - The OAuth2 authentication source.
56
+ * @param {string} providerAccountId - The user identifier reported by the provider.
57
+ * @return {Promise<boolean>} A promise resolving to true when the password has to be confirmed.
58
+ */
59
+ async requiresPasswordConfirmation(authUser, source, providerAccountId) {
60
+ if (!authUser.passwordHash) {
61
+ return false;
62
+ }
63
+ // Without the link table there is nothing to remember a previous confirmation, so asking every time is the only
64
+ // way to keep the guarantee.
65
+ if (!this._connectedAccountService) {
66
+ return true;
67
+ }
68
+ const account = await this.findConnectedAccount(source, providerAccountId);
69
+ return !account || this.connectedAccountOwnerId(account) !== authUser.id;
70
+ }
71
+ /**
72
+ * Finds the connected account linking an OAuth2 provider account to a local user.
73
+ *
74
+ * @param {AuthSource} source - The OAuth2 authentication source.
75
+ * @param {string} providerAccountId - The user identifier reported by the provider.
76
+ * @return {Promise<ConnectedAccount | null>} A promise resolving to the link, or null when the provider account is
77
+ * unknown or connected accounts are not being tracked.
78
+ */
79
+ async findConnectedAccount(source, providerAccountId) {
80
+ const service = this._connectedAccountService;
81
+ if (!service) {
82
+ return null;
83
+ }
84
+ try {
85
+ const result = await service.query({
86
+ provider: source,
87
+ providerAccountId
88
+ });
89
+ return result.items[0] ?? null;
90
+ }
91
+ catch (e) {
92
+ throw new errors_1.HttpError('Connected account find error', 500, e);
93
+ }
94
+ }
95
+ /**
96
+ * Records a successful OAuth2 sign-in, creating the link on the first one and refreshing it afterward. Does nothing
97
+ * when connected accounts are not being tracked.
98
+ *
99
+ * @param {AuthUser} authUser - The local user the provider account belongs to.
100
+ * @param {AuthSource} source - The OAuth2 authentication source.
101
+ * @param {string} providerAccountId - The user identifier reported by the provider.
102
+ * @param {string} [scope] - The scopes granted by the provider.
103
+ * @return {Promise<void>} A promise that resolves once the link is stored.
104
+ * @throws {HttpError} If the provider account is already linked to a different user.
105
+ */
106
+ async linkConnectedAccount(authUser, source, providerAccountId, scope) {
107
+ const service = this._connectedAccountService;
108
+ if (!service) {
109
+ return;
110
+ }
111
+ const account = await this.findConnectedAccount(source, providerAccountId);
112
+ if (account && this.connectedAccountOwnerId(account) !== authUser.id) {
113
+ throw new errors_1.HttpError('This provider account is already linked to another user', 403);
114
+ }
115
+ try {
116
+ if (account) {
117
+ await service.update(account.id, { scope, lastLoginAt: new Date() });
118
+ return;
119
+ }
120
+ await service.create({
121
+ provider: source,
122
+ providerAccountId,
123
+ scope,
124
+ lastLoginAt: new Date(),
125
+ [(0, common_1.uncapitalize)(this._authUserService.modelName)]: { id: authUser.id }
126
+ });
127
+ common_1.logger.debug({ id: authUser.id, source }, 'OAuth2 provider account linked');
128
+ }
129
+ catch (e) {
130
+ throw new errors_1.HttpError('Connected account link error', 500, e);
131
+ }
132
+ }
133
+ /**
134
+ * Reads the owning user id off a link, which the generated model exposes as a `<authModel>Id` foreign key.
135
+ *
136
+ * @internal
137
+ */
138
+ connectedAccountOwnerId(account) {
139
+ return account[`${(0, common_1.uncapitalize)(this._authUserService.modelName)}Id`];
140
+ }
141
+ }
142
+ exports.OAuth2Service = OAuth2Service;