Skip to content

Commit

Permalink
feat(webhooks): send end user on connection creation (#3065)
Browse files Browse the repository at this point in the history
## 🤓☝️ Changes

Fixes
https://linear.app/nango/issue/NAN-1880/send-back-the-end-user-profile-in-the-webhook

- Send `endUser` in webhook on connection creation
When we use a session token, we pass metadata, but when we receive a
webhook, we get a random connectionId that is impossible to link to an
actual user. Sending back the userId (or orgId) will allow customers to
link this connectionId to an actual user without needing to do any
additional work.

NB: we don't send it in any other webhook since the connectionId should
have been linked already


## 🧪 How to tests?

- Go to dashboard
- Modify webhook url to your favorite webhook dumper
- Allow webhook to be sent on connection creation
- Create a new connection with the connect UI

![Screenshot 2024-11-26 at 17 32
59](https://github.com/user-attachments/assets/84f5432f-46b7-4fe9-8fa1-bba30948de7d)
  • Loading branch information
bodinsamuel authored Nov 27, 2024
1 parent 57bac9a commit bd85a0d
Show file tree
Hide file tree
Showing 25 changed files with 210 additions and 213 deletions.
27 changes: 0 additions & 27 deletions packages/database/lib/getDbConfig.ts

This file was deleted.

20 changes: 12 additions & 8 deletions packages/server/lib/controllers/apiAuth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ import type { MessageRowInsert } from '@nangohq/types';

class ApiAuthController {
async apiKey(req: Request, res: Response<any, Required<RequestLocals>>, next: NextFunction) {
const { account, environment, authType } = res.locals;
const { account, environment } = res.locals;
const { providerConfigKey } = req.params;
const receivedConnectionId = req.query['connection_id'] as string | undefined;
const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {};
const isConnectSession = res.locals['authType'] === 'connectSession';

let logCtx: LogContext | undefined;
try {
Expand All @@ -53,7 +54,7 @@ class ApiAuthController {

const connectionId = receivedConnectionId || connectionService.generateConnectionId();

if (authType !== 'connectSession') {
if (!isConnectSession) {
const hmac = req.query['hmac'] as string | undefined;

const checked = await hmacCheck({ environment, logCtx, providerConfigKey, connectionId, hmac, res });
Expand Down Expand Up @@ -141,7 +142,7 @@ class ApiAuthController {
return;
}

if (authType === 'connectSession') {
if (isConnectSession) {
const session = res.locals.connectSession;
await linkConnection(db.knex, { endUserId: session.endUserId, connection: updatedConnection.connection });
}
Expand All @@ -156,7 +157,8 @@ class ApiAuthController {
environment,
account,
auth_mode: 'API_KEY',
operation: updatedConnection.operation
operation: updatedConnection.operation,
endUser: isConnectSession ? res.locals['endUser'] : undefined
},
config.provider,
logContextGetter,
Expand Down Expand Up @@ -203,10 +205,11 @@ class ApiAuthController {
}

async basic(req: Request, res: Response<any, Required<RequestLocals>>, next: NextFunction) {
const { account, environment, authType } = res.locals;
const { account, environment } = res.locals;
const { providerConfigKey } = req.params;
const receivedConnectionId = req.query['connection_id'] as string | undefined;
const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {};
const isConnectSession = res.locals['authType'] === 'connectSession';

let logCtx: LogContext | undefined;

Expand All @@ -229,7 +232,7 @@ class ApiAuthController {

const connectionId = receivedConnectionId || connectionService.generateConnectionId();

if (authType !== 'connectSession') {
if (!isConnectSession) {
const hmac = req.query['hmac'] as string | undefined;

const checked = await hmacCheck({ environment, logCtx, providerConfigKey, connectionId, hmac, res });
Expand Down Expand Up @@ -313,7 +316,7 @@ class ApiAuthController {
return;
}

if (authType === 'connectSession') {
if (isConnectSession) {
const session = res.locals.connectSession;
await linkConnection(db.knex, { endUserId: session.endUserId, connection: updatedConnection.connection });
}
Expand All @@ -328,7 +331,8 @@ class ApiAuthController {
environment,
account,
auth_mode: 'API_KEY',
operation: updatedConnection.operation
operation: updatedConnection.operation,
endUser: isConnectSession ? res.locals['endUser'] : undefined
},
config.provider,
logContextGetter,
Expand Down
16 changes: 12 additions & 4 deletions packages/server/lib/controllers/appAuth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { stringifyError } from '@nangohq/utils';
import { connectionCreated as connectionCreatedHook, connectionCreationFailed as connectionCreationFailedHook } from '../hooks/hooks.js';
import { linkConnection } from '../services/endUser.service.js';
import db from '@nangohq/database';
import type { ConnectSessionAndEndUser } from '../services/connectSession.service.js';
import { getConnectSession } from '../services/connectSession.service.js';

class AppAuthController {
Expand Down Expand Up @@ -188,15 +189,21 @@ class AppAuthController {
return publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, WSErrBuilder.UnknownError('failed to create connection'));
}

let connectSession: ConnectSessionAndEndUser | undefined;
if (session.connectSessionId) {
const connectSession = await getConnectSession(db.knex, { id: session.connectSessionId, accountId: account.id, environmentId: environment.id });
if (connectSession.isErr()) {
const connectSessionRes = await getConnectSession(db.knex, {
id: session.connectSessionId,
accountId: account.id,
environmentId: environment.id
});
if (connectSessionRes.isErr()) {
await logCtx.error('Failed to get session');
await logCtx.failed();
return publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, WSErrBuilder.UnknownError('failed to get session'));
}

await linkConnection(db.knex, { endUserId: connectSession.value.endUserId, connection: updatedConnection.connection });
connectSession = connectSessionRes.value;
await linkConnection(db.knex, { endUserId: connectSession.connectSession.endUserId, connection: updatedConnection.connection });
}

await logCtx.enrichOperation({ connectionId: updatedConnection.connection.id!, connectionName: updatedConnection.connection.connection_id });
Expand All @@ -206,7 +213,8 @@ class AppAuthController {
environment,
account,
auth_mode: 'APP',
operation: updatedConnection.operation
operation: updatedConnection.operation,
endUser: connectSession?.endUser
},
session.provider,
logContextGetter,
Expand Down
10 changes: 6 additions & 4 deletions packages/server/lib/controllers/appStoreAuth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import { isIntegrationAllowed } from '../utils/auth.js';

class AppStoreAuthController {
async auth(req: Request, res: Response<any, Required<RequestLocals>>, next: NextFunction) {
const { environment, account, authType } = res.locals;
const { environment, account } = res.locals;
const { providerConfigKey } = req.params;
const receivedConnectionId = req.query['connection_id'] as string | undefined;
const isConnectSession = res.locals['authType'] === 'connectSession';

let logCtx: LogContext | undefined;

Expand All @@ -38,7 +39,7 @@ class AppStoreAuthController {

const connectionId = receivedConnectionId || connectionService.generateConnectionId();

if (authType !== 'connectSession') {
if (!isConnectSession) {
const hmac = req.query['hmac'] as string | undefined;

const checked = await hmacCheck({ environment, logCtx, providerConfigKey, connectionId, hmac, res });
Expand Down Expand Up @@ -146,7 +147,7 @@ class AppStoreAuthController {
return;
}

if (authType === 'connectSession') {
if (isConnectSession) {
const session = res.locals.connectSession;
await linkConnection(db.knex, { endUserId: session.endUserId, connection: updatedConnection.connection });
}
Expand All @@ -161,7 +162,8 @@ class AppStoreAuthController {
environment,
account,
auth_mode: 'APP_STORE',
operation: updatedConnection.operation
operation: updatedConnection.operation,
endUser: isConnectSession ? res.locals['endUser'] : undefined
},
config.provider,
logContextGetter,
Expand Down
10 changes: 6 additions & 4 deletions packages/server/lib/controllers/auth/postBill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,14 @@ export const postPublicBillAuthorization = asyncWrapper<PostPublicBillAuthorizat
return;
}

const { account, environment, authType } = res.locals;
const { account, environment } = res.locals;
const { username: userName, password: password, organization_id: organizationId, dev_key: devkey }: PostPublicBillAuthorization['Body'] = val.data;
const queryString: PostPublicBillAuthorization['Querystring'] = queryStringVal.data;
const { providerConfigKey }: PostPublicBillAuthorization['Params'] = paramsVal.data;
const connectionConfig = queryString.params ? getConnectionConfig(queryString.params) : {};
const connectionId = queryString.connection_id || connectionService.generateConnectionId();
const hmac = 'hmac' in queryString ? queryString.hmac : undefined;
const isConnectSession = res.locals['authType'] === 'connectSession';

let logCtx: LogContext | undefined;

Expand All @@ -92,7 +93,7 @@ export const postPublicBillAuthorization = asyncWrapper<PostPublicBillAuthorizat
);
void analytics.track(AnalyticsTypes.PRE_BILL_AUTH, account.id);

if (authType !== 'connectSession') {
if (!isConnectSession) {
const checked = await hmacCheck({ environment, logCtx, providerConfigKey, connectionId, hmac, res });
if (!checked) {
return;
Expand Down Expand Up @@ -156,7 +157,7 @@ export const postPublicBillAuthorization = asyncWrapper<PostPublicBillAuthorizat
return;
}

if (authType === 'connectSession') {
if (isConnectSession) {
const session = res.locals.connectSession;
await linkConnection(db.knex, { endUserId: session.endUserId, connection: updatedConnection.connection });
}
Expand All @@ -171,7 +172,8 @@ export const postPublicBillAuthorization = asyncWrapper<PostPublicBillAuthorizat
environment,
account,
auth_mode: 'BILL',
operation: updatedConnection.operation
operation: updatedConnection.operation,
endUser: isConnectSession ? res.locals['endUser'] : undefined
},
config.provider,
logContextGetter,
Expand Down
10 changes: 6 additions & 4 deletions packages/server/lib/controllers/auth/postJwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,14 @@ export const postPublicJwtAuthorization = asyncWrapper<PostPublicJwtAuthorizatio
return;
}

const { account, environment, authType } = res.locals;
const { account, environment } = res.locals;
const { privateKeyId = '', issuerId = '', privateKey } = val.data as PostPublicJwtAuthorization['Body'];
const queryString: PostPublicJwtAuthorization['Querystring'] = queryStringVal.data;
const { providerConfigKey }: PostPublicJwtAuthorization['Params'] = paramVal.data;
const connectionConfig = queryString.params ? getConnectionConfig(queryString.params) : {};
const connectionId = queryString.connection_id || connectionService.generateConnectionId();
const hmac = 'hmac' in queryString ? queryString.hmac : undefined;
const isConnectSession = res.locals['authType'] === 'connectSession';

let logCtx: LogContext | undefined;

Expand All @@ -101,7 +102,7 @@ export const postPublicJwtAuthorization = asyncWrapper<PostPublicJwtAuthorizatio
);
void analytics.track(AnalyticsTypes.PRE_JWT_AUTH, account.id);

if (authType !== 'connectSession') {
if (!isConnectSession) {
const checked = await hmacCheck({ environment, logCtx, providerConfigKey, connectionId, hmac, res });
if (!checked) {
return;
Expand Down Expand Up @@ -180,7 +181,7 @@ export const postPublicJwtAuthorization = asyncWrapper<PostPublicJwtAuthorizatio
return;
}

if (authType === 'connectSession') {
if (isConnectSession) {
const session = res.locals.connectSession;
await linkConnection(db.knex, { endUserId: session.endUserId, connection: updatedConnection.connection });
}
Expand All @@ -195,7 +196,8 @@ export const postPublicJwtAuthorization = asyncWrapper<PostPublicJwtAuthorizatio
environment,
account,
auth_mode: 'JWT',
operation: updatedConnection.operation
operation: updatedConnection.operation,
endUser: isConnectSession ? res.locals['endUser'] : undefined
},
config.provider,
logContextGetter,
Expand Down
10 changes: 6 additions & 4 deletions packages/server/lib/controllers/auth/postSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,14 @@ export const postPublicSignatureAuthorization = asyncWrapper<PostPublicSignature
return;
}

const { account, environment, authType } = res.locals;
const { account, environment } = res.locals;
const { username, password }: PostPublicSignatureAuthorization['Body'] = val.data;
const queryString: PostPublicSignatureAuthorization['Querystring'] = queryStringVal.data;
const { providerConfigKey }: PostPublicSignatureAuthorization['Params'] = paramsVal.data;
const connectionConfig = queryString.params ? getConnectionConfig(queryString.params) : {};
const connectionId = queryString.connection_id || connectionService.generateConnectionId();
const hmac = 'hmac' in queryString ? queryString.hmac : undefined;
const isConnectSession = res.locals['authType'] === 'connectSession';

let logCtx: LogContext | undefined;

Expand All @@ -95,7 +96,7 @@ export const postPublicSignatureAuthorization = asyncWrapper<PostPublicSignature
);
void analytics.track(AnalyticsTypes.PRE_SIGNATURE_AUTH, account.id);

if (authType !== 'connectSession') {
if (!isConnectSession) {
const checked = await hmacCheck({ environment, logCtx, providerConfigKey, connectionId, hmac, res });
if (!checked) {
return;
Expand Down Expand Up @@ -174,7 +175,7 @@ export const postPublicSignatureAuthorization = asyncWrapper<PostPublicSignature
return;
}

if (authType === 'connectSession') {
if (isConnectSession) {
const session = res.locals.connectSession;
await linkConnection(db.knex, { endUserId: session.endUserId, connection: updatedConnection.connection });
}
Expand All @@ -189,7 +190,8 @@ export const postPublicSignatureAuthorization = asyncWrapper<PostPublicSignature
environment,
account,
auth_mode: 'SIGNATURE',
operation: updatedConnection.operation
operation: updatedConnection.operation,
endUser: isConnectSession ? res.locals['endUser'] : undefined
},
config.provider,
logContextGetter,
Expand Down
10 changes: 6 additions & 4 deletions packages/server/lib/controllers/auth/postTableau.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ export const postPublicTableauAuthorization = asyncWrapper<PostPublicTableauAuth
return;
}

const { account, environment, authType } = res.locals;
const { account, environment } = res.locals;
const { pat_name: patName, pat_secret: patSecret, content_url: contentUrl }: PostPublicTableauAuthorization['Body'] = val.data;
const queryString: PostPublicTableauAuthorization['Querystring'] = queryStringVal.data;
const { providerConfigKey }: PostPublicTableauAuthorization['Params'] = paramVal.data;
const connectionConfig = queryString.params ? getConnectionConfig(queryString.params) : {};
const connectionId = queryString.connection_id || connectionService.generateConnectionId();
const hmac = 'hmac' in queryString ? queryString.hmac : undefined;
const isConnectSession = res.locals['authType'] === 'connectSession';

let logCtx: LogContext | undefined;

Expand All @@ -91,7 +92,7 @@ export const postPublicTableauAuthorization = asyncWrapper<PostPublicTableauAuth
);
void analytics.track(AnalyticsTypes.PRE_TBA_AUTH, account.id);

if (authType !== 'connectSession') {
if (!isConnectSession) {
const checked = await hmacCheck({ environment, logCtx, providerConfigKey, connectionId, hmac, res });
if (!checked) {
return;
Expand Down Expand Up @@ -159,7 +160,7 @@ export const postPublicTableauAuthorization = asyncWrapper<PostPublicTableauAuth
return;
}

if (authType === 'connectSession') {
if (isConnectSession) {
const session = res.locals.connectSession;
await linkConnection(db.knex, { endUserId: session.endUserId, connection: updatedConnection.connection });
}
Expand All @@ -174,7 +175,8 @@ export const postPublicTableauAuthorization = asyncWrapper<PostPublicTableauAuth
environment,
account,
auth_mode: 'TABLEAU',
operation: updatedConnection.operation
operation: updatedConnection.operation,
endUser: isConnectSession ? res.locals['endUser'] : undefined
},
config.provider,
logContextGetter,
Expand Down
Loading

0 comments on commit bd85a0d

Please sign in to comment.