Skip to content

Commit db20d28

Browse files
feat(server): run credentials verifiacation on the connection import endpoint
1 parent c0091c6 commit db20d28

File tree

2 files changed

+90
-4
lines changed

2 files changed

+90
-4
lines changed

packages/server/lib/controllers/connection/postConnection.ts

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as z from 'zod';
22

33
import db from '@nangohq/database';
4-
import { logContextGetter } from '@nangohq/logs';
4+
import { defaultOperationExpiration, logContextGetter } from '@nangohq/logs';
55
import {
66
EndUserMapper,
77
buildTagsFromEndUser,
@@ -27,7 +27,8 @@ import {
2727
connectionTagsSchema,
2828
endUserSchema
2929
} from '../../helpers/validation.js';
30-
import { connectionCreated, connectionCreationStartCapCheck, connectionRefreshSuccess } from '../../hooks/hooks.js';
30+
import { validateConnection } from '../../hooks/connection/on/validate-connection.js';
31+
import { connectionCreated, connectionCreationStartCapCheck, connectionRefreshSuccess, testConnectionCredentials } from '../../hooks/hooks.js';
3132
import { asyncWrapper } from '../../utils/asyncWrapper.js';
3233

3334
import type { AuthOperationType, ConnectionConfig, ConnectionUpsertResponse, EndUser, PostPublicConnection, ProviderGithubApp } from '@nangohq/types';
@@ -141,6 +142,20 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
141142
return;
142143
}
143144

145+
const logCtx = await logContextGetter.create(
146+
{
147+
operation: { type: 'auth', action: 'create_connection' },
148+
meta: { authType: 'connection_api' },
149+
expiresAt: defaultOperationExpiration.auth()
150+
},
151+
{ account, environment }
152+
);
153+
await logCtx.enrichOperation({
154+
integrationId: integration.id!,
155+
integrationName: integration.unique_key,
156+
providerName
157+
});
158+
144159
let updatedConnection: ConnectionUpsertResponse | undefined;
145160

146161
const connCreatedHook = (res: ConnectionUpsertResponse) => {
@@ -187,13 +202,29 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
187202
}
188203
case 'API_KEY':
189204
case 'BASIC': {
205+
// the testconnection only works with API_KEY, BASIC and TBA from this list
206+
const connectionConfig = body.connection_config || {};
207+
const connectionResponse = await testConnectionCredentials({
208+
config: integration,
209+
connectionConfig,
210+
connectionId,
211+
credentials: body.credentials,
212+
provider,
213+
logCtx
214+
});
215+
if (connectionResponse.isErr()) {
216+
await logCtx.failed();
217+
res.status(400).send({ error: { code: 'connection_test_failed', message: connectionResponse.error.message } });
218+
return;
219+
}
220+
190221
const [imported] = await connectionService.importApiAuthConnection({
191222
connectionId,
192223
providerConfigKey: body.provider_config_key,
193224
metadata: body.metadata || {},
194225
environment,
195226
credentials: body.credentials,
196-
connectionConfig: body.connection_config || {},
227+
connectionConfig,
197228
connectionCreatedHook: connCreatedHook,
198229
tags: mergedTags
199230
});
@@ -215,6 +246,7 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
215246
connectionConfig
216247
});
217248
if (credentialsRes.isErr()) {
249+
await logCtx.failed();
218250
res.status(500).send({ error: { code: 'server_error', message: credentialsRes.error.message } });
219251
return;
220252
}
@@ -248,7 +280,8 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
248280
connectionConfig
249281
});
250282
if (credentialsRes.isErr()) {
251-
res.status(500).send({ error: { code: 'server_error', message: credentialsRes.error.message } });
283+
await logCtx.failed();
284+
res.status(400).send({ error: { code: 'server_error', message: credentialsRes.error.message } });
252285
return;
253286
}
254287

@@ -270,13 +303,29 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
270303
}
271304
case 'TBA': {
272305
if (!body.connection_config || !body.connection_config['accountId']) {
306+
await logCtx.failed();
273307
res.status(400).send({
274308
error: { code: 'invalid_body', message: 'Missing accountId in connection_config. This is required to create a TBA connection.' }
275309
});
276310

277311
return;
278312
}
279313

314+
const connectionConfig = body.connection_config || {};
315+
const connectionResponse = await testConnectionCredentials({
316+
config: integration,
317+
connectionConfig,
318+
connectionId,
319+
credentials: body.credentials,
320+
provider,
321+
logCtx
322+
});
323+
if (connectionResponse.isErr()) {
324+
await logCtx.failed();
325+
res.status(400).send({ error: { code: 'connection_test_failed', message: connectionResponse.error.message } });
326+
return;
327+
}
328+
280329
const [imported] = await connectionService.upsertAuthConnection({
281330
connectionId,
282331
providerConfigKey: body.provider_config_key,
@@ -316,6 +365,7 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
316365
}
317366
default:
318367
// Missing Bill, Signature, JWT, TwoStep, AppStore
368+
await logCtx.failed();
319369
res.status(400).send({ error: { code: 'invalid_body', message: `Unsupported auth type ${provider.auth_mode}` } });
320370
return;
321371
}
@@ -326,10 +376,37 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
326376
}
327377

328378
if (!updatedConnection) {
379+
await logCtx.failed();
329380
res.status(500).send({ error: { code: 'server_error', message: `Failed to create connection` } });
330381
return;
331382
}
332383

384+
const customValidationResponse = await validateConnection({
385+
connection: updatedConnection.connection,
386+
config: integration,
387+
account,
388+
logCtx
389+
});
390+
391+
if (customValidationResponse.isErr()) {
392+
if (updatedConnection.operation === 'creation') {
393+
// since this is a new invalid connection, delete it with no trace of it
394+
await connectionService.hardDelete(updatedConnection.connection.id);
395+
}
396+
397+
const payload = customValidationResponse.error?.payload;
398+
const message = typeof payload['message'] === 'string' ? payload['message'] : 'Connection failed validation';
399+
400+
await logCtx.failed();
401+
res.status(400).send({
402+
error: {
403+
code: 'connection_validation_failed',
404+
message
405+
}
406+
});
407+
return;
408+
}
409+
333410
let endUser: EndUser | undefined;
334411
if (body.end_user) {
335412
await db.knex.transaction(async (trx) => {
@@ -340,6 +417,7 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
340417
endUser: EndUserMapper.apiToEndUser(body.end_user!)
341418
});
342419
if (endUserRes.isErr()) {
420+
await logCtx.failed();
343421
res.status(500).send({ error: { code: 'server_error', message: 'Failed to update end user' } });
344422
return;
345423
}
@@ -356,6 +434,13 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
356434

357435
const connection = encryptionManager.decryptConnection(updatedConnection.connection);
358436

437+
await logCtx.enrichOperation({
438+
connectionId: updatedConnection.connection.id,
439+
connectionName: updatedConnection.connection.connection_id
440+
});
441+
void logCtx.info('Connection creation was successful');
442+
await logCtx.success();
443+
359444
res.status(201).send(
360445
connectionFullToPublicApi({ data: connection, provider: providerName, activeLog: [], endUser: endUser ? EndUserMapper.to(endUser) : null })
361446
);

packages/types/lib/connection/api/get.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export type PostPublicConnection = Endpoint<{
9696
end_user?: EndUserInput | undefined;
9797
tags?: Tags | undefined;
9898
};
99+
Error: ApiError<'connection_test_failed'> | ApiError<'connection_validation_failed'>;
99100
Success: ApiPublicConnectionFull;
100101
}>;
101102

0 commit comments

Comments
 (0)