Skip to content

Commit

Permalink
feat(core): Add PKCE for OAuth2 (n8n-io#6324)
Browse files Browse the repository at this point in the history
* Remove authorization header when empty

* Import pkce

* Add OAuth2 with new grant type to Twitter

* Add pkce logic auto assign authorization code if pkce not defined

* Add pkce to ui and interfaces

* Fix scopes for Oauth2 twitter

* Deubg + pass it through header

* Add debug console, add airtable cred

* Remove all console.logs, make PKCE in th body only when it exists

* Remove invalid character ~

* Remove more console.logs

* remove body inside query

* Remove useless grantype check

* Hide oauth2 twitter waiting for overhaul

* Remove redundant header removal

* Remove more console.logs

* Add comment for code verifier

* Remove uneeded scopes

* Restore client id in callback

* Revert "Add OAuth2 with new grant type to Twitter"

This reverts commit 1c3b331.

* Remove oauth2 from twitter

* Remove properties linked to oauth2

* Fix lodash imports

* remove redundant check

* remove redundant codeVerifier

* patch pkce-challenge to avoid generating `code_verifier` with `~`

* store `codeVerifier` on the DB like `csrfSecret`

* remove unrelated changes

---------

Co-authored-by: Marcus <[email protected]>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <[email protected]>
  • Loading branch information
3 people authored Jun 21, 2023
1 parent 4b0e0b7 commit fc7261a
Show file tree
Hide file tree
Showing 12 changed files with 130 additions and 12 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@
"patchedDependencies": {
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]",
"@sentry/[email protected]": "patches/@[email protected]"
"@sentry/[email protected]": "patches/@[email protected]",
"[email protected]": "patches/[email protected]"
}
}
}
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"passport-cookie": "^1.0.9",
"passport-jwt": "^4.0.0",
"pg": "^8.8.0",
"pkce-challenge": "^3.0.0",
"picocolors": "^1.0.0",
"posthog-node": "^2.2.2",
"prom-client": "^13.1.0",
Expand Down
25 changes: 22 additions & 3 deletions packages/cli/src/credentials/oauth2Credential.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ClientOAuth2Options } from '@n8n/client-oauth2';
import { ClientOAuth2 } from '@n8n/client-oauth2';
import Csrf from 'csrf';
import express from 'express';
import pkceChallenge from 'pkce-challenge';
import get from 'lodash/get';
import omit from 'lodash/omit';
import set from 'lodash/set';
Expand Down Expand Up @@ -142,6 +143,16 @@ oauth2CredentialController.get(
);
decryptedDataOriginal.csrfSecret = csrfSecret;

if (oauthCredentials.grantType === 'pkce') {
const { code_verifier, code_challenge } = pkceChallenge();
oAuthOptions.query = {
...oAuthOptions.query,
code_challenge,
code_challenge_method: 'S256',
};
decryptedDataOriginal.codeVerifier = code_verifier;
}

credentials.setData(decryptedDataOriginal, encryptionKey);
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;

