@appweaver/core 1.2.1 → 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.
- package/app/load-providers.js +1 -0
- package/factory/create-service.js +5 -5
- package/package.json +2 -2
- package/prisma/client/browser.d.ts +5 -0
- package/prisma/client/client.d.ts +5 -0
- package/prisma/client/internal/class.d.ts +11 -0
- package/prisma/client/internal/class.js +4 -4
- package/prisma/client/internal/prismaNamespace.d.ts +87 -1
- package/prisma/client/internal/prismaNamespace.js +11 -1
- package/prisma/client/internal/prismaNamespaceBrowser.d.ts +11 -0
- package/prisma/client/internal/prismaNamespaceBrowser.js +11 -1
- package/prisma/client/models/ConnectedAccount.d.ts +1089 -0
- package/prisma/client/models/ConnectedAccount.js +2 -0
- package/prisma/client/models.d.ts +1 -0
- package/resources.d.ts +2 -0
- package/resources.js +15 -11
- package/security/auth-routes.js +2 -2
- package/security/auth-schema.d.ts +2 -0
- package/security/auth-schema.js +5 -1
- package/security/auth-service.d.ts +7 -19
- package/security/auth-service.js +37 -32
- package/security/auth.js +15 -2
- package/security/create-auth-resources.js +15 -0
- package/security/helper.d.ts +8 -0
- package/security/helper.js +13 -0
- package/security/index.d.ts +2 -0
- package/security/index.js +2 -0
- package/security/oauth2/create-oauth2-plugin.d.ts +19 -4
- package/security/oauth2/create-oauth2-plugin.js +66 -23
- package/security/oauth2/index.d.ts +6 -0
- package/security/oauth2/index.js +6 -0
- package/security/oauth2/oauth2-apple.d.ts +10 -0
- package/security/oauth2/oauth2-apple.js +56 -0
- package/security/oauth2/oauth2-custom.d.ts +6 -4
- package/security/oauth2/oauth2-custom.js +6 -8
- package/security/oauth2/oauth2-facebook.d.ts +3 -1
- package/security/oauth2/oauth2-facebook.js +5 -11
- package/security/oauth2/oauth2-github.d.ts +3 -0
- package/security/oauth2/oauth2-github.js +47 -0
- package/security/oauth2/oauth2-gitlab.d.ts +3 -0
- package/security/oauth2/oauth2-gitlab.js +33 -0
- package/security/oauth2/oauth2-google.d.ts +3 -1
- package/security/oauth2/oauth2-google.js +6 -11
- package/security/oauth2/oauth2-linkedin.d.ts +3 -0
- package/security/oauth2/oauth2-linkedin.js +28 -0
- package/security/oauth2/oauth2-microsoft.d.ts +3 -0
- package/security/oauth2/oauth2-microsoft.js +36 -0
- package/security/oauth2/oauth2-schema.d.ts +9 -2
- package/security/oauth2/oauth2-schema.js +20 -6
- package/security/oauth2/oauth2-service.d.ts +58 -0
- package/security/oauth2/oauth2-service.js +142 -0
- package/security/oauth2/oauth2-util.d.ts +60 -0
- package/security/oauth2/oauth2-util.js +206 -0
- package/security/oauth2/oauth2-x.d.ts +3 -0
- package/security/oauth2/oauth2-x.js +33 -0
- package/security/resources/connected-account/model.d.ts +2 -0
- package/security/resources/connected-account/model.js +53 -0
- package/security/resources/connected-account/service.d.ts +2 -0
- package/security/resources/connected-account/service.js +7 -0
- package/types/auth.d.ts +23 -5
- 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
|
|
4
|
-
given_name
|
|
5
|
-
family_name
|
|
4
|
+
email?: string;
|
|
5
|
+
given_name?: string;
|
|
6
|
+
family_name?: string;
|
|
6
7
|
picture?: string;
|
|
7
8
|
};
|
|
8
|
-
export declare const oauth2Custom: (server: import("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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,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
|
-
|
|
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
|
|
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,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,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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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;
|