Skip to content

Commit

Permalink
[nan-638] add in credentials override and scopes override (#1906)
Browse files Browse the repository at this point in the history
## Describe your changes
Allow a user to override oauth credentials and scopes. Show in the UI
only for netsuite

## Issue ticket number and link
NAN-638

## 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
khaliqgant committed Mar 26, 2024
1 parent 8ddd5e7 commit 28e70dd
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 45 deletions.
24 changes: 21 additions & 3 deletions packages/frontend/lib/index.ts
Expand Up @@ -99,9 +99,14 @@ export default class Nango {
public auth(
providerConfigKey: string,
connectionId: string,
options?: (ConnectionConfig | BasicApiCredentials | ApiKeyCredentials | AppStoreCredentials) & AuthOptions
options?: (ConnectionConfig | OAuthCredentialsOverride | BasicApiCredentials | ApiKeyCredentials | AppStoreCredentials) & AuthOptions
): Promise<AuthResult> {
if (options && 'credentials' in options && Object.keys(options.credentials).length > 0) {
if (
options &&
'credentials' in options &&
(!('oauth_client_id' in options.credentials) || !('oauth_client_secret' in options.credentials)) &&
Object.keys(options.credentials).length > 0
) {
const credentials = options.credentials as BasicApiCredentials | ApiKeyCredentials;
const { credentials: _, ...connectionConfig } = options as ConnectionConfig;

Expand Down Expand Up @@ -311,6 +316,14 @@ export default class Nango {
query.push(`user_scope=${connectionConfig.user_scope.join(',')}`);
}

if (connectionConfig.credentials) {
const credentials = connectionConfig.credentials;
if ('oauth_client_id' in credentials && 'oauth_client_secret' in credentials) {
query.push(`credentials[oauth_client_id]=${credentials.oauth_client_id}`);
query.push(`credentials[oauth_client_secret]=${credentials.oauth_client_secret}`);
}
}

for (const param in connectionConfig.authorization_params) {
const val = connectionConfig.authorization_params[param];
if (typeof val === 'string') {
Expand All @@ -330,7 +343,12 @@ interface ConnectionConfig {
hmac?: string;
user_scope?: string[];
authorization_params?: Record<string, string | undefined>;
credentials?: BasicApiCredentials | ApiKeyCredentials | AppStoreCredentials;
credentials?: OAuthCredentialsOverride | BasicApiCredentials | ApiKeyCredentials | AppStoreCredentials;
}

interface OAuthCredentialsOverride {
oauth_client_id: string;
oauth_client_secret: string;
}

interface BasicApiCredentials {
Expand Down
20 changes: 20 additions & 0 deletions packages/server/lib/controllers/connection.controller.ts
Expand Up @@ -558,6 +558,26 @@ class ConnectionController {
raw: req.body.raw || req.body
};

if (req.body['oauth_client_id']) {
oAuthCredentials.config_override = {
client_id: req.body['oauth_client_id']
};
}

if (req.body['oauth_client_secret']) {
oAuthCredentials.config_override = {
...oAuthCredentials.config_override,
client_secret: req.body['oauth_client_secret']
};
}

if (req.body['oauth_scopes']) {
oAuthCredentials.config_override = {
...oAuthCredentials.config_override,
scopes: Array.isArray(req.body['oauth_scopes']) ? req.body['oauth_scopes'].join(',') : req.body['oauth_scopes']
};
}

const [imported] = await connectionService.importOAuthConnection(
connection_id,
provider_config_key,
Expand Down
69 changes: 66 additions & 3 deletions packages/server/lib/controllers/oauth.controller.ts
Expand Up @@ -16,7 +16,7 @@ import type {
TemplateOAuth2 as ProviderTemplateOAuth2,
OAuthSession,
OAuth1RequestTokenResult,
AuthCredentials,
OAuth2Credentials,
ConnectionConfig
} from '@nangohq/shared';
import {
Expand Down Expand Up @@ -95,6 +95,7 @@ class OAuthController {
const callbackUrl = await getOauthCallbackUrl(environmentId);
const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {};
const authorizationParams = req.query['authorization_params'] != null ? getAdditionalAuthorizationParams(req.query['authorization_params']) : {};
const overrideCredentials = req.query['credentials'] != null ? getAdditionalAuthorizationParams(req.query['credentials']) : {};

if (connectionId == null) {
await createActivityLogMessageAndEnd({
Expand Down Expand Up @@ -214,6 +215,24 @@ class OAuthController {

await updateSessionIdActivityLog(activityLogId as number, session.id);

// certain providers need the credentials to be specified in the config
if (overrideCredentials) {
if (overrideCredentials['oauth_client_id'] && overrideCredentials['oauth_client_secret']) {
config.oauth_client_id = overrideCredentials['oauth_client_id'];
config.oauth_client_secret = overrideCredentials['oauth_client_secret'];

session.connectionConfig = {
...session.connectionConfig,
oauth_client_id: config.oauth_client_id,
oauth_client_secret: config.oauth_client_secret
};
}
}

if (connectionConfig['oauth_scopes']) {
config.oauth_scopes = connectionConfig['oauth_scopes'];
}

if (config?.oauth_client_id == null || config?.oauth_client_secret == null || config.oauth_scopes == null) {
await createActivityLogMessageAndEnd({
level: 'error',
Expand Down Expand Up @@ -852,6 +871,16 @@ class OAuthController {
return publisher.notifySuccess(res, channel, providerConfigKey, connectionId);
}

// check for oauth overrides in the connnection config
if (session.connectionConfig['oauth_client_id'] && session.connectionConfig['oauth_client_secret']) {
config.oauth_client_id = session.connectionConfig['oauth_client_id'];
config.oauth_client_secret = session.connectionConfig['oauth_client_secret'];
}

if (session.connectionConfig['oauth_scopes']) {
config.oauth_scopes = session.connectionConfig['oauth_scopes'];
}

const simpleOAuthClient = new simpleOauth2.AuthorizationCode(oauth2Client.getSimpleOAuth2ClientConfig(config, template, session.connectionConfig));

let additionalTokenParams: Record<string, string> = {};
Expand Down Expand Up @@ -919,10 +948,10 @@ class OAuthController {

const tokenMetadata = getConnectionMetadataFromTokenResponse(rawCredentials, template);

let parsedRawCredentials: AuthCredentials;
let parsedRawCredentials: OAuth2Credentials;

try {
parsedRawCredentials = connectionService.parseRawCredentials(rawCredentials, ProviderAuthModes.OAuth2);
parsedRawCredentials = connectionService.parseRawCredentials(rawCredentials, ProviderAuthModes.OAuth2) as OAuth2Credentials;
} catch {
await createActivityLogMessageAndEnd({
level: 'error',
Expand Down Expand Up @@ -989,6 +1018,40 @@ class OAuthController {
};
}

if (connectionConfig['oauth_client_id'] && connectionConfig['oauth_client_secret']) {
parsedRawCredentials = {
...parsedRawCredentials,
config_override: {
client_id: connectionConfig['oauth_client_id'],
client_secret: connectionConfig['oauth_client_secret']
}
};

connectionConfig = Object.keys(session.connectionConfig).reduce((acc: Record<string, string>, key: string) => {
if (key !== 'oauth_client_id' && key !== 'oauth_client_secret') {
acc[key] = connectionConfig[key] as string;
}
return acc;
}, {});
}

if (connectionConfig['oauth_scopes']) {
parsedRawCredentials = {
...parsedRawCredentials,
config_override: {
...parsedRawCredentials.config_override,
scopes: Array.isArray(connectionConfig['oauth_scopes']) ? connectionConfig['oauth_scopes'].join(',') : connectionConfig['oauth_scopes']
}
};

connectionConfig = Object.keys(session.connectionConfig).reduce((acc: Record<string, string>, key: string) => {
if (key !== 'oauth_scopes') {
acc[key] = connectionConfig[key] as string;
}
return acc;
}, {});
}

const [updatedConnection] = await connectionService.upsertConnection(
connectionId,
providerConfigKey,
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/lib/models/Auth.ts
Expand Up @@ -129,6 +129,12 @@ export interface OAuth2Credentials extends CredentialsCommon {

refresh_token?: string;
expires_at?: Date | undefined;

config_override?: {
client_id?: string;
client_secret?: string;
scopes?: string;
};
}

export interface OAuth1Credentials extends CredentialsCommon {
Expand Down
50 changes: 28 additions & 22 deletions packages/webapp/src/components/ui/input/TagsInput.tsx
Expand Up @@ -32,6 +32,8 @@ const TagsInput = forwardRef<HTMLInputElement, TagsInputProps>(function TagsInpu

const [scopes, setScopes] = useState(selectedScopes);

const readOnly = props.readOnly || false;

useEffect(() => {
const selectedScopesStr = JSON.stringify(selectedScopes);
const optionalSelectedScopesStr = JSON.stringify(optionalSelectedScopes);
Expand Down Expand Up @@ -88,37 +90,41 @@ const TagsInput = forwardRef<HTMLInputElement, TagsInputProps>(function TagsInpu

return (
<>
<div className="flex gap-3">
<input onInvalid={showInvalid} value={scopes.join(',')} {...props} hidden />
<input
ref={ref}
value={enteredValue}
onChange={(e) => setEnteredValue(e.currentTarget.value)}
onKeyDown={handleEnter}
placeholder={`${scopes.length ? '' : 'Find the list of scopes in the documentation of the external API provider.'}`}
className="border-border-gray bg-active-gray text-white focus:border-white focus:ring-white block w-full appearance-none rounded-md border px-3 py-0.5 text-sm placeholder-gray-400 shadow-sm focus:outline-none"
/>
</div>
{error && <p className="text-red-600 text-sm mt-3">{error}</p>}
{enteredValue !== '' && (
<div
className="flex items-center border border-border-gray bg-active-gray text-white rounded-md px-3 py-0.5 mt-0.5 cursor-pointer"
onClick={handleAdd}
>
<PlusSmallIcon className="h-5 w-5" onClick={handleAdd} />
<span className="">Add new scope: &quot;{enteredValue}&quot;</span>
</div>
{!readOnly && (
<>
<div className="flex gap-3">
<input onInvalid={showInvalid} value={scopes.join(',')} {...props} hidden />
<input
ref={ref}
value={enteredValue}
onChange={(e) => setEnteredValue(e.currentTarget.value)}
onKeyDown={handleEnter}
placeholder={`${scopes.length ? '' : 'Find the list of scopes in the documentation of the external API provider.'}`}
className="border-border-gray bg-active-gray text-white focus:border-white focus:ring-white block w-full appearance-none rounded-md border px-3 py-0.5 text-sm placeholder-gray-400 shadow-sm focus:outline-none"
/>
</div>
{error && <p className="text-red-600 text-sm mt-3">{error}</p>}
{enteredValue !== '' && (
<div
className="flex items-center border border-border-gray bg-active-gray text-white rounded-md px-3 py-0.5 mt-0.5 cursor-pointer"
onClick={handleAdd}
>
<PlusSmallIcon className="h-5 w-5" onClick={handleAdd} />
<span className="">Add new scope: &quot;{enteredValue}&quot;</span>
</div>
)}
</>
)}
{Boolean(scopes.length) && (
<div className="pt-1 mb-3 flex flex-wrap space-x-2">
{scopes.map((selectedScope, i) => {
return (
<span
key={selectedScope + i}
className="flex flex-wrap gap-1 pl-4 pr-2 py-1 mt-0.5 justify-between items-center text-sm font-medium rounded-lg cursor-pointer bg-green-600 bg-opacity-20 text-green-600"
className={`${!readOnly ? 'cursor-pointer ' : ''}flex flex-wrap gap-1 pl-4 pr-2 py-1 mt-0.5 justify-between items-center text-sm font-medium rounded-lg bg-green-600 bg-opacity-20 text-green-600`}
>
{selectedScope}
<X onClick={() => removeScope(selectedScope)} className="h-5 w-5" />
{!readOnly && <X onClick={() => removeScope(selectedScope)} className="h-5 w-5" />}
</span>
);
})}
Expand Down
37 changes: 34 additions & 3 deletions packages/webapp/src/pages/Connection/Authorization.tsx
Expand Up @@ -3,10 +3,12 @@ import { Prism } from '@mantine/prism';
import { Loading } from '@geist-ui/core';

import PrismPlus from '../../components/ui/prism/PrismPlus';
import { Connection, AuthModes } from '../../types';
import { AuthModes } from '../../types';
import type { Connection } from '../../types';
import { formatDateToShortUSFormat } from '../../utils/utils';
import SecretInput from '../../components/ui/input/SecretInput';
import CopyButton from '../../components/ui/button/CopyButton';
import TagsInput from '../../components/ui/input/TagsInput';

interface AuthorizationProps {
connection: Connection | null;
Expand Down Expand Up @@ -47,7 +49,7 @@ export default function Authorization(props: AuthorizationProps) {
<span className="text-gray-400 text-xs uppercase mb-2">Auth Type</span>
<span className="text-white">{connection?.oauthType}</span>
</div>
{connection?.oauthType === AuthModes.ApiKey && (
{connection?.credentials && connection?.oauthType === AuthModes.ApiKey && 'apiKey' in connection.credentials && (
<div className="flex flex-col w-1/2">
<span className="text-gray-400 text-xs uppercase mb-1">{connection?.oauthType}</span>
<SecretInput disabled defaultValue={connection?.credentials?.apiKey} copy={true} />
Expand All @@ -60,7 +62,7 @@ export default function Authorization(props: AuthorizationProps) {
</div>
)}
</div>
{connection?.credentials && connection?.oauthType === AuthModes.Basic && (
{connection?.credentials && connection?.oauthType === AuthModes.Basic && 'password' in connection.credentials && (
<div className="flex">
{connection?.credentials.username && (
<div className="flex flex-col w-1/2">
Expand All @@ -76,6 +78,35 @@ export default function Authorization(props: AuthorizationProps) {
)}
</div>
)}
{connection?.credentials && 'config_override' in connection.credentials && (
<>
{connection?.credentials.config_override.client_id && (
<div className="flex flex-col">
<span className="text-gray-400 text-xs uppercase mb-1">Client ID</span>
<SecretInput disabled value={connection.credentials.config_override.client_id} copy={true} />
</div>
)}
{connection?.credentials.config_override.client_secret && (
<div className="flex flex-col">
<span className="text-gray-400 text-xs uppercase mb-1">Client Secret</span>
<SecretInput disabled value={connection.credentials.config_override.client_secret} copy={true} />
</div>
)}
{connection.credentials.config_override.scopes && (
<div className="mt-8">
<span className="text-gray-400 text-xs uppercase mb-1">Scopes</span>
<TagsInput
id="scopes"
name="scopes"
readOnly
type="text"
defaultValue={connection.credentials.config_override.scopes}
minLength={1}
/>
</div>
)}
</>
)}
{connection?.accessToken && (
<div className="flex flex-col">
<span className="text-gray-400 text-xs uppercase mb-1">Access Token</span>
Expand Down

0 comments on commit 28e70dd

Please sign in to comment.