@appweaver/core 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/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
package/resources.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export { default as apiKey } from './security/resources/api-key/model';
|
|
2
|
+
export { default as connectedAccount } from './security/resources/connected-account/model';
|
|
2
3
|
export { default as oneTimeToken } from './security/resources/one-time-token/model';
|
|
3
4
|
export { default as role } from './security/resources/role/model';
|
|
4
5
|
export { default as permission } from './security/resources/permission/model';
|
|
5
6
|
export { default as seeder } from './seeder/resources/seeder/model';
|
|
6
7
|
export { default as file } from './storage/resources/file/model';
|
|
7
8
|
export { default as apiKeyService } from './security/resources/api-key/service';
|
|
9
|
+
export { default as connectedAccountService } from './security/resources/connected-account/service';
|
|
8
10
|
export { default as apiKeyRoutes } from './security/resources/api-key/routes';
|
|
9
11
|
export { default as apiKeyPolicy } from './security/resources/api-key/policy';
|
package/resources.js
CHANGED
|
@@ -3,21 +3,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.apiKeyPolicy = exports.apiKeyRoutes = exports.apiKeyService = exports.file = exports.seeder = exports.permission = exports.role = exports.oneTimeToken = exports.apiKey = void 0;
|
|
6
|
+
exports.apiKeyPolicy = exports.apiKeyRoutes = exports.connectedAccountService = exports.apiKeyService = exports.file = exports.seeder = exports.permission = exports.role = exports.oneTimeToken = exports.connectedAccount = exports.apiKey = void 0;
|
|
7
7
|
var model_1 = require("./security/resources/api-key/model");
|
|
8
8
|
Object.defineProperty(exports, "apiKey", { enumerable: true, get: function () { return __importDefault(model_1).default; } });
|
|
9
|
-
var model_2 = require("./security/resources/
|
|
10
|
-
Object.defineProperty(exports, "
|
|
11
|
-
var model_3 = require("./security/resources/
|
|
12
|
-
Object.defineProperty(exports, "
|
|
13
|
-
var model_4 = require("./security/resources/
|
|
14
|
-
Object.defineProperty(exports, "
|
|
15
|
-
var model_5 = require("./
|
|
16
|
-
Object.defineProperty(exports, "
|
|
17
|
-
var model_6 = require("./
|
|
18
|
-
Object.defineProperty(exports, "
|
|
9
|
+
var model_2 = require("./security/resources/connected-account/model");
|
|
10
|
+
Object.defineProperty(exports, "connectedAccount", { enumerable: true, get: function () { return __importDefault(model_2).default; } });
|
|
11
|
+
var model_3 = require("./security/resources/one-time-token/model");
|
|
12
|
+
Object.defineProperty(exports, "oneTimeToken", { enumerable: true, get: function () { return __importDefault(model_3).default; } });
|
|
13
|
+
var model_4 = require("./security/resources/role/model");
|
|
14
|
+
Object.defineProperty(exports, "role", { enumerable: true, get: function () { return __importDefault(model_4).default; } });
|
|
15
|
+
var model_5 = require("./security/resources/permission/model");
|
|
16
|
+
Object.defineProperty(exports, "permission", { enumerable: true, get: function () { return __importDefault(model_5).default; } });
|
|
17
|
+
var model_6 = require("./seeder/resources/seeder/model");
|
|
18
|
+
Object.defineProperty(exports, "seeder", { enumerable: true, get: function () { return __importDefault(model_6).default; } });
|
|
19
|
+
var model_7 = require("./storage/resources/file/model");
|
|
20
|
+
Object.defineProperty(exports, "file", { enumerable: true, get: function () { return __importDefault(model_7).default; } });
|
|
19
21
|
var service_1 = require("./security/resources/api-key/service");
|
|
20
22
|
Object.defineProperty(exports, "apiKeyService", { enumerable: true, get: function () { return __importDefault(service_1).default; } });
|
|
23
|
+
var service_2 = require("./security/resources/connected-account/service");
|
|
24
|
+
Object.defineProperty(exports, "connectedAccountService", { enumerable: true, get: function () { return __importDefault(service_2).default; } });
|
|
21
25
|
var routes_1 = require("./security/resources/api-key/routes");
|
|
22
26
|
Object.defineProperty(exports, "apiKeyRoutes", { enumerable: true, get: function () { return __importDefault(routes_1).default; } });
|
|
23
27
|
var policy_1 = require("./security/resources/api-key/policy");
|
package/security/auth-routes.js
CHANGED
|
@@ -63,8 +63,8 @@ function authRoutes(server) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
}, async (request, reply) => {
|
|
66
|
-
const { token } = request.body;
|
|
67
|
-
const authResponse = await authService.exchangeToken(token);
|
|
66
|
+
const { token, password } = request.body;
|
|
67
|
+
const authResponse = await authService.exchangeToken(token, password);
|
|
68
68
|
return reply.send(authResponse);
|
|
69
69
|
});
|
|
70
70
|
const authUserModel = (0, helper_1.resourceAuthModel)();
|
|
@@ -19,6 +19,7 @@ export declare const LogoutResponse: TObject<{
|
|
|
19
19
|
}>;
|
|
20
20
|
export declare const ExchangeTokenRequest: TObject<{
|
|
21
21
|
token: import("@sinclair/typebox").TString;
|
|
22
|
+
password: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
22
23
|
}>;
|
|
23
24
|
export declare const loginSchema: {
|
|
24
25
|
tags: string[];
|
|
@@ -208,6 +209,7 @@ export declare const exchangeTokenSchema: {
|
|
|
208
209
|
};
|
|
209
210
|
body: TObject<{
|
|
210
211
|
token: import("@sinclair/typebox").TString;
|
|
212
|
+
password: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
211
213
|
}>;
|
|
212
214
|
};
|
|
213
215
|
export declare function authSchema(authTypes?: AuthType[]): any[];
|
package/security/auth-schema.js
CHANGED
|
@@ -30,7 +30,11 @@ exports.LogoutResponse = typebox_1.Type.Object({
|
|
|
30
30
|
success: typebox_1.Type.Boolean({ example: true })
|
|
31
31
|
}, { $id: 'LogoutResponse' });
|
|
32
32
|
exports.ExchangeTokenRequest = typebox_1.Type.Object({
|
|
33
|
-
token: typebox_1.Type.String({ example: 'aBcDeFgHijkLMnO123456789' })
|
|
33
|
+
token: typebox_1.Type.String({ example: 'aBcDeFgHijkLMnO123456789' }),
|
|
34
|
+
password: typebox_1.Type.Optional(typebox_1.Type.String({
|
|
35
|
+
description: 'Account password, required when the OAuth2 redirect returned `passwordRequired=true`',
|
|
36
|
+
example: 'yourPassword123!'
|
|
37
|
+
}))
|
|
34
38
|
}, { $id: 'ExchangeTokenRequest' });
|
|
35
39
|
exports.loginSchema = {
|
|
36
40
|
tags: ['Auth'],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthScope, AuthSource, AuthUser, RouteConfig } from '@appweaver/common';
|
|
2
|
-
import { AuthTokens, JwtPayload, UserAdditionalData
|
|
2
|
+
import { AuthTokens, JwtPayload, UserAdditionalData } from '../types';
|
|
3
3
|
export declare class AuthService {
|
|
4
4
|
/**
|
|
5
5
|
* Finds an authenticated user by their unique identifier.
|
|
@@ -40,21 +40,6 @@ export declare class AuthService {
|
|
|
40
40
|
* @throws {HttpError} Throws an error if the registration process fails.
|
|
41
41
|
*/
|
|
42
42
|
registerAuthUser(source: AuthSource, email: string, password?: string, data?: Partial<UserAdditionalData>): Promise<AuthUser>;
|
|
43
|
-
/**
|
|
44
|
-
* Checks whether a user is allowed to be registered and/or authenticated via OAuth2 by invoking the optional
|
|
45
|
-
* `checkOAuth2User` callback configured on the auth service. When the callback returns nothing, the OAuth2 flow
|
|
46
|
-
* proceeds normally (registration of a new user or login of an existing one). When it returns a string or an error,
|
|
47
|
-
* the flow is aborted by throwing an `HttpError`.
|
|
48
|
-
*
|
|
49
|
-
* @param {AuthSource} source - The OAuth2 authentication source, e.g., oauth2Google, oauth2Facebook, oauth2Custom.
|
|
50
|
-
* @param {UserInfo} userInfo - The user info extracted from the OAuth2 provider.
|
|
51
|
-
* @param {AuthUser | null} authUser - The existing authenticated user matched by email, or null when the user does
|
|
52
|
-
* not exist yet (i.e., a new user would be registered).
|
|
53
|
-
* @return {Promise<void>} A promise that resolves when the user is allowed to proceed.
|
|
54
|
-
* @throws {HttpError} If the configured callback returns a string or an error (status 403 unless an `HttpError` is
|
|
55
|
-
* returned, in which case it is thrown as-is).
|
|
56
|
-
*/
|
|
57
|
-
checkOAuth2User(source: AuthSource, userInfo: UserInfo, authUser: AuthUser | null): Promise<void>;
|
|
58
43
|
/**
|
|
59
44
|
* Changes the password for the authenticated user.
|
|
60
45
|
*
|
|
@@ -99,13 +84,16 @@ export declare class AuthService {
|
|
|
99
84
|
*/
|
|
100
85
|
login(username: string, password: string): Promise<AuthTokens>;
|
|
101
86
|
/**
|
|
102
|
-
* Exchanges an existing token for new authentication tokens.
|
|
87
|
+
* Exchanges an existing token for new authentication tokens. When the one-time token was issued for an OAuth2
|
|
88
|
+
* sign-in that needs confirming, the account password must be supplied alongside it.
|
|
103
89
|
*
|
|
104
90
|
* @param {string} token - The token to be exchanged for new authentication tokens.
|
|
91
|
+
* @param {string} [password] - The account password, required when the token was flagged for confirmation.
|
|
105
92
|
* @return {Promise<AuthTokens>} A promise that resolves to a set of new authentication tokens.
|
|
106
|
-
* @throws {HttpError} If the token is invalid, expired,
|
|
93
|
+
* @throws {HttpError} If the token is invalid, expired, associated with a non-existent or disabled user, or the
|
|
94
|
+
* required password confirmation is missing or wrong.
|
|
107
95
|
*/
|
|
108
|
-
exchangeToken(token: string): Promise<AuthTokens>;
|
|
96
|
+
exchangeToken(token: string, password?: string): Promise<AuthTokens>;
|
|
109
97
|
/**
|
|
110
98
|
* Generates authentication tokens (access and refresh tokens) for a given authenticated user.
|
|
111
99
|
*
|
package/security/auth-service.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.AuthService = void 0;
|
|
4
4
|
const common_1 = require("@appweaver/common");
|
|
5
5
|
const helper_1 = require("./helper");
|
|
6
|
+
const oauth2_service_1 = require("./oauth2/oauth2-service");
|
|
6
7
|
const context_1 = require("../context");
|
|
7
8
|
const cache_1 = require("../cache");
|
|
8
9
|
const errors_1 = require("../errors");
|
|
@@ -14,6 +15,9 @@ class AuthService {
|
|
|
14
15
|
_cacheService = (0, context_1.inject)(cache_1.CacheService);
|
|
15
16
|
/** @internal */
|
|
16
17
|
_authUserService = (0, helper_1.resourceAuthService)();
|
|
18
|
+
/** Optional, since an application with no OAuth2 provider never registers it.
|
|
19
|
+
* @internal */
|
|
20
|
+
_oauth2Service = (0, context_1.inject)(oauth2_service_1.OAuth2Service, false);
|
|
17
21
|
/**
|
|
18
22
|
* Finds an authenticated user by their unique identifier.
|
|
19
23
|
*
|
|
@@ -114,34 +118,6 @@ class AuthService {
|
|
|
114
118
|
throw new errors_1.HttpError('Auth user registration error', 500, e);
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Checks whether a user is allowed to be registered and/or authenticated via OAuth2 by invoking the optional
|
|
119
|
-
* `checkOAuth2User` callback configured on the auth service. When the callback returns nothing, the OAuth2 flow
|
|
120
|
-
* proceeds normally (registration of a new user or login of an existing one). When it returns a string or an error,
|
|
121
|
-
* the flow is aborted by throwing an `HttpError`.
|
|
122
|
-
*
|
|
123
|
-
* @param {AuthSource} source - The OAuth2 authentication source, e.g., oauth2Google, oauth2Facebook, oauth2Custom.
|
|
124
|
-
* @param {UserInfo} userInfo - The user info extracted from the OAuth2 provider.
|
|
125
|
-
* @param {AuthUser | null} authUser - The existing authenticated user matched by email, or null when the user does
|
|
126
|
-
* not exist yet (i.e., a new user would be registered).
|
|
127
|
-
* @return {Promise<void>} A promise that resolves when the user is allowed to proceed.
|
|
128
|
-
* @throws {HttpError} If the configured callback returns a string or an error (status 403 unless an `HttpError` is
|
|
129
|
-
* returned, in which case it is thrown as-is).
|
|
130
|
-
*/
|
|
131
|
-
async checkOAuth2User(source, userInfo, authUser) {
|
|
132
|
-
const serviceConfig = this._authUserService[common_1.CONFIG];
|
|
133
|
-
if (!serviceConfig.checkOAuth2User) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const result = await serviceConfig.checkOAuth2User(source, userInfo, authUser);
|
|
137
|
-
if (!result) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (result instanceof errors_1.HttpError) {
|
|
141
|
-
throw result;
|
|
142
|
-
}
|
|
143
|
-
throw new errors_1.HttpError(result instanceof Error ? result.message : result, 403, result instanceof Error ? result : undefined);
|
|
144
|
-
}
|
|
145
121
|
/**
|
|
146
122
|
* Changes the password for the authenticated user.
|
|
147
123
|
*
|
|
@@ -236,21 +212,50 @@ class AuthService {
|
|
|
236
212
|
return this.generateAuthTokens(authUser);
|
|
237
213
|
}
|
|
238
214
|
/**
|
|
239
|
-
* Exchanges an existing token for new authentication tokens.
|
|
215
|
+
* Exchanges an existing token for new authentication tokens. When the one-time token was issued for an OAuth2
|
|
216
|
+
* sign-in that needs confirming, the account password must be supplied alongside it.
|
|
240
217
|
*
|
|
241
218
|
* @param {string} token - The token to be exchanged for new authentication tokens.
|
|
219
|
+
* @param {string} [password] - The account password, required when the token was flagged for confirmation.
|
|
242
220
|
* @return {Promise<AuthTokens>} A promise that resolves to a set of new authentication tokens.
|
|
243
|
-
* @throws {HttpError} If the token is invalid, expired,
|
|
221
|
+
* @throws {HttpError} If the token is invalid, expired, associated with a non-existent or disabled user, or the
|
|
222
|
+
* required password confirmation is missing or wrong.
|
|
244
223
|
*/
|
|
245
|
-
async exchangeToken(token) {
|
|
246
|
-
const { authUserId, authSource } = await this._securityStore.useOneTimeToken(token, common_1.AuthOTTPurpose.Authentication);
|
|
224
|
+
async exchangeToken(token, password) {
|
|
225
|
+
const { authUserId, authSource, providerAccountId, scope, passwordRequired } = await this._securityStore.useOneTimeToken(token, common_1.AuthOTTPurpose.Authentication);
|
|
247
226
|
const authUser = await this.findById(authUserId);
|
|
248
227
|
if (!authUser || !authUser.enabled) {
|
|
249
228
|
throw new errors_1.HttpError('Auth user does not exist or is disabled', 400);
|
|
250
229
|
}
|
|
230
|
+
if (passwordRequired) {
|
|
231
|
+
await this.confirmPassword(authUser, password);
|
|
232
|
+
}
|
|
233
|
+
if (providerAccountId) {
|
|
234
|
+
await this._oauth2Service?.linkConnectedAccount(authUser, authSource, providerAccountId, scope);
|
|
235
|
+
// The provider confirmed the address, and ownership of the local account was proven by the password whenever it
|
|
236
|
+
// had one, so an address that was never verified locally is settled by this sign-in
|
|
237
|
+
if (!authUser.verifiedEmail) {
|
|
238
|
+
await this.updateAuthUser(authUser.id, { verifiedEmail: true });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
251
241
|
common_1.logger.debug({ id: authUser.id, token }, 'User token exchanged');
|
|
252
242
|
return this.generateAuthTokens(authUser, common_1.AuthScope.Auth, authSource);
|
|
253
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Verifies the account password supplied to confirm linking an OAuth2 provider.
|
|
246
|
+
*
|
|
247
|
+
* @internal
|
|
248
|
+
*/
|
|
249
|
+
async confirmPassword(authUser, password) {
|
|
250
|
+
if (!password) {
|
|
251
|
+
throw new errors_1.HttpError('Password confirmation is required to link this provider account', 401);
|
|
252
|
+
}
|
|
253
|
+
if (!authUser.passwordHash ||
|
|
254
|
+
!(await (0, helper_1.checkPassword)(password, authUser.passwordHash))) {
|
|
255
|
+
common_1.logger.debug({ id: authUser.id }, 'OAuth2 password confirmation rejected');
|
|
256
|
+
throw new errors_1.HttpError('Invalid user credentials', 401);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
254
259
|
/**
|
|
255
260
|
* Generates authentication tokens (access and refresh tokens) for a given authenticated user.
|
|
256
261
|
*
|
package/security/auth.js
CHANGED
|
@@ -48,6 +48,17 @@ const basic_1 = require("./basic");
|
|
|
48
48
|
const api_key_1 = require("./api-key");
|
|
49
49
|
const jwt_1 = require("./jwt");
|
|
50
50
|
const oauth2Plugins = __importStar(require("./oauth2"));
|
|
51
|
+
const PLUGIN_META = Symbol.for('plugin-meta');
|
|
52
|
+
/**
|
|
53
|
+
* Tells the fastify plugins exported from the oauth2 directory apart from the user info extractors the same modules
|
|
54
|
+
* export, using the metadata `fastify-plugin` attaches to every plugin it wraps.
|
|
55
|
+
*
|
|
56
|
+
* @param {unknown} value - The exported value to check.
|
|
57
|
+
* @return {boolean} Whether the value is a fastify plugin.
|
|
58
|
+
*/
|
|
59
|
+
function isFastifyPlugin(value) {
|
|
60
|
+
return typeof value === 'function' && PLUGIN_META in value;
|
|
61
|
+
}
|
|
51
62
|
exports.default = (0, fastify_plugin_1.default)((server) => {
|
|
52
63
|
server.register(auth_1.default);
|
|
53
64
|
if (common_1.config.SECURITY_RECAPTCHA_ENABLED) {
|
|
@@ -62,8 +73,10 @@ exports.default = (0, fastify_plugin_1.default)((server) => {
|
|
|
62
73
|
server.register(jwt_1.jwtAuth);
|
|
63
74
|
// Load and register all plugins exported from oauth2 dir, plugin-enabled
|
|
64
75
|
// status and required config values are checked at plugin initialization
|
|
65
|
-
for (const
|
|
66
|
-
|
|
76
|
+
for (const exported of Object.values(oauth2Plugins)) {
|
|
77
|
+
if (isFastifyPlugin(exported)) {
|
|
78
|
+
server.register(exported);
|
|
79
|
+
}
|
|
67
80
|
}
|
|
68
81
|
server.register(auth_routes_1.authRoutes, { prefix: common_1.config.SECURITY_ROUTE_PREFIX });
|
|
69
82
|
server.register(account_1.accountRoutes, {
|
|
@@ -85,6 +85,21 @@ function createAuthModel(config) {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
: {}),
|
|
89
|
+
...((0, helper_1.isOAuth2Enabled)()
|
|
90
|
+
? {
|
|
91
|
+
connectedAccounts: {
|
|
92
|
+
model: 'ConnectedAccount',
|
|
93
|
+
type: 'oneToMany',
|
|
94
|
+
mappedBy: (0, common_1.uncapitalize)(config.name),
|
|
95
|
+
input: {
|
|
96
|
+
type: 'none'
|
|
97
|
+
},
|
|
98
|
+
output: {
|
|
99
|
+
type: 'none'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
88
103
|
: {})
|
|
89
104
|
};
|
|
90
105
|
const authModelInputOmit = ['verifiedEmail', 'logoutAt'];
|
package/security/helper.d.ts
CHANGED
|
@@ -71,6 +71,14 @@ export declare function updatePasswordHash(authUser: Partial<AuthUser>, password
|
|
|
71
71
|
* @return {ValidationResult} An object with the validation result and message.
|
|
72
72
|
*/
|
|
73
73
|
export declare function validatePasswordComplexity(password: string): ValidationResult;
|
|
74
|
+
/**
|
|
75
|
+
* Checks whether at least one OAuth2 provider is enabled, by resolving each OAuth2 auth source to its
|
|
76
|
+
* `SECURITY_OAUTH2_<PROVIDER>_ENABLED` config flag. New providers are picked up automatically as long as they follow
|
|
77
|
+
* that naming convention.
|
|
78
|
+
*
|
|
79
|
+
* @return {boolean} True when any OAuth2 provider is enabled.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isOAuth2Enabled(): boolean;
|
|
74
82
|
/**
|
|
75
83
|
* Validates a URL and checks if it's allowed based on the configured list of allowed hosts.
|
|
76
84
|
*
|
package/security/helper.js
CHANGED
|
@@ -42,6 +42,7 @@ exports.hashPassword = hashPassword;
|
|
|
42
42
|
exports.checkPassword = checkPassword;
|
|
43
43
|
exports.updatePasswordHash = updatePasswordHash;
|
|
44
44
|
exports.validatePasswordComplexity = validatePasswordComplexity;
|
|
45
|
+
exports.isOAuth2Enabled = isOAuth2Enabled;
|
|
45
46
|
exports.validateRedirectUrl = validateRedirectUrl;
|
|
46
47
|
exports.checkScopeAccess = checkScopeAccess;
|
|
47
48
|
exports.hasRole = hasRole;
|
|
@@ -203,6 +204,18 @@ function validatePasswordComplexity(password) {
|
|
|
203
204
|
}
|
|
204
205
|
return { valid: true, message: 'OK' };
|
|
205
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Checks whether at least one OAuth2 provider is enabled, by resolving each OAuth2 auth source to its
|
|
209
|
+
* `SECURITY_OAUTH2_<PROVIDER>_ENABLED` config flag. New providers are picked up automatically as long as they follow
|
|
210
|
+
* that naming convention.
|
|
211
|
+
*
|
|
212
|
+
* @return {boolean} True when any OAuth2 provider is enabled.
|
|
213
|
+
*/
|
|
214
|
+
function isOAuth2Enabled() {
|
|
215
|
+
return Object.values(common_1.AuthSource)
|
|
216
|
+
.filter((source) => source.startsWith('oauth2'))
|
|
217
|
+
.some((source) => common_1.config[`SECURITY_OAUTH2_${source.replace('oauth2', '').toUpperCase()}_ENABLED`] === true);
|
|
218
|
+
}
|
|
206
219
|
/**
|
|
207
220
|
* Validates a URL and checks if it's allowed based on the configured list of allowed hosts.
|
|
208
221
|
*
|
package/security/index.d.ts
CHANGED
package/security/index.js
CHANGED
|
@@ -15,6 +15,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./oauth2/create-oauth2-plugin"), exports);
|
|
18
|
+
__exportStar(require("./oauth2/oauth2-service"), exports);
|
|
19
|
+
__exportStar(require("./oauth2/oauth2-util"), exports);
|
|
18
20
|
__exportStar(require("./auth-service"), exports);
|
|
19
21
|
__exportStar(require("./auth-schema"), exports);
|
|
20
22
|
__exportStar(require("./helper"), exports);
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
+
import { ProviderConfiguration } from '@fastify/oauth2';
|
|
1
2
|
import { AuthSource } from '@appweaver/common';
|
|
2
|
-
import { Server, UserInfo } from '../../types';
|
|
3
|
-
export type { UserInfo };
|
|
3
|
+
import { OAuth2TokenSet, OAuth2UserInfoContext, Server, UserInfo } from '../../types';
|
|
4
|
+
export type { UserInfo, OAuth2TokenSet, OAuth2UserInfoContext };
|
|
4
5
|
export type OAuth2Config = {
|
|
5
6
|
enabled: boolean;
|
|
6
7
|
clientId?: string;
|
|
7
|
-
|
|
8
|
+
/** A function is resolved lazily, only once the provider is known to be enabled. */
|
|
9
|
+
clientSecret?: string | (() => string | undefined);
|
|
8
10
|
issuer?: string;
|
|
9
11
|
scope: string[];
|
|
10
|
-
|
|
12
|
+
/** Provider endpoints. Defaults to the `@fastify/oauth2` preset matching the auth source name. */
|
|
13
|
+
auth?: ProviderConfiguration;
|
|
14
|
+
/** Human-readable provider name used in the generated OpenAPI schema. Defaults to the auth source name. */
|
|
15
|
+
displayName?: string;
|
|
16
|
+
/** Enables the PKCE extension, required by providers such as X. */
|
|
17
|
+
pkce?: 'S256' | 'plain';
|
|
18
|
+
/** Provider posts the authorization response as a form body instead of query parameters (`response_mode=form_post`). */
|
|
19
|
+
formPostCallback?: boolean;
|
|
20
|
+
/** `User-Agent` sent with the token request. Some providers reject the default one. */
|
|
21
|
+
userAgent?: string;
|
|
22
|
+
/** How client credentials are sent to the token endpoint. Defaults to `header` (HTTP Basic); providers such as
|
|
23
|
+
* Apple and LinkedIn only accept them in the request body. */
|
|
24
|
+
authorizationMethod?: 'header' | 'body';
|
|
25
|
+
extractUserInfo: (accessToken: string, context: OAuth2UserInfoContext) => Promise<UserInfo>;
|
|
11
26
|
};
|
|
12
27
|
export declare function createOAuth2Plugin(authSource: AuthSource, oAuth2Config: OAuth2Config): (server: Server) => Promise<void>;
|
|
@@ -10,45 +10,64 @@ const common_1 = require("@appweaver/common");
|
|
|
10
10
|
const context_1 = require("../../context");
|
|
11
11
|
const errors_1 = require("../../errors");
|
|
12
12
|
const auth_service_1 = require("../auth-service");
|
|
13
|
+
const oauth2_service_1 = require("./oauth2-service");
|
|
13
14
|
const helper_1 = require("../helper");
|
|
14
15
|
const oauth2_schema_1 = require("./oauth2-schema");
|
|
15
16
|
function createOAuth2Plugin(authSource, oAuth2Config) {
|
|
16
17
|
const name = authSource.replace('oauth2', '');
|
|
17
18
|
const upperName = name.toUpperCase();
|
|
18
19
|
const lowerName = name.toLowerCase();
|
|
20
|
+
const displayName = oAuth2Config.displayName ?? name;
|
|
19
21
|
return (0, fastify_plugin_1.default)(async (server) => {
|
|
20
22
|
if (!oAuth2Config.enabled) {
|
|
21
23
|
return;
|
|
22
24
|
}
|
|
23
|
-
|
|
25
|
+
const clientSecret = typeof oAuth2Config.clientSecret === 'function'
|
|
26
|
+
? oAuth2Config.clientSecret()
|
|
27
|
+
: oAuth2Config.clientSecret;
|
|
28
|
+
if (!oAuth2Config.clientId || !clientSecret) {
|
|
24
29
|
throw Error(`${name} OAuth2 configuration is missing`);
|
|
25
30
|
}
|
|
26
31
|
if (server[authSource] !== undefined) {
|
|
27
32
|
throw Error(`${name} OAuth2 provider is already registered`);
|
|
28
33
|
}
|
|
29
|
-
const authConfig = oauth2_1.default[`${upperName}_CONFIGURATION`];
|
|
34
|
+
const authConfig = oAuth2Config.auth ?? oauth2_1.default[`${upperName}_CONFIGURATION`];
|
|
30
35
|
if (!authConfig && !oAuth2Config.issuer) {
|
|
31
36
|
throw Error(`${name} OAuth2 provider is not supported`);
|
|
32
37
|
}
|
|
33
38
|
const authService = (0, context_1.inject)(auth_service_1.AuthService);
|
|
39
|
+
const oauth2Service = (0, context_1.inject)(oauth2_service_1.OAuth2Service);
|
|
34
40
|
const securityStore = (0, context_1.inject)(common_1.SecurityStore);
|
|
35
41
|
const prefix = common_1.config.SECURITY_ROUTE_PREFIX.replace(/\/$/, '');
|
|
42
|
+
const callbackPath = `${prefix}/login/${lowerName}/callback`;
|
|
43
|
+
// With `response_mode=form_post` the authorization response arrives in the request body, while
|
|
44
|
+
// @fastify/oauth2 always reads the `code` and `state` from the query string.
|
|
45
|
+
const authResponse = (request) => (oAuth2Config.formPostCallback
|
|
46
|
+
? request.body
|
|
47
|
+
: request.query) ?? {};
|
|
36
48
|
server.register(oauth2_1.default, {
|
|
37
49
|
name: authSource,
|
|
38
50
|
credentials: {
|
|
39
51
|
client: {
|
|
40
52
|
id: oAuth2Config.clientId,
|
|
41
|
-
secret:
|
|
53
|
+
secret: clientSecret
|
|
42
54
|
},
|
|
43
|
-
auth: authConfig
|
|
55
|
+
auth: authConfig,
|
|
56
|
+
...(oAuth2Config.authorizationMethod
|
|
57
|
+
? {
|
|
58
|
+
options: { authorizationMethod: oAuth2Config.authorizationMethod }
|
|
59
|
+
}
|
|
60
|
+
: {})
|
|
44
61
|
},
|
|
45
62
|
...(oAuth2Config.issuer
|
|
46
63
|
? { discovery: { issuer: oAuth2Config.issuer } }
|
|
47
64
|
: {}),
|
|
48
65
|
scope: oAuth2Config.scope,
|
|
49
|
-
schema: (0, oauth2_schema_1.createOAuth2RedirectSchema)(
|
|
66
|
+
schema: (0, oauth2_schema_1.createOAuth2RedirectSchema)(displayName),
|
|
50
67
|
startRedirectPath: `${prefix}/login/${lowerName}`,
|
|
51
|
-
callbackUri: `${common_1.config.APP_HOSTNAME}${
|
|
68
|
+
callbackUri: `${common_1.config.APP_HOSTNAME}${callbackPath}`,
|
|
69
|
+
...(oAuth2Config.pkce ? { pkce: oAuth2Config.pkce } : {}),
|
|
70
|
+
...(oAuth2Config.userAgent ? { userAgent: oAuth2Config.userAgent } : {}),
|
|
52
71
|
generateStateFunction: async (request) => {
|
|
53
72
|
const redirectToUrl = request.query.redirectToUrl;
|
|
54
73
|
const result = (0, helper_1.validateRedirectUrl)(redirectToUrl);
|
|
@@ -58,39 +77,63 @@ function createOAuth2Plugin(authSource, oAuth2Config) {
|
|
|
58
77
|
return securityStore.generateOneTimeToken(common_1.AuthOTTPurpose.OAuth2State, { redirectToUrl }, common_1.config.SECURITY_OAUTH2_STATE_TTL);
|
|
59
78
|
},
|
|
60
79
|
checkStateFunction: async (request) => {
|
|
61
|
-
const state = request.
|
|
80
|
+
const state = authResponse(request).state;
|
|
62
81
|
request.oauth2State =
|
|
63
82
|
await securityStore.useOneTimeToken(state, common_1.AuthOTTPurpose.OAuth2State);
|
|
64
83
|
return true;
|
|
65
84
|
}
|
|
66
85
|
});
|
|
67
|
-
|
|
68
|
-
schema: (0, oauth2_schema_1.createOAuth2CallbackSchema)(name)
|
|
69
|
-
}, async function (request, reply) {
|
|
86
|
+
const callbackHandler = async function (request, reply) {
|
|
70
87
|
const { token } = await server[authSource].getAccessTokenFromAuthorizationCodeFlow(request);
|
|
71
|
-
const userInfo = await oAuth2Config.extractUserInfo(token.access_token
|
|
88
|
+
const userInfo = await oAuth2Config.extractUserInfo(token.access_token, {
|
|
89
|
+
token: token,
|
|
90
|
+
request
|
|
91
|
+
});
|
|
72
92
|
let authUser = await authService.findByUsername(userInfo.email);
|
|
73
|
-
await
|
|
93
|
+
await oauth2Service.checkUser(authSource, userInfo, authUser);
|
|
94
|
+
// Decided before the user is (possibly) registered, so a freshly created account is never asked to confirm
|
|
95
|
+
const passwordRequired = !!authUser &&
|
|
96
|
+
(await oauth2Service.requiresPasswordConfirmation(authUser, authSource, userInfo.id));
|
|
74
97
|
if (!authUser) {
|
|
75
98
|
if (!common_1.config.SECURITY_OAUTH2_REGISTRATION_ENABLED) {
|
|
76
99
|
throw new errors_1.HttpError('Auth user does not exist and OAuth2 registration is disabled', 403);
|
|
77
100
|
}
|
|
78
101
|
authUser = await authService.registerAuthUser(authSource, userInfo.email, undefined, {
|
|
79
|
-
...(0, common_1.pickProperties)(userInfo, [
|
|
80
|
-
|
|
81
|
-
'lastName',
|
|
82
|
-
'avatarUrl'
|
|
83
|
-
]),
|
|
84
|
-
avatarFile: await fetchAvatarFile(userInfo)
|
|
102
|
+
...(0, common_1.pickProperties)(userInfo, ['firstName', 'lastName', 'avatarUrl']),
|
|
103
|
+
avatarFile: userInfo.avatarFile ?? (await fetchAvatarFile(userInfo))
|
|
85
104
|
});
|
|
86
105
|
}
|
|
87
|
-
else if (!authUser.verifiedEmail) {
|
|
88
|
-
throw new errors_1.HttpError('Auth user email address is not verified', 403);
|
|
89
|
-
}
|
|
90
106
|
const stateData = request.oauth2State;
|
|
91
|
-
const ott = await securityStore.generateOneTimeToken(common_1.AuthOTTPurpose.Authentication, {
|
|
107
|
+
const ott = await securityStore.generateOneTimeToken(common_1.AuthOTTPurpose.Authentication, {
|
|
108
|
+
authUserId: authUser.id,
|
|
109
|
+
authSource,
|
|
110
|
+
providerAccountId: userInfo.id,
|
|
111
|
+
scope: token.scope ?? oAuth2Config.scope.join(' '),
|
|
112
|
+
passwordRequired
|
|
113
|
+
}, common_1.config.SECURITY_AUTH_OTT_TTL);
|
|
92
114
|
const separator = stateData.redirectToUrl.includes('?') ? '&' : '?';
|
|
93
|
-
|
|
115
|
+
// Tell the client upfront so it can collect the password before spending the single-use token
|
|
116
|
+
const confirmation = passwordRequired ? '&passwordRequired=true' : '';
|
|
117
|
+
return reply.redirect(`${stateData.redirectToUrl}${separator}token=${ott}${confirmation}`);
|
|
118
|
+
};
|
|
119
|
+
if (!oAuth2Config.formPostCallback) {
|
|
120
|
+
server.get(callbackPath, { schema: (0, oauth2_schema_1.createOAuth2CallbackSchema)(displayName) }, callbackHandler);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Register the form-post callback in an encapsulated scope so its urlencoded body parser and
|
|
124
|
+
// the query rewrite below stay local to this single route.
|
|
125
|
+
await server.register(async (scope) => {
|
|
126
|
+
if (!scope.hasContentTypeParser('application/x-www-form-urlencoded')) {
|
|
127
|
+
scope.addContentTypeParser('application/x-www-form-urlencoded', { parseAs: 'string' }, (_request, body, done) => done(null, Object.fromEntries(new URLSearchParams(body))));
|
|
128
|
+
}
|
|
129
|
+
scope.post(callbackPath, {
|
|
130
|
+
schema: (0, oauth2_schema_1.createOAuth2CallbackSchema)(displayName, true),
|
|
131
|
+
preHandler: async (request) => {
|
|
132
|
+
// @fastify/oauth2 reads the authorization code off the query string only.
|
|
133
|
+
const { code, state } = authResponse(request);
|
|
134
|
+
request.query = { ...request.query, code, state };
|
|
135
|
+
}
|
|
136
|
+
}, callbackHandler);
|
|
94
137
|
});
|
|
95
138
|
});
|
|
96
139
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
export * from './oauth2-custom';
|
|
2
|
+
export * from './oauth2-apple';
|
|
2
3
|
export * from './oauth2-facebook';
|
|
4
|
+
export * from './oauth2-github';
|
|
5
|
+
export * from './oauth2-gitlab';
|
|
3
6
|
export * from './oauth2-google';
|
|
7
|
+
export * from './oauth2-linkedin';
|
|
8
|
+
export * from './oauth2-microsoft';
|
|
9
|
+
export * from './oauth2-x';
|
package/security/oauth2/index.js
CHANGED
|
@@ -15,5 +15,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./oauth2-custom"), exports);
|
|
18
|
+
__exportStar(require("./oauth2-apple"), exports);
|
|
18
19
|
__exportStar(require("./oauth2-facebook"), exports);
|
|
20
|
+
__exportStar(require("./oauth2-github"), exports);
|
|
21
|
+
__exportStar(require("./oauth2-gitlab"), exports);
|
|
19
22
|
__exportStar(require("./oauth2-google"), exports);
|
|
23
|
+
__exportStar(require("./oauth2-linkedin"), exports);
|
|
24
|
+
__exportStar(require("./oauth2-microsoft"), exports);
|
|
25
|
+
__exportStar(require("./oauth2-x"), exports);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { OAuth2UserInfoContext, UserInfo } from '../../types';
|
|
2
|
+
export declare const oauth2Apple: (server: import("../../types").Server) => Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* Builds the user info from Apple's token response. Apple has no user info endpoint: the identity is carried by the
|
|
5
|
+
* `id_token`, and the display name is posted alongside the authorization code on the very first authorization only.
|
|
6
|
+
*
|
|
7
|
+
* @param {OAuth2UserInfoContext} context - The token set and callback request from the authorization code flow.
|
|
8
|
+
* @return {Promise<UserInfo>} A promise resolving to the extracted user info.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractAppleUser({ token, request }: OAuth2UserInfoContext): Promise<UserInfo>;
|