Skip to content

Commit

Permalink
feat: make connectionId optional when creating a new connection (#2746)
Browse files Browse the repository at this point in the history
## Describe your changes

Making connectionId optional when creating a connection. If connectionId
is not provided a uuid is generated.
I haven't updated the documentation which is still relevant. I will do
it once we decide not passing the connectionId is the default way. With
this PR we are making it possible without promoting it yet.

## Issue ticket number and link


https://linear.app/nango/issue/NAN-1747/make-connectionid-optional-when-creating-a-new-connection

## Checklist before requesting a review (skip if just adding/editing
APIs & templates)
- [ ] I added tests, otherwise the reason is: 
- [ ] I added observability, otherwise the reason is:
- [ ] I added analytics, otherwise the reason is:
  • Loading branch information
TBonnin authored and hassan254-prog committed Sep 25, 2024
1 parent d4d5c35 commit 204a757
Show file tree
Hide file tree
Showing 14 changed files with 130 additions and 121 deletions.
46 changes: 33 additions & 13 deletions packages/frontend/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export interface AuthResult {
isPending?: boolean;
}

interface AuthOptions {
type AuthOptions = {
detectClosedAuthWindow?: boolean; // If true, `nango.auth()` would fail if the login window is closed before the authorization flow is completed
}
} & (ConnectionConfig | OAuth2ClientCredentials | OAuthCredentialsOverride | BasicApiCredentials | ApiKeyCredentials | AppStoreCredentials);

