fix(core): Improve the security on OAuth callback endpoints (#11593)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-11-19 16:17:20 +01:00
committed by GitHub
parent c265d44841
commit 274fcf45d3
9 changed files with 285 additions and 173 deletions

View File

@@ -8,11 +8,11 @@ import { jsonStringify } from 'n8n-workflow';
import pkceChallenge from 'pkce-challenge';
import * as qs from 'querystring';
import { GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE as GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE } from '@/constants';
import { Get, RestController } from '@/decorators';
import { OAuthRequest } from '@/requests';
import { AbstractOAuthController, type CsrfStateParam } from './abstract-oauth.controller';
import { GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE as GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE } from '../../constants';
import { AbstractOAuthController, skipAuthOnOAuthCallback } from './abstract-oauth.controller';
@RestController('/oauth2-credential')
export class OAuth2CredentialController extends AbstractOAuthController {
@@ -44,7 +44,10 @@ export class OAuth2CredentialController extends AbstractOAuthController {
);
// Generate a CSRF prevention token and send it as an OAuth2 state string
const [csrfSecret, state] = this.createCsrfState(credential.id);
const [csrfSecret, state] = this.createCsrfState(
credential.id,
skipAuthOnOAuthCallback ? undefined : req.user.id,
);
const oAuthOptions = {
...this.convertCredentialToOptions(oauthCredentials),
@@ -82,7 +85,7 @@ export class OAuth2CredentialController extends AbstractOAuthController {
}
/** Verify and store app code. Generate access tokens and store for respective credential */
@Get('/callback', { usesTemplates: true, skipAuth: true })
@Get('/callback', { usesTemplates: true, skipAuth: skipAuthOnOAuthCallback })
async handleCallback(req: OAuthRequest.OAuth2Credential.Callback, res: Response) {
try {
const { code, state: encodedState } = req.query;
@@ -94,34 +97,8 @@ export class OAuth2CredentialController extends AbstractOAuthController {
);
}
let state: CsrfStateParam;
try {
state = this.decodeCsrfState(encodedState);
} catch (error) {
return this.renderCallbackError(res, (error as Error).message);
}
const credentialId = state.cid;
const credential = await this.getCredentialWithoutUser(credentialId);
if (!credential) {
const errorMessage = 'OAuth2 callback failed because of insufficient permissions';
this.logger.error(errorMessage, { credentialId });
return this.renderCallbackError(res, errorMessage);
}
const additionalData = await this.getAdditionalData();
const decryptedDataOriginal = await this.getDecryptedData(credential, additionalData);
const oauthCredentials = this.applyDefaultsAndOverwrites<OAuth2CredentialData>(
credential,
decryptedDataOriginal,
additionalData,
);
if (this.verifyCsrfState(decryptedDataOriginal, state)) {
const errorMessage = 'The OAuth2 callback state is invalid!';
this.logger.debug(errorMessage, { credentialId });
return this.renderCallbackError(res, errorMessage);
}
const [credential, decryptedDataOriginal, oauthCredentials] =
await this.resolveCredential<OAuth2CredentialData>(req);
let options: Partial<ClientOAuth2Options> = {};
@@ -156,12 +133,6 @@ export class OAuth2CredentialController extends AbstractOAuthController {
set(oauthToken.data, 'callbackQueryString', omit(req.query, 'state', 'code'));
}
if (oauthToken === undefined) {
const errorMessage = 'Unable to get OAuth2 access tokens!';
this.logger.error(errorMessage, { credentialId });
return this.renderCallbackError(res, errorMessage);
}
if (decryptedDataOriginal.oauthTokenData) {
// Only overwrite supplied data as some providers do for example just return the
// refresh_token on the very first request and not on subsequent ones.
@@ -175,7 +146,7 @@ export class OAuth2CredentialController extends AbstractOAuthController {
await this.encryptAndSaveData(credential, decryptedDataOriginal);
this.logger.debug('OAuth2 callback successful for credential', {
credentialId,
credentialId: credential.id,
});
return res.render('oauth-callback');