Skip to content

Commit a197626

Browse files
authored
feat: add cron job to disable xsrf token validation on subscribed tenants automatically (#107)
1 parent c03b5ed commit a197626

File tree

8 files changed

+1504
-1413
lines changed

8 files changed

+1504
-1413
lines changed

README.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,15 @@ A sample usecase might be to access a configuration UI on your device or to e.g.
88

99
## Prerequisits & Limitations
1010

11-
This functionality is heavily relying on the [Cloud Remote Access feature of Cumulocity](https://cumulocity.com/guides/cloud-remote-access/cra-general-aspects/).
11+
This functionality is heavily relying on the [Cloud Remote Access feature of Cumulocity](https://cumulocity.com/docs/cloud-remote-access/cra-general-aspects/).
1212

1313
- The shell application that you are installing the UI plugin to should use at least version 1017+ of the Web SDK.
1414

1515
- **PASSTHROUGH endpoint:** To use the proxy you need a remote access `PASSTHROUGH` endpoint configured on each of your devices you want to connect to. See [this guide](https://tech.forums.softwareag.com/t/how-to-get-started-with-cloud-remote-access-for-cumulocity-iot/258446#step-by-step-guide-to-setup-a-passthrough-connection-16) for further details.
1616

17-
- **Tenant Authentication Method:** The desired tenant must be configured to use the [`OAI Secure` Authentication](https://cumulocity.com/guides/users-guide/administration/#authentication)
17+
- **Tenant Authentication Method:** The desired tenant must be configured to use the [`OAI Secure` Authentication](https://cumulocity.com/docs/authentication/basic-settings/#login-settings)
1818

19-
- **Disable XSRF-Token validation**: The XSRF-Token validation of Cumulocity needs to be disabled for the tenant. Please check on your own if this might be a security concern for you: [Cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery). To do this, the corresponding tenant option (category: `jwt`, key: `xsrf-validation.enabled`) must be set to `false`.
20-
21-
**Note**
22-
23-
If you're using [go-c8y-cli](https://goc8ycli.netlify.app/), then you can set the tenant option using the following command:
24-
25-
```
26-
c8y tenantoptions update --category jwt --key xsrf-validation.enabled --value false
27-
```
19+
- **Disable XSRF-Token validation**: The XSRF-Token validation of Cumulocity needs to be disabled for the tenant. Please check on your own if this might be a security concern for you: [Cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery). To do this, the corresponding tenant option (category: `jwt`, key: `xsrf-validation.enabled`) must be set to `false`. The microservice will take care of disabling this on subscribed tenants automatically.
2820

2921
- Requests through the proxy can be made via both HTTP and HTTPS. For requests made to an HTTPS server, the certificate of the server is not actually validated.
3022

@@ -48,7 +40,7 @@ It's functionality can be described in the following steps:
4840
The UI plugin adds tabs on device level to the application it has been installed to.
4941
The UI detects remote access connections available for the device that have been prefixed with `http:` and use the `PASSTHROUGH` protocol.
5042

51-
For instructions on how to install an UI plugin, please check [here](https://cumulocity.com/guides/users-guide/administration/#extensions).
43+
For instructions on how to install an UI plugin, please check [here](https://cumulocity.com/docs/standard-tenant/ecosystem/#extensions).
5244

5345
### Configuring a new connection
5446

backend/cumulocity.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
},
1010
"isolation": "MULTI_TENANT",
1111
"roles": [],
12-
"requiredRoles": [],
12+
"requiredRoles": [
13+
"ROLE_OPTION_MANAGEMENT_READ",
14+
"ROLE_OPTION_MANAGEMENT_ADMIN"
15+
],
1316
"livenessProbe": {
1417
"httpGet": {
1518
"path": "/health",

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"typescript": "~5.5.2"
2424
},
2525
"dependencies": {
26-
"@c8y/client": "1021.3.1",
26+
"@c8y/client": "1021.54.6",
2727
"agentkeepalive": "^4.5.0",
2828
"cookie-parse": "^0.4.0",
2929
"cron": "^3.2.1",

backend/src/index.ts

Lines changed: 108 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { RCAServerStore } from "./rca-server-store";
1111
import Agent from "agentkeepalive";
1212
import { HttpsAgent } from "agentkeepalive";
1313
import * as http from "http";
14+
import { BasicAuth, Client, ICredentials } from "@c8y/client";
1415

1516
dotenv.config();
1617

@@ -25,19 +26,91 @@ const serverStore = new RCAServerStore(logger);
2526
const agentOptions: Agent.HttpOptions = {
2627
timeout: 60_000, // active socket keepalive for 60 seconds
2728
freeSocketTimeout: 30_000, // free socket keepalive for 30 seconds
28-
}
29+
};
2930
const agents = {
3031
http: new Agent({
31-
...agentOptions
32+
...agentOptions,
3233
}),
3334
https: new HttpsAgent({
34-
...agentOptions
35-
})
36-
}
35+
...agentOptions,
36+
}),
37+
};
3738

3839
logger.debug(JSON.stringify(process.env));
3940
const app = express();
4041

42+
const tenantIdsWhereXSRFTokenValidationHasBeenDisabled = new Array<string>();
43+
async function disableXSRFTokenValidation() {
44+
let subscriptions = new Array<ICredentials>();
45+
try {
46+
subscriptions = await Client.getMicroserviceSubscriptions(
47+
{
48+
tenant: process.env.C8Y_BOOTSTRAP_TENANT,
49+
user: process.env.C8Y_BOOTSTRAP_USER,
50+
password: process.env.C8Y_BOOTSTRAP_PASSWORD,
51+
},
52+
process.env.C8Y_BASEURL
53+
);
54+
} catch (e) {
55+
logger.error("Failed to get subscriptions", { e });
56+
return;
57+
}
58+
59+
for (const subscription of subscriptions) {
60+
const { tenant } = subscription;
61+
try {
62+
if (tenantIdsWhereXSRFTokenValidationHasBeenDisabled.includes(tenant)) {
63+
logger.debug(
64+
`XSRF token validation already disabled for tenant ${tenant}`
65+
);
66+
continue;
67+
}
68+
69+
logger.debug(`Disabling XSRF token validation for tenant ${tenant}`);
70+
const client = new Client(
71+
new BasicAuth(subscription),
72+
process.env.C8Y_BASEURL
73+
);
74+
const category = "jwt";
75+
const key = "xsrf-validation.enabled";
76+
77+
const {
78+
data: { value },
79+
} = await client.options.tenant.detail({
80+
category,
81+
key,
82+
});
83+
if (value === "false" || <any>value === false) {
84+
logger.info(`XSRF token validation already disabled for tenant ${tenant}`);
85+
tenantIdsWhereXSRFTokenValidationHasBeenDisabled.push(tenant);
86+
continue;
87+
}
88+
await client.options.tenant.update({
89+
category: "jwt",
90+
key: "xsrf-validation.enabled",
91+
value: "false",
92+
});
93+
94+
logger.info(`Disabled XSRF token validation for tenant ${tenant}`);
95+
tenantIdsWhereXSRFTokenValidationHasBeenDisabled.push(tenant);
96+
} catch (e) {
97+
logger.warn(
98+
`Failed to disable XSRF token validation for tenant ${tenant}`,
99+
{ e }
100+
);
101+
}
102+
}
103+
}
104+
105+
CronJob.from({
106+
cronTime: "0 */5 * * * *",
107+
onTick: async () => {
108+
await disableXSRFTokenValidation();
109+
},
110+
start: true,
111+
runOnInit: true,
112+
});
113+
41114
app.get("/health", (req, res) => {
42115
res.status(200).json({
43116
memory: process.memoryUsage(),
@@ -47,10 +120,13 @@ app.get("/health", (req, res) => {
47120
});
48121

49122
app.use((req, res, next) => {
50-
if (req.headers.authorization || req.headers.cookie?.includes('authorization')) {
123+
if (
124+
req.headers.authorization ||
125+
req.headers.cookie?.includes("authorization")
126+
) {
51127
return next();
52128
}
53-
res.setHeader('WWW-Authenticate', 'Basic realm="My Realm"')
129+
res.setHeader("WWW-Authenticate", 'Basic realm="My Realm"');
54130
res.status(401).send();
55131
});
56132

@@ -116,18 +192,28 @@ function getRewriteOptions(
116192
changeOrigin: !hasCustomHost,
117193
protocolRewrite: (req.headers["x-forwarded-proto"] as string) || "http",
118194
cookieDomainRewrite:
119-
typeof forwardedHost === "string" ? `.${forwardedHost.replace(/:.*$/, '')}` : undefined,
120-
cookiePathRewrite: `/service/cloud-http-proxy${secure ? '/s' : ''}/${req.params.cloudProxyDeviceId}/${req.params.cloudProxyConfigId}/`,
195+
typeof forwardedHost === "string"
196+
? `.${forwardedHost.replace(/:.*$/, "")}`
197+
: undefined,
198+
cookiePathRewrite: `/service/cloud-http-proxy${secure ? "/s" : ""}/${
199+
req.params.cloudProxyDeviceId
200+
}/${req.params.cloudProxyConfigId}/`,
121201
};
122202
}
123203

124-
function hasCustomHostHeader(req: express.Request, deviceId: string, configId: string) {
204+
function hasCustomHostHeader(
205+
req: express.Request,
206+
deviceId: string,
207+
configId: string
208+
) {
125209
const headerToLookoutFor = `rca-http-header-host-${deviceId}-${configId}`;
126210
return !!req.headers[headerToLookoutFor];
127211
}
128212

129-
function prefixCookiesToBeSet(proxy: Server<http.IncomingMessage, http.ServerResponse<http.IncomingMessage>>) {
130-
proxy.on('proxyRes', (response) => {
213+
function prefixCookiesToBeSet(
214+
proxy: Server<http.IncomingMessage, http.ServerResponse<http.IncomingMessage>>
215+
) {
216+
proxy.on("proxyRes", (response) => {
131217
const cookiesToSet = response.headers["set-cookie"];
132218
if (!cookiesToSet?.length) {
133219
return;
@@ -149,7 +235,11 @@ app.use("/s/:cloudProxyDeviceId/:cloudProxyConfigId/", async (req, res) => {
149235
});
150236

151237
try {
152-
const hasCustomHost = hasCustomHostHeader(req, req.params.cloudProxyDeviceId, req.params.cloudProxyConfigId);
238+
const hasCustomHost = hasCustomHostHeader(
239+
req,
240+
req.params.cloudProxyDeviceId,
241+
req.params.cloudProxyConfigId
242+
);
153243
const target = await getTarget(req, requestLogger, true);
154244
const rewriteOptions = getRewriteOptions(req, true, hasCustomHost);
155245
const proxy = createProxyServer({
@@ -196,7 +286,11 @@ app.use("/:cloudProxyDeviceId/:cloudProxyConfigId/", async (req, res) => {
196286
});
197287

198288
try {
199-
const hasCustomHost = hasCustomHostHeader(req, req.params.cloudProxyDeviceId, req.params.cloudProxyConfigId);
289+
const hasCustomHost = hasCustomHostHeader(
290+
req,
291+
req.params.cloudProxyDeviceId,
292+
req.params.cloudProxyConfigId
293+
);
200294
const target = await getTarget(req, requestLogger);
201295
const rewriteOptions = getRewriteOptions(req, true, hasCustomHost);
202296
const proxy = createProxyServer({
@@ -254,4 +348,3 @@ if (!process.env.NO_STATISTICS) {
254348
start: true,
255349
});
256350
}
257-

frontend/cumulocity.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export default {
3939
'@angular/platform-browser',
4040
'@angular/platform-browser-dynamic',
4141
'@angular/router',
42-
'@angular/upgrade',
4342
'@c8y/client',
4443
'@c8y/ngx-components',
4544
'ngx-bootstrap',

frontend/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@
3131
"@angular/platform-browser": "^18.2.0",
3232
"@angular/platform-browser-dynamic": "^18.2.0",
3333
"@angular/router": "^18.2.0",
34-
"@c8y/bootstrap": "1021.3.1",
35-
"@c8y/client": "1021.3.1",
36-
"@c8y/ngx-components": "1021.3.1",
37-
"@c8y/style": "1021.3.1",
34+
"@c8y/bootstrap": "1021.54.6",
35+
"@c8y/client": "1021.54.6",
36+
"@c8y/ngx-components": "1021.54.6",
37+
"@c8y/style": "1021.54.6",
3838
"ngx-bootstrap": "18.0.0",
3939
"rxjs": "^7.8.1",
4040
"tslib": "^2.3.0",
@@ -44,8 +44,8 @@
4444
"@angular-devkit/build-angular": "^18.2.10",
4545
"@angular/cli": "^18.2.10",
4646
"@angular/compiler-cli": "^18.2.0",
47-
"@c8y/devkit": "1021.3.1",
48-
"@c8y/options": "1021.3.1",
47+
"@c8y/devkit": "1021.54.6",
48+
"@c8y/options": "1021.54.6",
4949
"@types/jasmine": "~4.3.0",
5050
"jasmine-core": "~5.2.0",
5151
"karma": "~6.4.0",

images/demo-node-red.png

-170 KB
Loading

0 commit comments

Comments
 (0)