mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
feat(core): Make OAuth2 error handling consistent with success handling (#5555)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
parent
135b0d3e27
commit
40aacf9279
|
@ -141,6 +141,7 @@
|
||||||
"curlconverter": "^3.0.0",
|
"curlconverter": "^3.0.0",
|
||||||
"dotenv": "^8.0.0",
|
"dotenv": "^8.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-handlebars": "^7.0.2",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
"express-openapi-validator": "^4.13.6",
|
"express-openapi-validator": "^4.13.6",
|
||||||
"express-prom-bundle": "^6.6.0",
|
"express-prom-bundle": "^6.6.0",
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { createHmac } from 'crypto';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import { engine as expressHandlebars } from 'express-handlebars';
|
||||||
import type { ServeStaticOptions } from 'serve-static';
|
import type { ServeStaticOptions } from 'serve-static';
|
||||||
import type { FindManyOptions } from 'typeorm';
|
import type { FindManyOptions } from 'typeorm';
|
||||||
import { Not, In } from 'typeorm';
|
import { Not, In } from 'typeorm';
|
||||||
|
@ -183,6 +184,10 @@ class Server extends AbstractServer {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.app.engine('handlebars', expressHandlebars({ defaultLayout: false }));
|
||||||
|
this.app.set('view engine', 'handlebars');
|
||||||
|
this.app.set('views', TEMPLATES_DIR);
|
||||||
|
|
||||||
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
|
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
|
||||||
this.credentialTypes = Container.get(CredentialTypes);
|
this.credentialTypes = Container.get(CredentialTypes);
|
||||||
this.nodeTypes = Container.get(NodeTypes);
|
this.nodeTypes = Container.get(NodeTypes);
|
||||||
|
|
|
@ -11,7 +11,6 @@ import type {
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
INodeCredentialsDetails,
|
INodeCredentialsDetails,
|
||||||
ICredentialsEncrypted,
|
ICredentialsEncrypted,
|
||||||
IDataObject,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { LoggerProxy } from 'n8n-workflow';
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
import { resolve as pathResolve } from 'path';
|
import { resolve as pathResolve } from 'path';
|
||||||
|
@ -112,7 +111,7 @@ oauth2CredentialController.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
const token = new Csrf();
|
const token = new Csrf();
|
||||||
// Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR
|
// Generate a CSRF prevention token and send it as an OAuth2 state string
|
||||||
const csrfSecret = token.secretSync();
|
const csrfSecret = token.secretSync();
|
||||||
const state = {
|
const state = {
|
||||||
token: token.create(csrfSecret),
|
token: token.create(csrfSecret),
|
||||||
|
@ -174,6 +173,9 @@ oauth2CredentialController.get(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderCallbackError = (res: express.Response, errorMessage: string) =>
|
||||||
|
res.render('oauth-error-callback', { error: { message: errorMessage } });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /oauth2-credential/callback
|
* GET /oauth2-credential/callback
|
||||||
*
|
*
|
||||||
|
@ -188,12 +190,12 @@ oauth2CredentialController.get(
|
||||||
const { code, state: stateEncoded } = req.query;
|
const { code, state: stateEncoded } = req.query;
|
||||||
|
|
||||||
if (!code || !stateEncoded) {
|
if (!code || !stateEncoded) {
|
||||||
const errorResponse = new ResponseHelper.ServiceUnavailableError(
|
return renderCallbackError(
|
||||||
|
res,
|
||||||
`Insufficient parameters for OAuth2 callback. Received following query parameters: ${JSON.stringify(
|
`Insufficient parameters for OAuth2 callback. Received following query parameters: ${JSON.stringify(
|
||||||
req.query,
|
req.query,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let state;
|
let state;
|
||||||
|
@ -203,31 +205,21 @@ oauth2CredentialController.get(
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorResponse = new ResponseHelper.ServiceUnavailableError(
|
return renderCallbackError(res, 'Invalid state format returned');
|
||||||
'Invalid state format returned',
|
|
||||||
);
|
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const credential = await getCredentialWithoutUser(state.cid);
|
const credential = await getCredentialWithoutUser(state.cid);
|
||||||
|
|
||||||
if (!credential) {
|
if (!credential) {
|
||||||
LoggerProxy.error('OAuth2 callback failed because of insufficient permissions', {
|
const errorMessage = 'OAuth2 callback failed because of insufficient permissions';
|
||||||
|
LoggerProxy.error(errorMessage, {
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
credentialId: state.cid,
|
credentialId: state.cid,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.NotFoundError(
|
return renderCallbackError(res, errorMessage);
|
||||||
RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL,
|
|
||||||
);
|
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let encryptionKey: string;
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
try {
|
|
||||||
encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
} catch (error) {
|
|
||||||
throw new ResponseHelper.InternalServerError((error as IDataObject).message as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
const timezone = config.getEnv('generic.timezone');
|
const timezone = config.getEnv('generic.timezone');
|
||||||
|
@ -251,14 +243,12 @@ oauth2CredentialController.get(
|
||||||
decryptedDataOriginal.csrfSecret === undefined ||
|
decryptedDataOriginal.csrfSecret === undefined ||
|
||||||
!token.verify(decryptedDataOriginal.csrfSecret as string, state.token)
|
!token.verify(decryptedDataOriginal.csrfSecret as string, state.token)
|
||||||
) {
|
) {
|
||||||
LoggerProxy.debug('OAuth2 callback state is invalid', {
|
const errorMessage = 'The OAuth2 callback state is invalid!';
|
||||||
|
LoggerProxy.debug(errorMessage, {
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
credentialId: state.cid,
|
credentialId: state.cid,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.NotFoundError(
|
return renderCallbackError(res, errorMessage);
|
||||||
'The OAuth2 callback state is invalid!',
|
|
||||||
);
|
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = {};
|
let options = {};
|
||||||
|
@ -298,12 +288,12 @@ oauth2CredentialController.get(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oauthToken === undefined) {
|
if (oauthToken === undefined) {
|
||||||
LoggerProxy.error('OAuth2 callback failed: unable to get access tokens', {
|
const errorMessage = 'Unable to get OAuth2 access tokens!';
|
||||||
|
LoggerProxy.error(errorMessage, {
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
credentialId: state.cid,
|
credentialId: state.cid,
|
||||||
});
|
});
|
||||||
const errorResponse = new ResponseHelper.NotFoundError('Unable to get access tokens!');
|
return renderCallbackError(res, errorMessage);
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decryptedDataOriginal.oauthTokenData) {
|
if (decryptedDataOriginal.oauthTokenData) {
|
||||||
|
@ -336,9 +326,7 @@ oauth2CredentialController.get(
|
||||||
|
|
||||||
return res.sendFile(pathResolve(TEMPLATES_DIR, 'oauth-callback.html'));
|
return res.sendFile(pathResolve(TEMPLATES_DIR, 'oauth-callback.html'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error response
|
return renderCallbackError(res, (error as Error).message);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
19
packages/cli/templates/oauth-error-callback.handlebars
Normal file
19
packages/cli/templates/oauth-error-callback.handlebars
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>n8n - OAuth Callback</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Open Sans', sans-serif; padding: 10px;}
|
||||||
|
pre.error { background: #f7f7f7; border: 1px solid #ddd; border-radius: 3px; padding: 10px; overflow: auto; overflow-wrap: break-word; white-space: pre-wrap; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{#if error}}
|
||||||
|
<h4>Error:</h4>
|
||||||
|
<pre class='error'>{{error.message}}</pre>
|
||||||
|
{{/if}}
|
||||||
|
Failed to connect. The window can be closed now.
|
||||||
|
<script>
|
||||||
|
(function messageParent() { window.opener.postMessage('error', '*'); })();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -182,6 +182,7 @@ importers:
|
||||||
dotenv: ^8.0.0
|
dotenv: ^8.0.0
|
||||||
express: ^4.18.2
|
express: ^4.18.2
|
||||||
express-async-errors: ^3.1.1
|
express-async-errors: ^3.1.1
|
||||||
|
express-handlebars: ^7.0.2
|
||||||
express-openapi-validator: ^4.13.6
|
express-openapi-validator: ^4.13.6
|
||||||
express-prom-bundle: ^6.6.0
|
express-prom-bundle: ^6.6.0
|
||||||
fast-glob: ^3.2.5
|
fast-glob: ^3.2.5
|
||||||
|
@ -283,6 +284,7 @@ importers:
|
||||||
dotenv: 8.6.0
|
dotenv: 8.6.0
|
||||||
express: 4.18.2
|
express: 4.18.2
|
||||||
express-async-errors: 3.1.1_express@4.18.2
|
express-async-errors: 3.1.1_express@4.18.2
|
||||||
|
express-handlebars: 7.0.2
|
||||||
express-openapi-validator: 4.13.8
|
express-openapi-validator: 4.13.8
|
||||||
express-prom-bundle: 6.6.0_prom-client@13.2.0
|
express-prom-bundle: 6.6.0_prom-client@13.2.0
|
||||||
fast-glob: 3.2.12
|
fast-glob: 3.2.12
|
||||||
|
@ -11095,6 +11097,15 @@ packages:
|
||||||
express: 4.18.2
|
express: 4.18.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/express-handlebars/7.0.2:
|
||||||
|
resolution: {integrity: sha512-W7QuI8i4jjxGNqZJebXzugHuJV32uGTMWqSzqTKsgBTPVsWR3oYR2b9VNQ53vmWbUH1G8r/9GIO3WCqa1Myf1w==}
|
||||||
|
engines: {node: '>=v16'}
|
||||||
|
dependencies:
|
||||||
|
glob: 9.3.1
|
||||||
|
graceful-fs: 4.2.10
|
||||||
|
handlebars: 4.7.7
|
||||||
|
dev: false
|
||||||
|
|
||||||
/express-openapi-validator/4.13.8:
|
/express-openapi-validator/4.13.8:
|
||||||
resolution: {integrity: sha512-89/sdkq+BKBuIyykaMl/vR9grFc3WFUPTjFo0THHbu+5g+q8rA7fKeoMfz+h84yOQIBcztmJ5ZJdk5uhEls31A==}
|
resolution: {integrity: sha512-89/sdkq+BKBuIyykaMl/vR9grFc3WFUPTjFo0THHbu+5g+q8rA7fKeoMfz+h84yOQIBcztmJ5ZJdk5uhEls31A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -12026,6 +12037,16 @@ packages:
|
||||||
minimatch: 5.1.5
|
minimatch: 5.1.5
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
|
||||||
|
/glob/9.3.1:
|
||||||
|
resolution: {integrity: sha512-qERvJb7IGsnkx6YYmaaGvDpf77c951hICMdWaFXyH3PlVob8sbPJJyJX0kWkiCWyXUzoy9UOTNjGg0RbD8bYIw==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
dependencies:
|
||||||
|
fs.realpath: 1.0.0
|
||||||
|
minimatch: 7.4.2
|
||||||
|
minipass: 4.2.5
|
||||||
|
path-scurry: 1.6.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/global-dirs/3.0.0:
|
/global-dirs/3.0.0:
|
||||||
resolution: {integrity: sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==}
|
resolution: {integrity: sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -14968,6 +14989,11 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
|
|
||||||
|
/lru-cache/7.18.3:
|
||||||
|
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lru-memoizer/2.1.4:
|
/lru-memoizer/2.1.4:
|
||||||
resolution: {integrity: sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==}
|
resolution: {integrity: sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -15312,6 +15338,13 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.1
|
brace-expansion: 2.0.1
|
||||||
|
|
||||||
|
/minimatch/7.4.2:
|
||||||
|
resolution: {integrity: sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 2.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/minimist/1.2.7:
|
/minimist/1.2.7:
|
||||||
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
|
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
|
||||||
|
|
||||||
|
@ -15365,11 +15398,9 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
|
|
||||||
/minipass/4.0.0:
|
/minipass/4.2.5:
|
||||||
resolution: {integrity: sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==}
|
resolution: {integrity: sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dependencies:
|
|
||||||
yallist: 4.0.0
|
|
||||||
|
|
||||||
/minizlib/2.1.2:
|
/minizlib/2.1.2:
|
||||||
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||||
|
@ -16597,6 +16628,14 @@ packages:
|
||||||
path-root-regex: 0.1.2
|
path-root-regex: 0.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/path-scurry/1.6.2:
|
||||||
|
resolution: {integrity: sha512-J6MQNh56h6eHFY3vsQ+Lq+zKPwn71POieutmVt2leU8W+zz8HVIdJyn3I3Zs6IKbIQtuKXirVjTBFNBcbFO44Q==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
dependencies:
|
||||||
|
lru-cache: 7.18.3
|
||||||
|
minipass: 4.2.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/path-to-regexp/0.1.7:
|
/path-to-regexp/0.1.7:
|
||||||
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
||||||
|
|
||||||
|
@ -19523,7 +19562,7 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
chownr: 2.0.0
|
chownr: 2.0.0
|
||||||
fs-minipass: 2.1.0
|
fs-minipass: 2.1.0
|
||||||
minipass: 4.0.0
|
minipass: 4.2.5
|
||||||
minizlib: 2.1.2
|
minizlib: 2.1.2
|
||||||
mkdirp: 1.0.4
|
mkdirp: 1.0.4
|
||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
|
|
Loading…
Reference in a new issue