Expand Down Expand Up @@ -189,7 +200,6 @@ oauth2CredentialController.get(
try {
// realmId it's currently just use for the quickbook OAuth2 flow
const { code, state: stateEncoded } = req.query;

if (!code || !stateEncoded) {
return renderCallbackError(
res,
Expand Down Expand Up @@ -265,12 +275,21 @@ oauth2CredentialController.get(
if ((get(oauthCredentials, 'authentication', 'header') as string) === 'body') {
options = {
body: {
client_id: get(oauthCredentials, 'clientId') as string,
client_secret: get(oauthCredentials, 'clientSecret', '') as string,
...(oauthCredentials.grantType === 'pkce' && {
code_verifier: decryptedDataOriginal.codeVerifier,
}),
...(oauthCredentials.grantType === 'authorizationCode' && {
client_id: get(oauthCredentials, 'clientId') as string,
client_secret: get(oauthCredentials, 'clientSecret', '') as string,
}),
},
};
// @ts-ignore
delete oAuth2Parameters.clientSecret;
} else if (oauthCredentials.grantType === 'pkce') {
options = {
body: { code_verifier: decryptedDataOriginal.codeVerifier },
};
}

await Container.get(ExternalHooks).run('oauth2.callback', [oAuth2Parameters]);
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/NodeExecuteFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,14 +1104,12 @@ export async function requestOAuth2(
});

let oauthTokenData = credentials.oauthTokenData as ClientOAuth2TokenData;

// if it's the first time using the credentials, get the access token and save it into the DB.
if (
credentials.grantType === OAuth2GrantType.clientCredentials &&
(oauthTokenData === undefined || Object.keys(oauthTokenData).length === 0)
) {
const { data } = await getClientCredentialsToken(oAuthClient, credentials);

// Find the credentials
if (!node.credentials?.[credentialsType]) {
throw new Error(
Expand Down Expand Up @@ -1150,7 +1148,6 @@ export async function requestOAuth2(
if (oAuth2Options?.keepBearer === false && typeof newRequestHeaders.Authorization === 'string') {
newRequestHeaders.Authorization = newRequestHeaders.Authorization.split(' ')[1];
}

if (oAuth2Options?.keyToIncludeInAccessTokenHeader) {
Object.assign(newRequestHeaders, {
[oAuth2Options.keyToIncludeInAccessTokenHeader]: token.accessToken,
Expand All @@ -1166,7 +1163,9 @@ export async function requestOAuth2(
if (oAuth2Options?.includeCredentialsOnRefreshOnBody) {
const body: IDataObject = {
client_id: credentials.clientId as string,
client_secret: credentials.clientSecret as string,
...(credentials.grantType === 'authorizationCode' && {
client_secret: credentials.clientSecret as string,
}),
};
tokenRefreshOptions.body = body;
tokenRefreshOptions.headers = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,8 @@ export default defineComponent({
return (
!!this.credentialTypeName &&
(((this.credentialTypeName === 'oAuth2Api' || this.parentTypes.includes('oAuth2Api')) &&
this.credentialData.grantType === 'authorizationCode') ||
(this.credentialData.grantType === 'authorizationCode' ||
this.credentialData.grantType === 'pkce')) ||
this.credentialTypeName === 'oAuth1Api' ||
this.parentTypes.includes('oAuth1Api'))
);
Expand Down
52 changes: 52 additions & 0 deletions packages/nodes-base/credentials/AirtableOAuth2Api.credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';

const scopes = ['schema.bases:read', 'data.records:read', 'data.records:write'];

export class AirtableOAuth2Api implements ICredentialType {
name = 'airtableOAuth2Api';

extends = ['oAuth2Api'];

displayName = 'Airtable OAuth2 API';

documentationUrl = 'airtable';

properties: INodeProperties[] = [
{
displayName: 'Grant Type',
name: 'grantType',
type: 'hidden',
default: 'pkce',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden',
default: 'https://airtable.com/oauth2/v1/authorize',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden',
default: 'https://airtable.com/oauth2/v1/token',
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: `${scopes.join(' ')}`,
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden',
default: 'header',
},
];
}
8 changes: 6 additions & 2 deletions packages/nodes-base/credentials/OAuth2Api.credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export class OAuth2Api implements ICredentialType {
name: 'Client Credentials',
value: 'clientCredentials',
},
{
name: 'PKCE',
value: 'pkce',
},
],
default: 'authorizationCode',
},
Expand All @@ -32,7 +36,7 @@ export class OAuth2Api implements ICredentialType {
type: 'string',
displayOptions: {
show: {
grantType: ['authorizationCode'],
grantType: ['authorizationCode', 'pkce'],
},
},
default: '',
Expand Down Expand Up @@ -74,7 +78,7 @@ export class OAuth2Api implements ICredentialType {
type: 'string',
displayOptions: {
show: {
grantType: ['authorizationCode'],
grantType: ['authorizationCode', 'pkce'],
},
},
default: '',
Expand Down
13 changes: 13 additions & 0 deletions packages/nodes-base/nodes/Airtable/Airtable.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ export class Airtable implements INodeType {
},
},
},
{
name: 'airtableOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['airtableOAuth2Api'],
},
},
},
],
properties: [
{
Expand All @@ -57,6 +66,10 @@ export class Airtable implements INodeType {
name: 'Access Token',
value: 'airtableTokenApi',
},
{
name: 'OAuth2',
value: 'airtableOAuth2Api',
},
],
default: 'airtableApi',
},
Expand Down
1 change: 1 addition & 0 deletions packages/nodes-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dist/credentials/AffinityApi.credentials.js",
"dist/credentials/AgileCrmApi.credentials.js",
"dist/credentials/AirtableApi.credentials.js",
"dist/credentials/AirtableOAuth2Api.credentials.js",
"dist/credentials/AirtableTokenApi.credentials.js",
"dist/credentials/Amqp.credentials.js",
"dist/credentials/ApiTemplateIoApi.credentials.js",
Expand Down
3 changes: 2 additions & 1 deletion packages/workflow/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1907,11 +1907,12 @@ export interface IConnectedNode {
}

export const enum OAuth2GrantType {
pkce = 'pkce',
authorizationCode = 'authorizationCode',
clientCredentials = 'clientCredentials',
}
export interface IOAuth2Credentials {
grantType: 'authorizationCode' | 'clientCredentials';
grantType: 'authorizationCode' | 'clientCredentials' | 'pkce';
clientId: string;
clientSecret: string;
accessTokenUrl: string;
Expand Down
13 changes: 13 additions & 0 deletions patches/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
diff --git a/dist/main.js b/dist/main.js
index 86be84f44210b26583e0a7f1732acd8b98a5e701..a2b05be6a45355704fedf43b51a34793580eaf6c 100644
--- a/dist/main.js
+++ b/dist/main.js
@@ -42,7 +42,7 @@ $parcel$export(module.exports, "verifyChallenge", () => $f5bfd4ce37214f4f$export
* @param size The desired length of the string
* @returns The random string
*/ function $f5bfd4ce37214f4f$var$random(size) {
- const mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
+ const mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._";
let result = "";
const randomUints = $f5bfd4ce37214f4f$var$getRandomValues(size);
for(let i = 0; i < size; i++){
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit fc7261a

Please sign in to comment.