Skip to content

Commit 5a7b811

Browse files
authored
fix(auth): split Github App from connection (#3946)
## Changes Fixes https://linear.app/nango/issue/NAN-2935/app-client - Split Github App from connection.service.ts No logic change. Just removing some unrelated code from the model This one is a bit all over the place, unfortunately <!-- This is an auto-generated description by mrge. --> --- ## Summary by mrge Moved Github App authentication logic out of connection.service.ts into a new githubApp.ts file for better separation. No logic changes were made. <!-- End of auto-generated description by mrge. -->
1 parent 7e0dffb commit 5a7b811

File tree

10 files changed

+146
-106
lines changed

10 files changed

+146
-106
lines changed

docs-v2/integrations/all/docuware/connect.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ sidebarTitle: DocuWare
88

99
To authenticate with DocuWare, you will need:
1010
1. **Domain** - The domain to your DocuWare account.
11-
2. **Username** - The username used to log in to your DocuWare account.
11+
2. **Username** - The username used to log in to your DocuWare account.
1212
3. **Password** - The password associated with the above DocuWare account.
1313

1414

packages/server/lib/controllers/appAuth.controller.ts

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import db from '@nangohq/database';
22
import { logContextGetter } from '@nangohq/logs';
3-
import { configService, connectionService, environmentService, errorManager, getProvider, linkConnection } from '@nangohq/shared';
4-
import { stringifyError } from '@nangohq/utils';
3+
import { configService, connectionService, environmentService, errorManager, getProvider, githubAppClient, linkConnection } from '@nangohq/shared';
4+
import { report, stringifyError } from '@nangohq/utils';
55

66
import publisher from '../clients/publisher.client.js';
77
import { connectionCreated as connectionCreatedHook, connectionCreationFailed as connectionCreationFailedHook } from '../hooks/hooks.js';
@@ -11,7 +11,7 @@ import { missesInterpolationParam } from '../utils/utils.js';
1111
import * as WSErrBuilder from '../utils/web-socket-error.js';
1212

1313
import type { ConnectSessionAndEndUser } from '../services/connectSession.service.js';
14-
import type { AuthCredentials, NangoError } from '@nangohq/shared';
14+
import type { ProviderGithubApp } from '@nangohq/types';
1515
import type { NextFunction, Request, Response } from 'express';
1616

1717
class AppAuthController {
@@ -125,36 +125,19 @@ class AppAuthController {
125125
return;
126126
}
127127

128-
const { success, error, response: credentials } = await connectionService.getAppCredentials(provider, config, connectionConfig);
129-
130-
if (!success || !credentials) {
131-
void logCtx.error('Error during app token retrieval call', { error });
128+
const credentialsRes = await githubAppClient.createCredentials({ provider: provider as ProviderGithubApp, integration: config, connectionConfig });
129+
if (credentialsRes.isErr()) {
130+
report(credentialsRes.error);
131+
void logCtx.error('Error during Github App credentials creation', { error: credentialsRes.error });
132132
await logCtx.failed();
133-
134-
void connectionCreationFailedHook(
135-
{
136-
connection: { connection_id: connectionId, provider_config_key: providerConfigKey },
137-
environment,
138-
account,
139-
auth_mode: 'APP',
140-
error: {
141-
type: 'unknown',
142-
description: `Error during app token retrieval call: ${error?.message}`
143-
},
144-
operation: 'unknown'
145-
},
146-
account,
147-
config
148-
);
149-
150-
await publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, error as NangoError);
133+
await publisher.notifyErr(res, wsClientId, providerConfigKey, connectionId, credentialsRes.error);
151134
return;
152135
}
153136

154137
const [updatedConnection] = await connectionService.upsertConnection({
155138
connectionId,
156139
providerConfigKey,
157-
parsedRawCredentials: credentials as unknown as AuthCredentials,
140+
parsedRawCredentials: credentialsRes.value,
158141
connectionConfig,
159142
environmentId: environment.id
160143
});

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import db from '@nangohq/database';
22
import { logContextGetter } from '@nangohq/logs';
3-
import { NangoError, accountService, configService, connectionService, errorManager, getProvider } from '@nangohq/shared';
3+
import { NangoError, accountService, configService, connectionService, errorManager, getProvider, githubAppClient } from '@nangohq/shared';
44

55
import { NANGO_ADMIN_UUID } from './account.controller.js';
66
import { preConnectionDeletion } from '../hooks/connection/on/connection-deleted.js';
@@ -13,8 +13,16 @@ import { slackService } from '../services/slack.js';
1313
import { getOrchestrator } from '../utils/utils.js';
1414

1515
import type { RequestLocals } from '../utils/express.js';
16-
import type { AuthCredentials, ConnectionUpsertResponse, OAuth2Credentials } from '@nangohq/shared';
17-
import type { ApiKeyCredentials, BasicApiCredentials, ConnectionConfig, OAuth1Credentials, OAuth2ClientCredentials, TbaCredentials } from '@nangohq/types';
16+
import type { ConnectionUpsertResponse, OAuth2Credentials } from '@nangohq/shared';
17+
import type {
18+
ApiKeyCredentials,
19+
BasicApiCredentials,
20+
ConnectionConfig,
21+
OAuth1Credentials,
22+
OAuth2ClientCredentials,
23+
ProviderGithubApp,
24+
TbaCredentials
25+
} from '@nangohq/types';
1826
import type { NextFunction, Request, Response } from 'express';
1927

2028
const orchestrator = getOrchestrator();
@@ -494,17 +502,20 @@ class ConnectionController {
494502
return;
495503
}
496504

497-
const { success, error, response: credentials } = await connectionService.getAppCredentials(provider, config, connectionConfig);
498-
499-
if (!success || !credentials) {
500-
errorManager.errResFromNangoErr(res, error);
505+
const credentialsRes = await githubAppClient.createCredentials({
506+
provider: provider as ProviderGithubApp,
507+
integration: config,
508+
connectionConfig
509+
});
510+
if (credentialsRes.isErr()) {
511+
errorManager.errResFromNangoErr(res, credentialsRes.error);
501512
return;
502513
}
503514

504515
const [imported] = await connectionService.upsertConnection({
505516
connectionId,
506517
providerConfigKey: provider_config_key,
507-
parsedRawCredentials: credentials as unknown as AuthCredentials,
518+
parsedRawCredentials: credentialsRes.value,
508519
connectionConfig,
509520
environmentId: environment.id,
510521
metadata

packages/server/lib/controllers/oauth.controller.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import type { ConnectSessionAndEndUser } from '../services/connectSession.servic
4444
import type { RequestLocals } from '../utils/express.js';
4545
import type { LogContext } from '@nangohq/logs';
4646
import type { Config as ProviderConfig, ConnectionUpsertResponse, OAuth1RequestTokenResult, OAuth2Credentials, OAuthSession } from '@nangohq/shared';
47-
import type { ConnectionConfig, DBEnvironment, DBTeam, Provider, ProviderOAuth2 } from '@nangohq/types';
47+
import type { ConnectionConfig, DBEnvironment, DBTeam, Provider, ProviderGithubApp, ProviderOAuth2 } from '@nangohq/types';
4848
import type { NextFunction, Request, Response } from 'express';
4949

5050
class OAuthController {
@@ -1188,7 +1188,20 @@ class OAuthController {
11881188
{ initiateSync: true, runPostConnectionScript: false }
11891189
);
11901190
};
1191-
await connectionService.getAppCredentialsAndFinishConnection(connectionId, config, provider, connectionConfig, logCtx, connCreatedHook);
1191+
const createRes = await connectionService.getAppCredentialsAndFinishConnection(
1192+
connectionId,
1193+
config,
1194+
provider as unknown as ProviderGithubApp,
1195+
connectionConfig,
1196+
logCtx,
1197+
connCreatedHook
1198+
);
1199+
if (createRes.isErr()) {
1200+
void logCtx.error('Failed to create credentials');
1201+
await logCtx.failed();
1202+
await publisher.notifyErr(res, channel, providerConfigKey, connectionId, WSErrBuilder.UnknownError('failed to create credentials'));
1203+
return;
1204+
}
11921205
}
11931206

11941207
await logCtx.success();

packages/server/lib/webhook/github-app-oauth-webhook-routing.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { connectionCreated as connectionCreatedHook } from '../hooks/hooks.js';
1010
import type { WebhookHandler } from './types.js';
1111
import type { LogContextGetter } from '@nangohq/logs';
1212
import type { Config as ProviderConfig, ConnectionUpsertResponse } from '@nangohq/shared';
13-
import type { ConnectionConfig } from '@nangohq/types';
13+
import type { ConnectionConfig, ProviderGithubApp } from '@nangohq/types';
1414

1515
const logger = getLogger('Webhook.GithubAppOauth');
1616

@@ -116,7 +116,7 @@ async function handleCreateWebhook(integration: ProviderConfig, body: any, logCo
116116
await connectionService.getAppCredentialsAndFinishConnection(
117117
connection.connection_id,
118118
integration,
119-
provider,
119+
provider as ProviderGithubApp,
120120
connectionConfig,
121121
logCtx,
122122
connCreatedHook

packages/shared/lib/auth/githubApp.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Err, Ok } from '@nangohq/utils';
2+
3+
import * as jwtClient from './jwt.js';
4+
import { AuthCredentialsError } from '../utils/error.js';
5+
import { interpolateStringFromObject } from '../utils/utils.js';
6+
7+
import type { AppCredentials, ConnectionConfig, IntegrationConfig, ProviderGithubApp } from '@nangohq/types';
8+
import type { Result } from '@nangohq/utils';
9+
10+
/**
11+
* Create Github APP credentials
12+
*/
13+
export async function createCredentials({
14+
provider,
15+
integration,
16+
connectionConfig
17+
}: {
18+
provider: ProviderGithubApp;
19+
integration: IntegrationConfig;
20+
connectionConfig: ConnectionConfig;
21+
}): Promise<Result<AppCredentials, AuthCredentialsError>> {
22+
try {
23+
const tokenUrl = interpolateStringFromObject(provider.token_url, { connectionConfig });
24+
const privateKeyBase64 = integration.custom ? integration.custom['private_key'] : integration.oauth_client_secret;
25+
26+
const privateKey = Buffer.from(privateKeyBase64 as string, 'base64').toString('utf8');
27+
28+
const headers = {
29+
Accept: 'application/vnd.github.v3+json'
30+
};
31+
32+
const now = Math.floor(Date.now() / 1000);
33+
const expiration = now + 10 * 60;
34+
35+
const payload: Record<string, string | number> = {
36+
iat: now,
37+
exp: expiration,
38+
iss: (integration.custom ? integration.custom['app_id'] : integration.oauth_client_id) as string
39+
};
40+
41+
if (!payload['iss'] && connectionConfig['app_id']) {
42+
payload['iss'] = connectionConfig['app_id'];
43+
}
44+
45+
const create = await jwtClient.createCredentialsFromURL({
46+
privateKey,
47+
url: tokenUrl,
48+
payload,
49+
additionalApiHeaders: headers,
50+
options: { algorithm: 'RS256' }
51+
});
52+
53+
if (create.isErr()) {
54+
return Err(create.error);
55+
}
56+
57+
const rawCredentials = create.value;
58+
const credentials: AppCredentials = {
59+
type: 'APP',
60+
access_token: rawCredentials.token!,
61+
expires_at: rawCredentials.expires_at,
62+
raw: rawCredentials
63+
};
64+
65+
return Ok(credentials);
66+
} catch (err) {
67+
return Err(new AuthCredentialsError('github_app_token_fetch_error', { cause: err }));
68+
}
69+
}

packages/shared/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import errorManager, { ErrorSourceEnum } from './utils/error.manager.js';
2020
export { productTracking } from './utils/productTracking.js';
2121
export * as appleAppStoreClient from './auth/appleAppStore.js';
2222
export * as billClient from './auth/bill.js';
23+
export * as githubAppClient from './auth/githubApp.js';
2324
export * as jwtClient from './auth/jwt.js';
2425
export * as signatureClient from './auth/signature.js';
2526
export * as tableauClient from './auth/tableau.js';

packages/shared/lib/models/Auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export type AuthCredentials =
7070
| JwtCredentials
7171
| ApiKeyCredentials
7272
| BasicApiCredentials
73+
| AppCredentials
7374
| AppStoreCredentials;
7475

7576
export interface AppCredentials {

0 commit comments

Comments
 (0)