@app-connect/core 1.7.26 → 1.7.27
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/handlers/auth.js +26 -2
- package/handlers/log.js +7 -1
- package/handlers/managedOAuth.js +247 -0
- package/index.js +99 -77
- package/mcp/tools/logout.js +4 -1
- package/package.json +1 -1
- package/releaseNotes.json +8 -0
- package/test/handlers/auth.test.js +74 -0
- package/test/routes/managedAuthRoutes.test.js +131 -0
package/handlers/auth.js
CHANGED
|
@@ -7,6 +7,7 @@ const adminCore = require('./admin');
|
|
|
7
7
|
const { Connector } = require('../models/dynamo/connectorSchema');
|
|
8
8
|
const { handleDatabaseError } = require('../lib/errorHandler');
|
|
9
9
|
const managedAuthCore = require('./managedAuth');
|
|
10
|
+
const managedOAuthCore = require('./managedOAuth');
|
|
10
11
|
const { getHashValue } = require('../lib/util');
|
|
11
12
|
|
|
12
13
|
async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcExtensionId, isFromMCP = false }) {
|
|
@@ -20,7 +21,19 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcEx
|
|
|
20
21
|
if (proxyId) {
|
|
21
22
|
proxyConfig = await Connector.getProxyConfig(proxyId);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
let managedOAuthSource = null;
|
|
25
|
+
let oauthInfo = null;
|
|
26
|
+
if (query.rcAccountId) {
|
|
27
|
+
const managedOAuthResult = await managedOAuthCore.resolveManagedOAuthInfo({
|
|
28
|
+
rcAccountId: query.rcAccountId,
|
|
29
|
+
platform
|
|
30
|
+
});
|
|
31
|
+
managedOAuthSource = managedOAuthResult.source;
|
|
32
|
+
oauthInfo = managedOAuthResult.oauthInfo;
|
|
33
|
+
}
|
|
34
|
+
if (!oauthInfo) {
|
|
35
|
+
oauthInfo = await platformModule.getOauthInfo({ tokenUrl, hostname, rcAccountId: query.rcAccountId, proxyId, proxyConfig, userEmail, isFromMCP });
|
|
36
|
+
}
|
|
24
37
|
if (oauthInfo.failMessage) {
|
|
25
38
|
return {
|
|
26
39
|
userInfo: null,
|
|
@@ -66,6 +79,12 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcEx
|
|
|
66
79
|
if (platformModule.postSaveUserInfo) {
|
|
67
80
|
userInfo = await platformModule.postSaveUserInfo({ userInfo, oauthApp });
|
|
68
81
|
}
|
|
82
|
+
if (managedOAuthSource === 'pending') {
|
|
83
|
+
await managedOAuthCore.migratePendingManagedOAuth({
|
|
84
|
+
rcAccountId: query.rcAccountId,
|
|
85
|
+
platform
|
|
86
|
+
});
|
|
87
|
+
}
|
|
69
88
|
return {
|
|
70
89
|
userInfo,
|
|
71
90
|
returnMessage
|
|
@@ -249,7 +268,12 @@ async function authValidation({ platform, userId }) {
|
|
|
249
268
|
if (existingUser) {
|
|
250
269
|
const platformModule = connectorRegistry.getConnector(platform);
|
|
251
270
|
const proxyId = existingUser?.platformAdditionalInfo?.proxyId;
|
|
252
|
-
const
|
|
271
|
+
const managedOAuthResult = await managedOAuthCore.resolveManagedOAuthInfo({
|
|
272
|
+
rcAccountId: existingUser.rcAccountId,
|
|
273
|
+
platform
|
|
274
|
+
});
|
|
275
|
+
const oauthInfo = managedOAuthResult.oauthInfo ?? await platformModule.getOauthInfo({ tokenUrl: existingUser?.platformAdditionalInfo?.tokenUrl, hostname: existingUser?.hostname, proxyId });
|
|
276
|
+
const oauthApp = oauth.getOAuthApp(oauthInfo);
|
|
253
277
|
existingUser = await oauth.checkAndRefreshAccessToken(oauthApp, existingUser);
|
|
254
278
|
const { successful, returnMessage, status } = await platformModule.authValidation({ user: existingUser });
|
|
255
279
|
return {
|
package/handlers/log.js
CHANGED
|
@@ -184,7 +184,7 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
)
|
|
187
|
-
const syncedPluginJwtToken = syncPluginTokenResponse
|
|
187
|
+
const syncedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: syncPluginTokenResponse.headers });
|
|
188
188
|
axios.post(pluginEndpointUrl, {
|
|
189
189
|
data: incomingData,
|
|
190
190
|
config: userConfig,
|
|
@@ -304,6 +304,12 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
|
|
|
304
304
|
pluginAsyncTaskIds
|
|
305
305
|
};
|
|
306
306
|
}
|
|
307
|
+
else {
|
|
308
|
+
return {
|
|
309
|
+
successful: false,
|
|
310
|
+
returnMessage
|
|
311
|
+
};
|
|
312
|
+
}
|
|
307
313
|
} catch (e) {
|
|
308
314
|
return handleApiError(e, platform, 'createCallLog', { userId });
|
|
309
315
|
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const { AccountDataModel } = require('../models/accountDataModel');
|
|
2
|
+
const { CacheModel } = require('../models/cacheModel');
|
|
3
|
+
const { encode, decoded } = require('../lib/encode');
|
|
4
|
+
|
|
5
|
+
const MANAGED_OAUTH_ACCOUNT_DATA_KEY = 'managed-oauth-account';
|
|
6
|
+
const MANAGED_OAUTH_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
const MANAGED_OAUTH_FIELDS = [
|
|
8
|
+
'clientId',
|
|
9
|
+
'clientSecret',
|
|
10
|
+
'accessTokenUri',
|
|
11
|
+
'authorizationUri',
|
|
12
|
+
'redirectUri',
|
|
13
|
+
'scopes',
|
|
14
|
+
'hostname'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function getPendingManagedOAuthCacheId({ rcAccountId }) {
|
|
18
|
+
return `${rcAccountId}-${MANAGED_OAUTH_ACCOUNT_DATA_KEY}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isFilled(value) {
|
|
22
|
+
return value !== undefined && value !== null && value !== '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function encryptStoredValue(value) {
|
|
26
|
+
return {
|
|
27
|
+
version: 1,
|
|
28
|
+
encrypted: true,
|
|
29
|
+
value: encode(JSON.stringify(value))
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function decryptStoredValue(value) {
|
|
34
|
+
if (!value) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (value?.encrypted && value?.value) {
|
|
38
|
+
return JSON.parse(decoded(value.value));
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeManagedOAuthValues(values = {}) {
|
|
44
|
+
const normalized = {};
|
|
45
|
+
MANAGED_OAUTH_FIELDS.forEach(field => {
|
|
46
|
+
if (isFilled(values[field])) {
|
|
47
|
+
normalized[field] = values[field];
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildStoredData(values = {}) {
|
|
54
|
+
const normalized = normalizeManagedOAuthValues(values);
|
|
55
|
+
const fields = {};
|
|
56
|
+
Object.keys(normalized).forEach(key => {
|
|
57
|
+
fields[key] = encryptStoredValue(normalized[key]);
|
|
58
|
+
});
|
|
59
|
+
return { fields };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function decryptStoredData(data = {}) {
|
|
63
|
+
const fields = data?.fields ?? {};
|
|
64
|
+
const values = {};
|
|
65
|
+
Object.keys(fields).forEach(key => {
|
|
66
|
+
values[key] = decryptStoredValue(fields[key]);
|
|
67
|
+
});
|
|
68
|
+
return values;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function removeSecret(values = {}) {
|
|
72
|
+
const sanitized = { ...values };
|
|
73
|
+
delete sanitized.clientSecret;
|
|
74
|
+
return sanitized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getAccountManagedOAuthRecord({ rcAccountId, platform }) {
|
|
78
|
+
if (!rcAccountId || !platform) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return AccountDataModel.findOne({
|
|
82
|
+
where: {
|
|
83
|
+
rcAccountId,
|
|
84
|
+
platformName: platform,
|
|
85
|
+
dataKey: MANAGED_OAUTH_ACCOUNT_DATA_KEY
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function getPendingManagedOAuthRecord({ rcAccountId }) {
|
|
91
|
+
if (!rcAccountId) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const record = await CacheModel.findByPk(getPendingManagedOAuthCacheId({ rcAccountId }));
|
|
95
|
+
if (!record) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (record.expiry && new Date(record.expiry).getTime() <= Date.now()) {
|
|
99
|
+
await record.destroy();
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return record;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function getAccountManagedOAuthValues({ rcAccountId, platform }) {
|
|
106
|
+
const record = await getAccountManagedOAuthRecord({ rcAccountId, platform });
|
|
107
|
+
return decryptStoredData(record?.data);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function getPendingManagedOAuthValues({ rcAccountId }) {
|
|
111
|
+
const record = await getPendingManagedOAuthRecord({ rcAccountId });
|
|
112
|
+
return decryptStoredData(record?.data);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function getManagedOAuthState({ rcAccountId, platform, isAdmin }) {
|
|
116
|
+
const accountValues = await getAccountManagedOAuthValues({ rcAccountId, platform });
|
|
117
|
+
const hasAccountOAuth = Object.keys(accountValues).length > 0;
|
|
118
|
+
const pendingValues = isAdmin && !hasAccountOAuth
|
|
119
|
+
? await getPendingManagedOAuthValues({ rcAccountId })
|
|
120
|
+
: {};
|
|
121
|
+
const hasPendingOAuth = Object.keys(pendingValues).length > 0;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
isAdmin: !!isAdmin,
|
|
125
|
+
hasAccountOAuth,
|
|
126
|
+
hasPendingOAuth,
|
|
127
|
+
...(hasAccountOAuth ? { oauthValues: removeSecret(accountValues) } : {}),
|
|
128
|
+
...(isAdmin && hasPendingOAuth ? { pendingValues } : {})
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function upsertPendingManagedOAuth({ rcAccountId, values = {} }) {
|
|
133
|
+
if (!rcAccountId) {
|
|
134
|
+
throw new Error('rcAccountId is required');
|
|
135
|
+
}
|
|
136
|
+
const id = getPendingManagedOAuthCacheId({ rcAccountId });
|
|
137
|
+
const data = buildStoredData(values);
|
|
138
|
+
const expiry = new Date(Date.now() + MANAGED_OAUTH_CACHE_TTL_MS);
|
|
139
|
+
const existing = await CacheModel.findByPk(id);
|
|
140
|
+
if (existing) {
|
|
141
|
+
await existing.update({
|
|
142
|
+
status: 'pending',
|
|
143
|
+
userId: rcAccountId,
|
|
144
|
+
cacheKey: MANAGED_OAUTH_ACCOUNT_DATA_KEY,
|
|
145
|
+
data,
|
|
146
|
+
expiry
|
|
147
|
+
});
|
|
148
|
+
return existing;
|
|
149
|
+
}
|
|
150
|
+
return CacheModel.create({
|
|
151
|
+
id,
|
|
152
|
+
status: 'pending',
|
|
153
|
+
userId: rcAccountId,
|
|
154
|
+
cacheKey: MANAGED_OAUTH_ACCOUNT_DATA_KEY,
|
|
155
|
+
data,
|
|
156
|
+
expiry
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function migratePendingManagedOAuth({ rcAccountId, platform }) {
|
|
161
|
+
const pending = await getPendingManagedOAuthRecord({ rcAccountId });
|
|
162
|
+
if (!pending) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const existing = await getAccountManagedOAuthRecord({ rcAccountId, platform });
|
|
166
|
+
const data = pending.data ?? { fields: {} };
|
|
167
|
+
if (existing) {
|
|
168
|
+
await existing.update({ data });
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
await AccountDataModel.create({
|
|
172
|
+
rcAccountId,
|
|
173
|
+
platformName: platform,
|
|
174
|
+
dataKey: MANAGED_OAUTH_ACCOUNT_DATA_KEY,
|
|
175
|
+
data
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
await pending.destroy();
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function resolveManagedOAuthInfo({ rcAccountId, platform }) {
|
|
183
|
+
const accountValues = await getAccountManagedOAuthValues({ rcAccountId, platform });
|
|
184
|
+
if (Object.keys(accountValues).length > 0) {
|
|
185
|
+
return {
|
|
186
|
+
source: 'account',
|
|
187
|
+
oauthInfo: accountValues
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const pendingValues = await getPendingManagedOAuthValues({ rcAccountId });
|
|
191
|
+
if (Object.keys(pendingValues).length > 0) {
|
|
192
|
+
return {
|
|
193
|
+
source: 'pending',
|
|
194
|
+
oauthInfo: pendingValues
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
source: null,
|
|
199
|
+
oauthInfo: null
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function clearPendingManagedOAuth({ rcAccountId }) {
|
|
204
|
+
if (!rcAccountId) {
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
return CacheModel.destroy({
|
|
208
|
+
where: {
|
|
209
|
+
id: getPendingManagedOAuthCacheId({ rcAccountId })
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function clearAccountManagedOAuth({ rcAccountId, platform }) {
|
|
215
|
+
if (!rcAccountId || !platform) {
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
return AccountDataModel.destroy({
|
|
219
|
+
where: {
|
|
220
|
+
rcAccountId,
|
|
221
|
+
platformName: platform,
|
|
222
|
+
dataKey: MANAGED_OAUTH_ACCOUNT_DATA_KEY
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function resetManagedOAuth({ rcAccountId, platform }) {
|
|
228
|
+
const deletedAccountCount = await clearAccountManagedOAuth({ rcAccountId, platform });
|
|
229
|
+
const deletedPendingCount = await clearPendingManagedOAuth({ rcAccountId });
|
|
230
|
+
return {
|
|
231
|
+
deletedAccountCount,
|
|
232
|
+
deletedPendingCount
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
exports.MANAGED_OAUTH_ACCOUNT_DATA_KEY = MANAGED_OAUTH_ACCOUNT_DATA_KEY;
|
|
237
|
+
exports.MANAGED_OAUTH_FIELDS = MANAGED_OAUTH_FIELDS;
|
|
238
|
+
exports.getManagedOAuthState = getManagedOAuthState;
|
|
239
|
+
exports.upsertPendingManagedOAuth = upsertPendingManagedOAuth;
|
|
240
|
+
exports.getPendingManagedOAuthValues = getPendingManagedOAuthValues;
|
|
241
|
+
exports.getAccountManagedOAuthValues = getAccountManagedOAuthValues;
|
|
242
|
+
exports.migratePendingManagedOAuth = migratePendingManagedOAuth;
|
|
243
|
+
exports.resolveManagedOAuthInfo = resolveManagedOAuthInfo;
|
|
244
|
+
exports.clearPendingManagedOAuth = clearPendingManagedOAuth;
|
|
245
|
+
exports.clearAccountManagedOAuth = clearAccountManagedOAuth;
|
|
246
|
+
exports.resetManagedOAuth = resetManagedOAuth;
|
|
247
|
+
exports.removeSecret = removeSecret;
|
package/index.js
CHANGED
|
@@ -41,6 +41,7 @@ const {
|
|
|
41
41
|
sqliteCallLogsPkIncludesExtension,
|
|
42
42
|
} = require('./lib/migrateCallLogsSchema');
|
|
43
43
|
const managedAuthCore = require('./handlers/managedAuth');
|
|
44
|
+
const managedOAuthCore = require('./handlers/managedOAuth');
|
|
44
45
|
|
|
45
46
|
let packageJson = null;
|
|
46
47
|
try {
|
|
@@ -80,83 +81,6 @@ async function initDB() {
|
|
|
80
81
|
await CacheModel.sync();
|
|
81
82
|
await CallDownListModel.sync();
|
|
82
83
|
await AccountDataModel.sync();
|
|
83
|
-
|
|
84
|
-
const queryInterface = CallLogModel.sequelize.getQueryInterface();
|
|
85
|
-
const sequelizeInstance = CallLogModel.sequelize;
|
|
86
|
-
const dialect = sequelizeInstance.getDialect();
|
|
87
|
-
const callLogsColumns = await queryInterface.describeTable('callLogs');
|
|
88
|
-
const hasExtensionNumber = Object.keys(callLogsColumns).some(
|
|
89
|
-
(col) => col.toLowerCase() === 'extensionnumber',
|
|
90
|
-
);
|
|
91
|
-
// Sequelize's SQLite addConstraint appends PRIMARY KEY instead of replacing it.
|
|
92
|
-
const needsSqliteCallLogsRebuild =
|
|
93
|
-
dialect === 'sqlite' &&
|
|
94
|
-
(!hasExtensionNumber || !(await sqliteCallLogsPkIncludesExtension(sequelizeInstance)));
|
|
95
|
-
|
|
96
|
-
if (needsSqliteCallLogsRebuild) {
|
|
97
|
-
const transaction = await sequelizeInstance.transaction();
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
await migrateCallLogsExtensionNumberSqlite(sequelizeInstance, transaction);
|
|
101
|
-
await transaction.commit();
|
|
102
|
-
} catch (err) {
|
|
103
|
-
await transaction.rollback();
|
|
104
|
-
throw err;
|
|
105
|
-
}
|
|
106
|
-
} else if (!hasExtensionNumber) {
|
|
107
|
-
// sync() fills new Postgres DBs; legacy installs need alter + new PK constraint.
|
|
108
|
-
const transaction = await sequelizeInstance.transaction();
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
await queryInterface.addColumn('callLogs', 'extensionNumber', {
|
|
112
|
-
type: Sequelize.STRING,
|
|
113
|
-
defaultValue: '',
|
|
114
|
-
allowNull: false,
|
|
115
|
-
}, { transaction });
|
|
116
|
-
|
|
117
|
-
// Postgres already has a PK on (id, sessionId); addConstraint would try to add a second PK and fail.
|
|
118
|
-
if (dialect === 'postgres') {
|
|
119
|
-
const relname = CallLogModel.tableName;
|
|
120
|
-
const pkRows = await sequelizeInstance.query(
|
|
121
|
-
`
|
|
122
|
-
SELECT c.conname AS name
|
|
123
|
-
FROM pg_constraint c
|
|
124
|
-
INNER JOIN pg_class rel ON c.conrelid = rel.oid
|
|
125
|
-
INNER JOIN pg_namespace nsp ON rel.relnamespace = nsp.oid
|
|
126
|
-
WHERE c.contype = 'p'
|
|
127
|
-
AND nsp.nspname = current_schema()
|
|
128
|
-
AND rel.relname = :relname
|
|
129
|
-
`,
|
|
130
|
-
{
|
|
131
|
-
replacements: { relname },
|
|
132
|
-
transaction,
|
|
133
|
-
type: Sequelize.QueryTypes.SELECT,
|
|
134
|
-
},
|
|
135
|
-
);
|
|
136
|
-
const pkName = pkRows[0]?.name;
|
|
137
|
-
if (pkName) {
|
|
138
|
-
const qTable = queryInterface.queryGenerator.quoteTable(CallLogModel.tableName);
|
|
139
|
-
const qName = queryInterface.queryGenerator.quoteIdentifier(pkName);
|
|
140
|
-
await sequelizeInstance.query(
|
|
141
|
-
`ALTER TABLE ${qTable} DROP CONSTRAINT ${qName}`,
|
|
142
|
-
{ transaction },
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
await queryInterface.addConstraint('callLogs', {
|
|
148
|
-
fields: ['id', 'sessionId', 'extensionNumber'],
|
|
149
|
-
type: 'primary key',
|
|
150
|
-
name: 'callLogs_pkey',
|
|
151
|
-
transaction,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
await transaction.commit();
|
|
155
|
-
} catch (err) {
|
|
156
|
-
await transaction.rollback();
|
|
157
|
-
throw err;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
84
|
}
|
|
161
85
|
}
|
|
162
86
|
|
|
@@ -518,6 +442,35 @@ function createCoreRouter() {
|
|
|
518
442
|
res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
|
|
519
443
|
}
|
|
520
444
|
});
|
|
445
|
+
router.get('/oauthManagedAuthState', async function (req, res) {
|
|
446
|
+
const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
|
|
447
|
+
tracer?.trace('oauthManagedAuthState:start', { query: req.query });
|
|
448
|
+
try {
|
|
449
|
+
const platform = req.query.platform;
|
|
450
|
+
const rcAccessToken = req.query.rcAccessToken;
|
|
451
|
+
if (!platform) {
|
|
452
|
+
res.status(400).send(tracer ? tracer.wrapResponse('Missing platform name') : 'Missing platform name');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (!rcAccessToken) {
|
|
456
|
+
res.status(400).send(tracer ? tracer.wrapResponse('Missing RingCentral access token') : 'Missing RingCentral access token');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const { rcAccountId } = await adminCore.validateRcUserToken({ rcAccessToken });
|
|
460
|
+
const adminValidation = await adminCore.validateAdminRole({ rcAccessToken });
|
|
461
|
+
const state = await managedOAuthCore.getManagedOAuthState({
|
|
462
|
+
platform,
|
|
463
|
+
rcAccountId,
|
|
464
|
+
isAdmin: !!adminValidation.isValidated
|
|
465
|
+
});
|
|
466
|
+
res.status(200).send(tracer ? tracer.wrapResponse(state) : state);
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
logger.error('Get managed OAuth state failed', { stack: e.stack });
|
|
470
|
+
tracer?.traceError('oauthManagedAuthState:error', e);
|
|
471
|
+
res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
|
|
472
|
+
}
|
|
473
|
+
});
|
|
521
474
|
// Obsolete
|
|
522
475
|
router.get('/serverVersionInfo', (req, res) => {
|
|
523
476
|
const defaultCrmManifest = connectorRegistry.getManifest('default');
|
|
@@ -711,6 +664,75 @@ function createCoreRouter() {
|
|
|
711
664
|
res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
|
|
712
665
|
}
|
|
713
666
|
});
|
|
667
|
+
router.post('/admin/managedOAuth/cache', async function (req, res) {
|
|
668
|
+
const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
|
|
669
|
+
tracer?.trace('setAdminManagedOAuthCache:start', {
|
|
670
|
+
body: {
|
|
671
|
+
hasValues: !!req.body?.values,
|
|
672
|
+
valueKeys: Object.keys(req.body?.values ?? {}).filter(key => key !== 'clientSecret')
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
try {
|
|
676
|
+
const { isValidated, rcAccountId } = await adminCore.validateAdminRole({ rcAccessToken: req.query.rcAccessToken });
|
|
677
|
+
if (!isValidated) {
|
|
678
|
+
res.status(403).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
await managedOAuthCore.upsertPendingManagedOAuth({
|
|
682
|
+
rcAccountId: rcAccountId?.toString(),
|
|
683
|
+
values: req.body?.values ?? {}
|
|
684
|
+
});
|
|
685
|
+
res.status(200).send(tracer ? tracer.wrapResponse({ successful: true }) : { successful: true });
|
|
686
|
+
}
|
|
687
|
+
catch (e) {
|
|
688
|
+
logger.error('Set managed OAuth pending cache failed', { stack: e.stack });
|
|
689
|
+
tracer?.traceError('setAdminManagedOAuthCache:error', e);
|
|
690
|
+
res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
router.delete('/admin/managedOAuth/cache', async function (req, res) {
|
|
694
|
+
const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
|
|
695
|
+
tracer?.trace('deleteAdminManagedOAuthCache:start', {});
|
|
696
|
+
try {
|
|
697
|
+
const { isValidated, rcAccountId } = await adminCore.validateAdminRole({ rcAccessToken: req.query.rcAccessToken });
|
|
698
|
+
if (!isValidated) {
|
|
699
|
+
res.status(403).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
await managedOAuthCore.clearPendingManagedOAuth({ rcAccountId: rcAccountId?.toString() });
|
|
703
|
+
res.status(200).send(tracer ? tracer.wrapResponse({ successful: true }) : { successful: true });
|
|
704
|
+
}
|
|
705
|
+
catch (e) {
|
|
706
|
+
logger.error('Delete managed OAuth pending cache failed', { stack: e.stack });
|
|
707
|
+
tracer?.traceError('deleteAdminManagedOAuthCache:error', e);
|
|
708
|
+
res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
router.delete('/admin/managedOAuth/account', async function (req, res) {
|
|
712
|
+
const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
|
|
713
|
+
tracer?.trace('deleteAdminManagedOAuthAccount:start', { query: req.query });
|
|
714
|
+
try {
|
|
715
|
+
const { isValidated, rcAccountId } = await adminCore.validateAdminRole({ rcAccessToken: req.query.rcAccessToken });
|
|
716
|
+
if (!isValidated) {
|
|
717
|
+
res.status(403).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (!req.query.platform) {
|
|
721
|
+
res.status(400).send(tracer ? tracer.wrapResponse('Missing platform name') : 'Missing platform name');
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
await managedOAuthCore.resetManagedOAuth({
|
|
725
|
+
rcAccountId: rcAccountId?.toString(),
|
|
726
|
+
platform: req.query.platform
|
|
727
|
+
});
|
|
728
|
+
res.status(200).send(tracer ? tracer.wrapResponse({ successful: true }) : { successful: true });
|
|
729
|
+
}
|
|
730
|
+
catch (e) {
|
|
731
|
+
logger.error('Delete managed OAuth account failed', { stack: e.stack });
|
|
732
|
+
tracer?.traceError('deleteAdminManagedOAuthAccount:error', e);
|
|
733
|
+
res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
|
|
734
|
+
}
|
|
735
|
+
});
|
|
714
736
|
router.post('/admin/userMapping', async function (req, res) {
|
|
715
737
|
const requestStartTime = new Date().getTime();
|
|
716
738
|
const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
|
package/mcp/tools/logout.js
CHANGED
|
@@ -71,7 +71,10 @@ async function execute(args) {
|
|
|
71
71
|
}
|
|
72
72
|
const platformModule = connectorRegistry.getConnector(platform);
|
|
73
73
|
try {
|
|
74
|
-
await platformModule.unAuthorize({ user: userToLogout });
|
|
74
|
+
const logoutResult = await platformModule.unAuthorize({ user: userToLogout });
|
|
75
|
+
if (!logoutResult.successful) {
|
|
76
|
+
throw new Error(logoutResult.returnMessage.message);
|
|
77
|
+
}
|
|
75
78
|
}
|
|
76
79
|
catch (error) {
|
|
77
80
|
console.log(error);
|
package/package.json
CHANGED
package/releaseNotes.json
CHANGED
|
@@ -23,6 +23,7 @@ const { Connector } = require('../../models/dynamo/connectorSchema');
|
|
|
23
23
|
const { RingCentral } = require('../../lib/ringcentral');
|
|
24
24
|
const adminCore = require('../../handlers/admin');
|
|
25
25
|
const { AccountDataModel } = require('../../models/accountDataModel');
|
|
26
|
+
const { CacheModel } = require('../../models/cacheModel');
|
|
26
27
|
const { encode } = require('../../lib/encode');
|
|
27
28
|
const { getHashValue } = require('../../lib/util');
|
|
28
29
|
|
|
@@ -664,6 +665,11 @@ describe('Auth Handler', () => {
|
|
|
664
665
|
oauth.getOAuthApp.mockReturnValue(mockOAuthApp);
|
|
665
666
|
});
|
|
666
667
|
|
|
668
|
+
afterEach(async () => {
|
|
669
|
+
await AccountDataModel.destroy({ where: {} });
|
|
670
|
+
await CacheModel.destroy({ where: {} });
|
|
671
|
+
});
|
|
672
|
+
|
|
667
673
|
test('should handle successful OAuth callback', async () => {
|
|
668
674
|
// Arrange
|
|
669
675
|
const mockUserInfo = {
|
|
@@ -892,6 +898,74 @@ describe('Auth Handler', () => {
|
|
|
892
898
|
overridingOption
|
|
893
899
|
);
|
|
894
900
|
});
|
|
901
|
+
|
|
902
|
+
test('should use pending managed OAuth info and promote it after successful callback', async () => {
|
|
903
|
+
await CacheModel.create({
|
|
904
|
+
id: 'rc-managed-managed-oauth-account',
|
|
905
|
+
status: 'pending',
|
|
906
|
+
userId: 'rc-managed',
|
|
907
|
+
cacheKey: 'managed-oauth-account',
|
|
908
|
+
expiry: new Date(Date.now() + 60000),
|
|
909
|
+
data: {
|
|
910
|
+
fields: {
|
|
911
|
+
clientId: { version: 1, encrypted: true, value: encode(JSON.stringify('managed-client-id')) },
|
|
912
|
+
clientSecret: { version: 1, encrypted: true, value: encode(JSON.stringify('managed-client-secret')) },
|
|
913
|
+
accessTokenUri: { version: 1, encrypted: true, value: encode(JSON.stringify('https://managed.example.com/token')) },
|
|
914
|
+
authorizationUri: { version: 1, encrypted: true, value: encode(JSON.stringify('https://managed.example.com/authorize')) },
|
|
915
|
+
redirectUri: { version: 1, encrypted: true, value: encode(JSON.stringify('https://ringcentral.github.io/ringcentral-embeddable/redirect.html')) },
|
|
916
|
+
hostname: { version: 1, encrypted: true, value: encode(JSON.stringify('managed.example.com')) }
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
const mockConnector = global.testUtils.createMockConnector({
|
|
922
|
+
getOauthInfo: jest.fn(),
|
|
923
|
+
getUserInfo: jest.fn().mockResolvedValue({
|
|
924
|
+
successful: true,
|
|
925
|
+
platformUserInfo: { id: 'managed-oauth-user', name: 'Managed OAuth User' },
|
|
926
|
+
returnMessage: { messageType: 'success', message: 'OK' }
|
|
927
|
+
})
|
|
928
|
+
});
|
|
929
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
930
|
+
mockOAuthApp.code.getToken.mockResolvedValue({
|
|
931
|
+
accessToken: 'token',
|
|
932
|
+
refreshToken: 'refresh',
|
|
933
|
+
expires: new Date()
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
const result = await authHandler.onOAuthCallback({
|
|
937
|
+
platform: 'testCRM',
|
|
938
|
+
hostname: 'old.example.com',
|
|
939
|
+
tokenUrl: '',
|
|
940
|
+
query: {
|
|
941
|
+
callbackUri: 'https://app.example.com/callback?code=code123',
|
|
942
|
+
rcAccountId: 'rc-managed'
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
expect(result.userInfo.id).toBe('managed-oauth-user');
|
|
947
|
+
expect(mockConnector.getOauthInfo).not.toHaveBeenCalled();
|
|
948
|
+
expect(oauth.getOAuthApp).toHaveBeenCalledWith(expect.objectContaining({
|
|
949
|
+
clientId: 'managed-client-id',
|
|
950
|
+
clientSecret: 'managed-client-secret',
|
|
951
|
+
accessTokenUri: 'https://managed.example.com/token',
|
|
952
|
+
authorizationUri: 'https://managed.example.com/authorize'
|
|
953
|
+
}));
|
|
954
|
+
expect(mockConnector.getUserInfo).toHaveBeenCalledWith(expect.objectContaining({
|
|
955
|
+
hostname: 'managed.example.com',
|
|
956
|
+
tokenUrl: 'https://managed.example.com/token'
|
|
957
|
+
}));
|
|
958
|
+
const promoted = await AccountDataModel.findOne({
|
|
959
|
+
where: {
|
|
960
|
+
rcAccountId: 'rc-managed',
|
|
961
|
+
platformName: 'testCRM',
|
|
962
|
+
dataKey: 'managed-oauth-account'
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
expect(promoted).not.toBeNull();
|
|
966
|
+
const pending = await CacheModel.findByPk('rc-managed-managed-oauth-account');
|
|
967
|
+
expect(pending).toBeNull();
|
|
968
|
+
});
|
|
895
969
|
});
|
|
896
970
|
|
|
897
971
|
describe('getLicenseStatus', () => {
|
|
@@ -8,6 +8,12 @@ jest.mock('../../handlers/admin', () => ({
|
|
|
8
8
|
jest.mock('../../handlers/managedAuth', () => ({
|
|
9
9
|
getManagedAuthState: jest.fn(),
|
|
10
10
|
}));
|
|
11
|
+
jest.mock('../../handlers/managedOAuth', () => ({
|
|
12
|
+
getManagedOAuthState: jest.fn(),
|
|
13
|
+
upsertPendingManagedOAuth: jest.fn(),
|
|
14
|
+
clearPendingManagedOAuth: jest.fn(),
|
|
15
|
+
resetManagedOAuth: jest.fn(),
|
|
16
|
+
}));
|
|
11
17
|
jest.mock('../../handlers/auth', () => ({
|
|
12
18
|
onApiKeyLogin: jest.fn(),
|
|
13
19
|
}));
|
|
@@ -18,6 +24,7 @@ jest.mock('../../lib/jwt', () => ({
|
|
|
18
24
|
|
|
19
25
|
const adminCore = require('../../handlers/admin');
|
|
20
26
|
const managedAuthCore = require('../../handlers/managedAuth');
|
|
27
|
+
const managedOAuthCore = require('../../handlers/managedOAuth');
|
|
21
28
|
const authCore = require('../../handlers/auth');
|
|
22
29
|
const { createCoreRouter } = require('../../index');
|
|
23
30
|
|
|
@@ -63,6 +70,130 @@ describe('Managed Auth Routes', () => {
|
|
|
63
70
|
});
|
|
64
71
|
});
|
|
65
72
|
|
|
73
|
+
describe('GET /oauthManagedAuthState', () => {
|
|
74
|
+
test('should validate rcAccessToken and return managed OAuth state for validated account', async () => {
|
|
75
|
+
adminCore.validateRcUserToken.mockResolvedValue({
|
|
76
|
+
rcAccountId: 'validated-account-id',
|
|
77
|
+
rcExtensionId: 'validated-extension-id',
|
|
78
|
+
});
|
|
79
|
+
adminCore.validateAdminRole.mockResolvedValue({
|
|
80
|
+
isValidated: true,
|
|
81
|
+
rcAccountId: 'validated-account-id',
|
|
82
|
+
});
|
|
83
|
+
managedOAuthCore.getManagedOAuthState.mockResolvedValue({
|
|
84
|
+
isAdmin: true,
|
|
85
|
+
hasAccountOAuth: false,
|
|
86
|
+
hasPendingOAuth: true,
|
|
87
|
+
pendingValues: {
|
|
88
|
+
clientId: 'client-id',
|
|
89
|
+
clientSecret: 'client-secret',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const response = await request(app)
|
|
94
|
+
.get('/oauthManagedAuthState')
|
|
95
|
+
.query({
|
|
96
|
+
platform: 'testCRM',
|
|
97
|
+
rcAccessToken: 'valid-rc-token',
|
|
98
|
+
rcAccountId: 'spoofed-account-id',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(response.status).toBe(200);
|
|
102
|
+
expect(adminCore.validateRcUserToken).toHaveBeenCalledWith({ rcAccessToken: 'valid-rc-token' });
|
|
103
|
+
expect(adminCore.validateAdminRole).toHaveBeenCalledWith({ rcAccessToken: 'valid-rc-token' });
|
|
104
|
+
expect(managedOAuthCore.getManagedOAuthState).toHaveBeenCalledWith({
|
|
105
|
+
platform: 'testCRM',
|
|
106
|
+
rcAccountId: 'validated-account-id',
|
|
107
|
+
isAdmin: true,
|
|
108
|
+
});
|
|
109
|
+
expect(response.body.pendingValues.clientSecret).toBe('client-secret');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('POST /admin/managedOAuth/cache', () => {
|
|
114
|
+
test('should require admin role and write pending managed OAuth values for validated account', async () => {
|
|
115
|
+
adminCore.validateAdminRole.mockResolvedValue({
|
|
116
|
+
isValidated: true,
|
|
117
|
+
rcAccountId: 'validated-account-id',
|
|
118
|
+
});
|
|
119
|
+
managedOAuthCore.upsertPendingManagedOAuth.mockResolvedValue({});
|
|
120
|
+
|
|
121
|
+
const values = {
|
|
122
|
+
clientId: 'client-id',
|
|
123
|
+
clientSecret: 'client-secret',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const response = await request(app)
|
|
127
|
+
.post('/admin/managedOAuth/cache')
|
|
128
|
+
.query({ rcAccessToken: 'valid-rc-token' })
|
|
129
|
+
.send({ values });
|
|
130
|
+
|
|
131
|
+
expect(response.status).toBe(200);
|
|
132
|
+
expect(managedOAuthCore.upsertPendingManagedOAuth).toHaveBeenCalledWith({
|
|
133
|
+
rcAccountId: 'validated-account-id',
|
|
134
|
+
values,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should reject non-admin users', async () => {
|
|
139
|
+
adminCore.validateAdminRole.mockResolvedValue({
|
|
140
|
+
isValidated: false,
|
|
141
|
+
rcAccountId: 'validated-account-id',
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const response = await request(app)
|
|
145
|
+
.post('/admin/managedOAuth/cache')
|
|
146
|
+
.query({ rcAccessToken: 'valid-rc-token' })
|
|
147
|
+
.send({ values: { clientId: 'client-id' } });
|
|
148
|
+
|
|
149
|
+
expect(response.status).toBe(403);
|
|
150
|
+
expect(managedOAuthCore.upsertPendingManagedOAuth).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('DELETE /admin/managedOAuth/account', () => {
|
|
155
|
+
test('should require admin role and delete managed OAuth account for validated account', async () => {
|
|
156
|
+
adminCore.validateAdminRole.mockResolvedValue({
|
|
157
|
+
isValidated: true,
|
|
158
|
+
rcAccountId: 'validated-account-id',
|
|
159
|
+
});
|
|
160
|
+
managedOAuthCore.resetManagedOAuth.mockResolvedValue({
|
|
161
|
+
deletedAccountCount: 1,
|
|
162
|
+
deletedPendingCount: 1,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const response = await request(app)
|
|
166
|
+
.delete('/admin/managedOAuth/account')
|
|
167
|
+
.query({
|
|
168
|
+
rcAccessToken: 'valid-rc-token',
|
|
169
|
+
platform: 'testCRM',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(response.status).toBe(200);
|
|
173
|
+
expect(managedOAuthCore.resetManagedOAuth).toHaveBeenCalledWith({
|
|
174
|
+
rcAccountId: 'validated-account-id',
|
|
175
|
+
platform: 'testCRM',
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('should reject non-admin users', async () => {
|
|
180
|
+
adminCore.validateAdminRole.mockResolvedValue({
|
|
181
|
+
isValidated: false,
|
|
182
|
+
rcAccountId: 'validated-account-id',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const response = await request(app)
|
|
186
|
+
.delete('/admin/managedOAuth/account')
|
|
187
|
+
.query({
|
|
188
|
+
rcAccessToken: 'valid-rc-token',
|
|
189
|
+
platform: 'testCRM',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(response.status).toBe(403);
|
|
193
|
+
expect(managedOAuthCore.resetManagedOAuth).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
66
197
|
describe('POST /apiKeyLogin', () => {
|
|
67
198
|
|
|
68
199
|
test('should validate rcAccessToken and ignore spoofed rc ids in body', async () => {
|