export default class Nango {
private hostBaseUrl: string;
Expand Down Expand Up @@ -82,11 +82,24 @@ export default class Nango {
/**
* Creates a new unauthenticated connection using the specified provider configuration key and connection ID
* @param providerConfigKey - The key identifying the provider configuration on Nango
* @param connectionId - The ID of the connection
* @param connectionId - Optional. The ID of the connection
* @param connectionConfig - Optional. Additional configuration for the connection
* @returns A promise that resolves with the authentication result
*/
public async create(providerConfigKey: string, connectionId: string, connectionConfig?: ConnectionConfig): Promise<AuthResult> {
public async create(providerConfigKey: string, connectionConfig?: ConnectionConfig): Promise<AuthResult>;
public async create(providerConfigKey: string, connectionId: string, connectionConfig?: ConnectionConfig): Promise<AuthResult>;
public async create(
providerConfigKey: string,
connectionIdOrConnectionConfig?: string | ConnectionConfig,
moreConnectionConfig?: ConnectionConfig
): Promise<AuthResult> {
let connectionId: string | null = null;
let connectionConfig: ConnectionConfig | undefined = moreConnectionConfig;
if (typeof connectionIdOrConnectionConfig === 'string') {
connectionId = connectionIdOrConnectionConfig;
} else {
connectionConfig = connectionIdOrConnectionConfig;
}
const url = this.hostBaseUrl + `/auth/unauthenticated/${providerConfigKey}${this.toQueryString(connectionId, connectionConfig)}`;

const res = await fetch(url, {
Expand All @@ -107,16 +120,23 @@ export default class Nango {
/**
* Initiates the authorization process for a connection
* @param providerConfigKey - The key identifying the provider configuration on Nango
* @param connectionId - The ID of the connection for which to authorize
* @param connectionId - Optional. The ID of the connection for which to authorize
* @param options - Optional. Additional options for authorization
* @returns A promise that resolves with the authorization result
*/
public auth(
providerConfigKey: string,
connectionId: string,
options?: (ConnectionConfig | OAuth2ClientCredentials | OAuthCredentialsOverride | BasicApiCredentials | ApiKeyCredentials | AppStoreCredentials) &
AuthOptions
): Promise<AuthResult> {
public auth(providerConfigKey: string, options?: AuthOptions): Promise<AuthResult>;
public auth(providerConfigKey: string, connectionId: string, options?: AuthOptions): Promise<AuthResult>;
public auth(providerConfigKey: string, connectionIdOrOptions?: string | AuthOptions, moreOptions?: AuthOptions): Promise<AuthResult> {
let connectionId: string | null = null;
let options: AuthOptions | undefined = moreOptions;
if (typeof connectionIdOrOptions === 'string') {
connectionId = connectionIdOrOptions;
} else {
options = {
...options,
...connectionIdOrOptions
};
}
if (
options &&
'credentials' in options &&
Expand Down Expand Up @@ -286,7 +306,7 @@ export default class Nango {
*/
private async customAuth(
providerConfigKey: string,
connectionId: string,
connectionId: string | null,
connectionConfigWithCredentials: ConnectionConfig,
connectionConfig?: ConnectionConfig
): Promise<AuthResult> {
Expand Down Expand Up @@ -430,7 +450,7 @@ export default class Nango {
* @param connectionConfig - Optional. Additional configuration for the connection
* @returns The generated query string
*/
private toQueryString(connectionId: string, connectionConfig?: ConnectionConfig): string {
private toQueryString(connectionId: string | null, connectionConfig?: ConnectionConfig): string {
const query: string[] = [];

if (connectionId) {
Expand Down
32 changes: 12 additions & 20 deletions packages/server/lib/controllers/apiAuth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ApiAuthController {
async apiKey(req: Request, res: Response<any, Required<RequestLocals>>, next: NextFunction) {
const { account, environment } = res.locals;
const { providerConfigKey } = req.params;
const connectionId = req.query['connection_id'] as string | undefined;
const receivedConnectionId = req.query['connection_id'] as string | undefined;
const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {};

let logCtx: LogContext | undefined;
Expand All @@ -48,12 +48,6 @@ class ApiAuthController {
return;
}

if (!connectionId) {
errorManager.errRes(res, 'missing_connection_id');

return;
}

const hmacEnabled = await hmacService.isEnabled(environment.id);
if (hmacEnabled) {
const hmac = req.query['hmac'] as string | undefined;
Expand All @@ -65,7 +59,7 @@ class ApiAuthController {

return;
}
const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, connectionId);
const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, receivedConnectionId);
if (!verified) {
await logCtx.error('Invalid HMAC');
await logCtx.failed();
Expand All @@ -76,6 +70,8 @@ class ApiAuthController {
}
}

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

const config = await configService.getProviderConfig(providerConfigKey, environment.id);

if (config == null) {
Expand Down Expand Up @@ -176,7 +172,7 @@ class ApiAuthController {
if (logCtx) {
void connectionCreationFailedHook(
{
connection: { connection_id: connectionId!, provider_config_key: providerConfigKey! },
connection: { connection_id: receivedConnectionId!, provider_config_key: providerConfigKey! },
environment,
account,
auth_mode: 'API_KEY',
Expand All @@ -199,7 +195,7 @@ class ApiAuthController {
environmentId: environment.id,
metadata: {
providerConfigKey,
connectionId
receivedConnectionId
}
});

Expand All @@ -210,7 +206,7 @@ class ApiAuthController {
async basic(req: Request, res: Response<any, Required<RequestLocals>>, next: NextFunction) {
const { account, environment } = res.locals;
const { providerConfigKey } = req.params;
const connectionId = req.query['connection_id'] as string | undefined;
const receivedConnectionId = req.query['connection_id'] as string | undefined;
const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {};

let logCtx: LogContext | undefined;
Expand All @@ -232,12 +228,6 @@ class ApiAuthController {
return;
}

if (!connectionId) {
errorManager.errRes(res, 'missing_connection_id');

return;
}

const hmacEnabled = await hmacService.isEnabled(environment.id);
if (hmacEnabled) {
const hmac = req.query['hmac'] as string | undefined;
Expand All @@ -249,7 +239,7 @@ class ApiAuthController {

return;
}
const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, connectionId);
const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, receivedConnectionId);
if (!verified) {
await logCtx.error('Invalid HMAC');
await logCtx.failed();
Expand All @@ -259,6 +249,8 @@ class ApiAuthController {
}
}

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

const { username = '', password = '' } = req.body;

const config = await configService.getProviderConfig(providerConfigKey, environment.id);
Expand Down Expand Up @@ -354,7 +346,7 @@ class ApiAuthController {
if (logCtx) {
void connectionCreationFailedHook(
{
connection: { connection_id: connectionId!, provider_config_key: providerConfigKey! },
connection: { connection_id: receivedConnectionId!, provider_config_key: providerConfigKey! },
environment,
account,
auth_mode: 'API_KEY',
Expand All @@ -377,7 +369,7 @@ class ApiAuthController {
environmentId: environment.id,
metadata: {
providerConfigKey,
connectionId
connectionId: receivedConnectionId
}
});

Expand Down
15 changes: 5 additions & 10 deletions packages/server/lib/controllers/appAuth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,16 @@ class AppAuthController {

void analytics.track(AnalyticsTypes.PRE_APP_AUTH, account.id);

const { providerConfigKey, connectionId, webSocketClientId: wsClientId } = session;
const { providerConfigKey, connectionId: receivedConnectionId, webSocketClientId: wsClientId } = session;
const logCtx = await logContextGetter.get({ id: session.activityLogId });

try {
if (!providerConfigKey) {
errorManager.errRes(res, 'missing_connection');

return;
}

if (!connectionId) {
errorManager.errRes(res, 'missing_connection_id');

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

const config = await configService.getProviderConfig(providerConfigKey, environment.id);

Expand Down Expand Up @@ -226,12 +221,12 @@ class AppAuthController {
await telemetry.log(LogTypes.AUTH_TOKEN_REQUEST_FAILURE, `App auth request process failed ${content}`, LogActionEnum.AUTH, {
environmentId: String(environment.id),
providerConfigKey: String(providerConfigKey),
connectionId: String(connectionId)
connectionId: String(receivedConnectionId)
});

void connectionCreationFailedHook(
{
connection: { connection_id: connectionId, provider_config_key: providerConfigKey },
connection: { connection_id: receivedConnectionId, provider_config_key: providerConfigKey },
environment,
account,
auth_mode: 'APP',
Expand All @@ -245,7 +240,7 @@ class AppAuthController {
logCtx
);

return publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, WSErrBuilder.UnknownError(prettyError));
return publisher.notifyErr(res, wsClientId, providerConfigKey, receivedConnectionId, WSErrBuilder.UnknownError(prettyError));
}
}
}
Expand Down
16 changes: 6 additions & 10 deletions packages/server/lib/controllers/appStoreAuth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class AppStoreAuthController {
async auth(req: Request, res: Response<any, Required<RequestLocals>>, next: NextFunction) {
const { environment, account } = res.locals;
const { providerConfigKey } = req.params;
const connectionId = req.query['connection_id'] as string | undefined;
const receivedConnectionId = req.query['connection_id'] as string | undefined;

let logCtx: LogContext | undefined;

Expand All @@ -42,12 +42,6 @@ class AppStoreAuthController {
return;
}

if (!connectionId) {
errorManager.errRes(res, 'missing_connection_id');

return;
}

const hmacEnabled = await hmacService.isEnabled(environment.id);
if (hmacEnabled) {
const hmac = req.query['hmac'] as string | undefined;
Expand All @@ -59,7 +53,7 @@ class AppStoreAuthController {

return;
}
const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, connectionId);
const verified = await hmacService.verify(hmac, environment.id, providerConfigKey, receivedConnectionId);
if (!verified) {
await logCtx.error('Invalid HMAC');
await logCtx.failed();
Expand All @@ -70,6 +64,8 @@ class AppStoreAuthController {
}
}

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

const config = await configService.getProviderConfig(providerConfigKey, environment.id);

if (config == null) {
Expand Down Expand Up @@ -185,7 +181,7 @@ class AppStoreAuthController {

void connectionCreationFailedHook(
{
connection: { connection_id: connectionId!, provider_config_key: providerConfigKey! },
connection: { connection_id: receivedConnectionId!, provider_config_key: providerConfigKey! },
environment,
account,
auth_mode: 'APP_STORE',
Expand All @@ -209,7 +205,7 @@ class AppStoreAuthController {
environmentId: environment.id,
metadata: {
providerConfigKey,
connectionId
connectionId: receivedConnectionId
}
});

Expand Down
12 changes: 7 additions & 5 deletions packages/server/lib/controllers/auth/postTableau.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const bodyValidation = z

const queryStringValidation = z
.object({
connection_id: connectionIdSchema,
connection_id: connectionIdSchema.optional(),
params: z.record(z.any()).optional(),
public_key: z.string().uuid(),
hmac: z.string().optional()
Expand Down Expand Up @@ -70,7 +70,7 @@ export const postPublicTableauAuthorization = asyncWrapper<PostPublicTableauAuth

const { account, environment } = res.locals;
const { pat_name: patName, pat_secret: patSecret, content_url: contentUrl }: PostPublicTableauAuthorization['Body'] = val.data;
const { connection_id: connectionId, params, hmac }: PostPublicTableauAuthorization['Querystring'] = queryStringVal.data;
const { connection_id: receivedConnectionId, params, hmac }: PostPublicTableauAuthorization['Querystring'] = queryStringVal.data;
const { providerConfigKey }: PostPublicTableauAuthorization['Params'] = paramVal.data;
const connectionConfig = params ? getConnectionConfig(params) : {};

Expand All @@ -91,7 +91,7 @@ export const postPublicTableauAuthorization = asyncWrapper<PostPublicTableauAuth
environment,
logCtx,
providerConfigKey,
connectionId,
connectionId: receivedConnectionId,
hmac,
res
});
Expand Down Expand Up @@ -138,6 +138,8 @@ export const postPublicTableauAuthorization = asyncWrapper<PostPublicTableauAuth

await logCtx.info('Tableau credentials creation was successful');
await logCtx.success();

const connectionId = receivedConnectionId || connectionService.generateConnectionId();
const [updatedConnection] = await connectionService.upsertTableauConnection({
connectionId,
providerConfigKey,
Expand Down Expand Up @@ -172,7 +174,7 @@ export const postPublicTableauAuthorization = asyncWrapper<PostPublicTableauAuth

void connectionCreationFailedHook(
{
connection: { connection_id: connectionId, provider_config_key: providerConfigKey },
connection: { connection_id: receivedConnectionId!, provider_config_key: providerConfigKey },
environment,
account,
auth_mode: 'TABLEAU',
Expand All @@ -196,7 +198,7 @@ export const postPublicTableauAuthorization = asyncWrapper<PostPublicTableauAuth
environmentId: environment.id,
metadata: {
providerConfigKey,
connectionId
connectionId: receivedConnectionId
}
});

Expand Down
8 changes: 5 additions & 3 deletions packages/server/lib/controllers/auth/postTba.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const bodyValidation = z

const queryStringValidation = z
.object({
connection_id: connectionIdSchema,
connection_id: connectionIdSchema.optional(),
params: z.record(z.any()).optional(),
hmac: z.string().optional(),
public_key: z.string().uuid()
Expand Down Expand Up @@ -64,7 +64,7 @@ export const postPublicTbaAuthorization = asyncWrapper<PostPublicTbaAuthorizatio

const { token_id: tokenId, token_secret: tokenSecret, oauth_client_id_override, oauth_client_secret_override } = body;

const { connection_id: connectionId, params, hmac }: PostPublicTbaAuthorization['Querystring'] = queryStringVal.data;
const { connection_id: receivedConnectionId, params, hmac }: PostPublicTbaAuthorization['Querystring'] = queryStringVal.data;
const { providerConfigKey }: PostPublicTbaAuthorization['Params'] = paramVal.data;

const logCtx = await logContextGetter.create(
Expand All @@ -77,7 +77,7 @@ export const postPublicTbaAuthorization = asyncWrapper<PostPublicTbaAuthorizatio
);
void analytics.track(AnalyticsTypes.PRE_TBA_AUTH, account.id);

await hmacCheck({ environment, logCtx, providerConfigKey, connectionId, hmac, res });
await hmacCheck({ environment, logCtx, providerConfigKey, connectionId: receivedConnectionId, hmac, res });

const config = await configService.getProviderConfig(providerConfigKey, environment.id);

Expand Down Expand Up @@ -139,6 +139,8 @@ export const postPublicTbaAuthorization = asyncWrapper<PostPublicTbaAuthorizatio
}
}

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

const connectionResponse = await connectionTestHook(
config.provider,
provider,
Expand Down
Loading

0 comments on commit 204a757

Please sign in to comment.