Skip to content

Commit

Permalink
feat(connect): allow passing oauth_scopes (#3104)
Browse files Browse the repository at this point in the history
## Changes

Fixes
https://linear.app/nango/issue/NAN-2304/connect-support-oauth-scopes

- Support passing `oauth_scopes_override`  and `user_scopes`


## Tests

I don't know, if you have an idea of an integration that supports this
let me know :D
  • Loading branch information
bodinsamuel authored Dec 4, 2024
1 parent 4054d73 commit e238a7b
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 9 deletions.
9 changes: 9 additions & 0 deletions docs-v2/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2158,6 +2158,15 @@ components:
additionalProperties:
type: object
description: the unique name of an integration
additionalProperties: false
properties:
user_scopes:
type: string
description: User scopes (for Slack only)
connection_config:
type: object
additionalProperties: true
properties:
oauth_scopes_override:
type: string
description: Override oauth scopes
3 changes: 1 addition & 2 deletions packages/server/lib/controllers/auth/postUnauthenticated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { isIntegrationAllowed } from '../../utils/auth.js';
const queryStringValidation = z
.object({
connection_id: connectionIdSchema.optional(),
params: z.record(z.any()).optional(),
user_scope: z.string().optional()
params: z.record(z.any()).optional()
})
.and(connectionCredential);

Expand Down
12 changes: 10 additions & 2 deletions packages/server/lib/controllers/connect/postSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ const bodySchema = z
.record(
z
.object({
connection_config: z.record(z.unknown())
user_scopes: z.string().optional(),
connection_config: z
.object({
oauth_scopes_override: z.string().optional()
})
.passthrough()
})
.strict()
)
Expand Down Expand Up @@ -120,7 +125,10 @@ export const postConnectSessions = asyncWrapper<PostConnectSessions>(async (req,
allowedIntegrations: req.body.allowed_integrations || null,
integrationsConfigDefaults: req.body.integrations_config_defaults
? Object.fromEntries(
Object.entries(req.body.integrations_config_defaults).map(([key, value]) => [key, { connectionConfig: value.connection_config }])
Object.entries(req.body.integrations_config_defaults).map(([key, value]) => [
key,
{ user_scopes: value.user_scopes, connectionConfig: value.connection_config }
])
)
: null
});
Expand Down
17 changes: 14 additions & 3 deletions packages/server/lib/controllers/oauth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class OAuthController {
const { providerConfigKey } = req.params;
const receivedConnectionId = req.query['connection_id'] as string | undefined;
const wsClientId = req.query['ws_client_id'] as string | undefined;
const userScope = req.query['user_scope'] as string | undefined;
let userScope = req.query['user_scope'] as string | undefined;
const isConnectSession = res.locals['authType'] === 'connectSession';

let logCtx: LogContext | undefined;
Expand Down Expand Up @@ -149,6 +149,12 @@ class OAuthController {
return;
}

if (isConnectSession) {
// Session token always win
const defaults = res.locals.connectSession.integrationsConfigDefaults?.[config.unique_key];
userScope = defaults?.user_scopes || undefined;
}

const session: OAuthSession = {
providerConfigKey: providerConfigKey,
provider: config.provider,
Expand Down Expand Up @@ -195,7 +201,12 @@ class OAuthController {
});
}

if (connectionConfig['oauth_scopes_override']) {
if (isConnectSession) {
const defaults = res.locals.connectSession.integrationsConfigDefaults?.[config.unique_key];
if (defaults?.connectionConfig.oauth_scopes_override) {
config.oauth_scopes = defaults?.connectionConfig.oauth_scopes_override;
}
} else if (connectionConfig['oauth_scopes_override']) {
config.oauth_scopes = connectionConfig['oauth_scopes_override'];
}

Expand Down Expand Up @@ -923,7 +934,7 @@ class OAuthController {
return publisher.notifySuccess(res, channel, providerConfigKey, connectionId);
}

// check for oauth overrides in the connnection config
// check for oauth overrides in the connection config
if (session.connectionConfig['oauth_client_id_override']) {
config.oauth_client_id = session.connectionConfig['oauth_client_id_override'];
}
Expand Down
13 changes: 12 additions & 1 deletion packages/types/lib/connect/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@ import type { Endpoint } from '../api.js';

export interface ConnectSessionPayload {
allowed_integrations?: string[] | undefined;
integrations_config_defaults?: Record<string, { connection_config: Record<string, unknown> }> | undefined;
integrations_config_defaults?:
| Record<
string,
{
user_scopes?: string | undefined;
connection_config: {
[key: string]: unknown;
oauth_scopes_override?: string | undefined;
};
}
>
| undefined;
end_user: {
id: string;
email: string;
Expand Down
12 changes: 11 additions & 1 deletion packages/types/lib/connect/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ export interface ConnectSession {
readonly accountId: number;
readonly environmentId: number;
readonly allowedIntegrations?: string[] | null;
readonly integrationsConfigDefaults?: Record<string, { connectionConfig: Record<string, unknown> }> | null;
readonly integrationsConfigDefaults?: Record<
string,
{
/** Only used by Slack */
user_scopes?: string | undefined;
connectionConfig: {
[key: string]: unknown;
oauth_scopes_override?: string | undefined;
};
}
> | null;
readonly createdAt: Date;
readonly updatedAt: Date | null;
}

0 comments on commit e238a7b

Please sign in to comment.