@app-connect/core 1.7.33 → 1.7.35
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 +1 -1
- package/handlers/log.js +2 -0
- package/index.js +13 -8
- package/package.json +1 -1
- package/releaseNotes.json +48 -0
- package/test/handlers/auth.test.js +6 -2
- package/test/handlers/log.test.js +3 -2
- package/test/index.test.js +64 -1
package/handlers/auth.js
CHANGED
|
@@ -50,7 +50,7 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcEx
|
|
|
50
50
|
let overridingOAuthOption = null;
|
|
51
51
|
if (platformModule.getOverridingOAuthOption != null) {
|
|
52
52
|
const code = new URL(callbackUri).searchParams.get('code');
|
|
53
|
-
overridingOAuthOption = platformModule.getOverridingOAuthOption({ code });
|
|
53
|
+
overridingOAuthOption = platformModule.getOverridingOAuthOption({ code, oauthInfo });
|
|
54
54
|
}
|
|
55
55
|
const oauthApp = oauth.getOAuthApp(oauthInfo);
|
|
56
56
|
const { accessToken, refreshToken, expires, data } = await oauthApp.code.getToken(callbackUri, overridingOAuthOption);
|
package/handlers/log.js
CHANGED
|
@@ -103,6 +103,7 @@ async function runSyncCallPlugins({ syncCallPlugins, incomingData, user, platfor
|
|
|
103
103
|
const processedResultResponse = await axios.post(pluginEndpointUrl, {
|
|
104
104
|
data: processedIncomingData,
|
|
105
105
|
config: userConfig,
|
|
106
|
+
hashedExtensionId: user.hashedRcExtensionId,
|
|
106
107
|
}, {
|
|
107
108
|
headers: {
|
|
108
109
|
Authorization: `Bearer ${pluginJwtToken}`,
|
|
@@ -182,6 +183,7 @@ async function dispatchAsyncCallPlugin({ plugin, incomingData, user, platform, o
|
|
|
182
183
|
config: pluginCore.getPluginConfigFromUserSettings({ userSettings: user.userSettings, pluginId }),
|
|
183
184
|
asyncTaskId: taskId,
|
|
184
185
|
callbackUrl,
|
|
186
|
+
hashedExtensionId: user.hashedRcExtensionId,
|
|
185
187
|
}, {
|
|
186
188
|
headers: {
|
|
187
189
|
Authorization: `Bearer ${syncedPluginJwtToken ?? pluginJwtToken}`,
|
package/index.js
CHANGED
|
@@ -118,13 +118,12 @@ function normalizeJwtFromRequest(req, res, next) {
|
|
|
118
118
|
const originalBearerToken = getBearerTokenFromRequest(req);
|
|
119
119
|
const queryToken = req.query?.jwtToken;
|
|
120
120
|
let bearerToken = originalBearerToken;
|
|
121
|
+
const isQueryAuth = !originalBearerToken && !!queryToken;
|
|
121
122
|
|
|
122
123
|
// Backward compatibility: promote query jwtToken to Authorization Bearer.
|
|
123
|
-
if (
|
|
124
|
+
if (isQueryAuth) {
|
|
124
125
|
req.headers.authorization = `Bearer ${queryToken}`;
|
|
125
126
|
bearerToken = queryToken;
|
|
126
|
-
// Don't refresh JWT because old version cannot update its local token storage to support refreshed token.
|
|
127
|
-
return next();
|
|
128
127
|
}
|
|
129
128
|
|
|
130
129
|
const token = bearerToken;
|
|
@@ -136,7 +135,7 @@ function normalizeJwtFromRequest(req, res, next) {
|
|
|
136
135
|
const decodedToken = jwt.decodeJwt(token);
|
|
137
136
|
if (!decodedToken?.id) {
|
|
138
137
|
req.invalidJwtToken = true;
|
|
139
|
-
if (req.query?.jwtToken) {
|
|
138
|
+
if (originalBearerToken && req.query?.jwtToken) {
|
|
140
139
|
delete req.query.jwtToken;
|
|
141
140
|
}
|
|
142
141
|
return next();
|
|
@@ -148,16 +147,17 @@ function normalizeJwtFromRequest(req, res, next) {
|
|
|
148
147
|
if (typeof decodedToken.exp === 'number') {
|
|
149
148
|
const now = Math.floor(Date.now() / 1000);
|
|
150
149
|
const timeLeft = decodedToken.exp - now;
|
|
151
|
-
const isBearerAuth = !!originalBearerToken;
|
|
152
150
|
const shouldRefreshNearExpiry = timeLeft <= JWT_REFRESH_THRESHOLD_SECONDS;
|
|
153
|
-
|
|
154
|
-
const shouldRefreshLegacyLongLivedBearer = isBearerAuth && timeLeft > JWT_LEGACY_LONG_LIVED_THRESHOLD_SECONDS;
|
|
155
|
-
if (shouldRefreshNearExpiry || shouldRefreshLegacyLongLivedBearer) {
|
|
151
|
+
if (shouldRefreshNearExpiry) {
|
|
156
152
|
const refreshedToken = jwt.generateJwt({
|
|
157
153
|
id: decodedToken.id.toString(),
|
|
158
154
|
platform: decodedToken.platform
|
|
159
155
|
});
|
|
160
156
|
res.setHeader('x-refreshed-jwt-token', refreshedToken);
|
|
157
|
+
req.headers.authorization = `Bearer ${refreshedToken}`;
|
|
158
|
+
if (isQueryAuth && req.query?.jwtToken) {
|
|
159
|
+
req.query.jwtToken = refreshedToken;
|
|
160
|
+
}
|
|
161
161
|
req.jwtToken = refreshedToken;
|
|
162
162
|
req.jwtAuth = jwt.decodeJwt(refreshedToken) || decodedToken;
|
|
163
163
|
}
|
|
@@ -1040,6 +1040,11 @@ function createCoreRouter() {
|
|
|
1040
1040
|
if (jwtToken) {
|
|
1041
1041
|
const unAuthData = jwt.decodeJwt(jwtToken);
|
|
1042
1042
|
platformName = unAuthData?.platform ?? 'Unknown';
|
|
1043
|
+
if(!unAuthData || !unAuthData?.id) {
|
|
1044
|
+
tracer?.trace('getUserSettings:noToken', {});
|
|
1045
|
+
res.status(400).send(tracer ? tracer.wrapResponse('Please go to Settings and authorize CRM platform') : 'Please go to Settings and authorize CRM platform');
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1043
1048
|
const user = await UserModel.findByPk(unAuthData?.id);
|
|
1044
1049
|
if (!user) {
|
|
1045
1050
|
tracer?.trace('getUserSettings:userNotFound', {});
|
package/package.json
CHANGED
package/releaseNotes.json
CHANGED
|
@@ -1,4 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.7.35": {
|
|
3
|
+
"global": [
|
|
4
|
+
{
|
|
5
|
+
"type": "New",
|
|
6
|
+
"description": "Can show RingCentral Account Id on support page"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "New",
|
|
10
|
+
"description": "Add hashedExtensionId in plugin calls"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "New",
|
|
14
|
+
"description": "Show notification when having incoming call while minized"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "Fix",
|
|
18
|
+
"description": "User session refresh and validtion issue"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "Fix",
|
|
22
|
+
"description": "Warm-transfer call pop"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "Fix",
|
|
26
|
+
"description": "Refresh manifest for override properties"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"1.7.34": {
|
|
31
|
+
"global": [
|
|
32
|
+
{
|
|
33
|
+
"type": "New",
|
|
34
|
+
"description": "Call pop fallback url when no matched contact"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "Fix",
|
|
38
|
+
"description": "Hostname page input debounce"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "Fix",
|
|
42
|
+
"description": "RingCentral additional info submission during auth"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "Fix",
|
|
46
|
+
"description": "Managed auth, admins can now see hidden fields"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
2
50
|
"1.7.33": {
|
|
3
51
|
"global": [
|
|
4
52
|
{
|
|
@@ -863,8 +863,9 @@ describe('Auth Handler', () => {
|
|
|
863
863
|
test('should use overriding OAuth option if provided', async () => {
|
|
864
864
|
// Arrange
|
|
865
865
|
const overridingOption = { redirect_uri: 'custom-redirect' };
|
|
866
|
+
const oauthInfo = { clientId: 'id', clientSecret: 'secret' };
|
|
866
867
|
const mockConnector = global.testUtils.createMockConnector({
|
|
867
|
-
getOauthInfo: jest.fn().mockResolvedValue(
|
|
868
|
+
getOauthInfo: jest.fn().mockResolvedValue(oauthInfo),
|
|
868
869
|
getOverridingOAuthOption: jest.fn().mockReturnValue(overridingOption),
|
|
869
870
|
getUserInfo: jest.fn().mockResolvedValue({
|
|
870
871
|
successful: true,
|
|
@@ -892,7 +893,10 @@ describe('Auth Handler', () => {
|
|
|
892
893
|
await authHandler.onOAuthCallback(requestData);
|
|
893
894
|
|
|
894
895
|
// Assert
|
|
895
|
-
expect(mockConnector.getOverridingOAuthOption).toHaveBeenCalledWith({
|
|
896
|
+
expect(mockConnector.getOverridingOAuthOption).toHaveBeenCalledWith({
|
|
897
|
+
code: 'code123',
|
|
898
|
+
oauthInfo: { clientId: 'id', clientSecret: 'secret' }
|
|
899
|
+
});
|
|
896
900
|
expect(mockOAuthApp.code.getToken).toHaveBeenCalledWith(
|
|
897
901
|
expect.any(String),
|
|
898
902
|
overridingOption
|
|
@@ -324,7 +324,7 @@ describe('Log Handler', () => {
|
|
|
324
324
|
expect(result.successful).toBe(true);
|
|
325
325
|
expect(axios.post).toHaveBeenCalledWith(
|
|
326
326
|
'https://plugins.example.com/plugin/testPlugin',
|
|
327
|
-
{ data: mockIncomingData, config: null },
|
|
327
|
+
{ data: mockIncomingData, config: null, hashedExtensionId: null },
|
|
328
328
|
{
|
|
329
329
|
headers: {
|
|
330
330
|
Authorization: 'Bearer plugin-jwt-token'
|
|
@@ -402,7 +402,8 @@ describe('Log Handler', () => {
|
|
|
402
402
|
data: mockIncomingData,
|
|
403
403
|
config: null,
|
|
404
404
|
asyncTaskId: cache.id,
|
|
405
|
-
callbackUrl: `https://app.example.com/plugin/async-callback/${cache.id}
|
|
405
|
+
callbackUrl: `https://app.example.com/plugin/async-callback/${cache.id}`,
|
|
406
|
+
hashedExtensionId: null
|
|
406
407
|
});
|
|
407
408
|
expect(pluginCall[2]).toEqual({
|
|
408
409
|
headers: {
|
package/test/index.test.js
CHANGED
|
@@ -33,7 +33,7 @@ describe('Core Router JWT normalization', () => {
|
|
|
33
33
|
jest.clearAllMocks();
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
test('should accept query jwtToken without refreshing it', async () => {
|
|
36
|
+
test('should accept query jwtToken without exp without refreshing it', async () => {
|
|
37
37
|
jwt.decodeJwt.mockReturnValue({ id: 'user-1', platform: 'testCRM' });
|
|
38
38
|
authCore.authValidation.mockResolvedValue({
|
|
39
39
|
successful: true,
|
|
@@ -54,6 +54,69 @@ describe('Core Router JWT normalization', () => {
|
|
|
54
54
|
expect(jwt.generateJwt).not.toHaveBeenCalled();
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
test('should refresh near-expiry query jwtToken and expose header', async () => {
|
|
58
|
+
const nowMs = 1700000000000;
|
|
59
|
+
const nowSeconds = Math.floor(nowMs / 1000);
|
|
60
|
+
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(nowMs);
|
|
61
|
+
jwt.decodeJwt.mockImplementation((token) => {
|
|
62
|
+
if (token === 'query-token') {
|
|
63
|
+
return { id: 'user-1', platform: 'testCRM', exp: nowSeconds + 60 };
|
|
64
|
+
}
|
|
65
|
+
if (token === 'new-token') {
|
|
66
|
+
return { id: 'user-1', platform: 'testCRM', exp: nowSeconds + (14 * 24 * 60 * 60) };
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
});
|
|
70
|
+
jwt.generateJwt.mockReturnValue('new-token');
|
|
71
|
+
authCore.authValidation.mockResolvedValue({
|
|
72
|
+
successful: true,
|
|
73
|
+
returnMessage: { message: 'ok' },
|
|
74
|
+
failReason: null,
|
|
75
|
+
status: 200,
|
|
76
|
+
});
|
|
77
|
+
const app = buildApp();
|
|
78
|
+
|
|
79
|
+
const response = await request(app)
|
|
80
|
+
.get('/authValidation?jwtToken=query-token')
|
|
81
|
+
.set('Origin', 'https://example.com');
|
|
82
|
+
|
|
83
|
+
expect(response.status).toBe(200);
|
|
84
|
+
expect(response.headers['x-refreshed-jwt-token']).toBe('new-token');
|
|
85
|
+
expect(response.headers['access-control-expose-headers']).toContain('x-refreshed-jwt-token');
|
|
86
|
+
expect(jwt.generateJwt).toHaveBeenCalledWith({ id: 'user-1', platform: 'testCRM' });
|
|
87
|
+
expect(authCore.authValidation).toHaveBeenCalledWith({
|
|
88
|
+
platform: 'testCRM',
|
|
89
|
+
userId: 'user-1',
|
|
90
|
+
});
|
|
91
|
+
expect(jwt.decodeJwt).toHaveBeenCalledWith('new-token');
|
|
92
|
+
nowSpy.mockRestore();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should not rotate long-lived query jwtToken', async () => {
|
|
96
|
+
const nowMs = 1700000000000;
|
|
97
|
+
const nowSeconds = Math.floor(nowMs / 1000);
|
|
98
|
+
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(nowMs);
|
|
99
|
+
jwt.decodeJwt.mockReturnValue({
|
|
100
|
+
id: 'user-1',
|
|
101
|
+
platform: 'testCRM',
|
|
102
|
+
exp: nowSeconds + (120 * 365 * 24 * 60 * 60),
|
|
103
|
+
});
|
|
104
|
+
authCore.authValidation.mockResolvedValue({
|
|
105
|
+
successful: true,
|
|
106
|
+
returnMessage: { message: 'ok' },
|
|
107
|
+
failReason: null,
|
|
108
|
+
status: 200,
|
|
109
|
+
});
|
|
110
|
+
const app = buildApp();
|
|
111
|
+
|
|
112
|
+
const response = await request(app).get('/authValidation?jwtToken=query-token');
|
|
113
|
+
|
|
114
|
+
expect(response.status).toBe(200);
|
|
115
|
+
expect(response.headers['x-refreshed-jwt-token']).toBeUndefined();
|
|
116
|
+
expect(jwt.generateJwt).not.toHaveBeenCalled();
|
|
117
|
+
nowSpy.mockRestore();
|
|
118
|
+
});
|
|
119
|
+
|
|
57
120
|
test('should refresh near-expiry bearer token and expose header', async () => {
|
|
58
121
|
const nowMs = 1700000000000;
|
|
59
122
|
const nowSeconds = Math.floor(nowMs / 1000);
|