Skip to content

Commit f2deb0f

Browse files
committed
feature #2022 [LiveComponent] Update CSRF token after component request (tijnema)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [LiveComponent] Update CSRF token after component request | Q | A | ------------- | --- | Bug fix? | yes | New feature? | yes | Issues | | License | MIT When CSRF is enabled on live components, each request will received a new CSRF token in the response. Currently all subsequent requests made by live components will use the initial CSRF token received. This PR solves this, by updating the CSRF token with the one received in the last response. Commits ------- 52ca1c9 [LiveComponent] Update CSRF token after component request
2 parents e22484b + 52ca1c9 commit f2deb0f

File tree

8 files changed

+53
-2
lines changed

8 files changed

+53
-2
lines changed

src/LiveComponent/assets/dist/Backend/Backend.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface BackendInterface {
1313
}, files: {
1414
[key: string]: FileList;
1515
}): BackendRequest;
16+
updateCsrfToken(csrfToken: string): void;
1617
}
1718
export interface BackendAction {
1819
name: string;
@@ -28,4 +29,5 @@ export default class implements BackendInterface {
2829
}, files: {
2930
[key: string]: FileList;
3031
}): BackendRequest;
32+
updateCsrfToken(csrfToken: string): void;
3133
}

src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { BackendAction, ChildrenFingerprints } from './Backend';
22
export default class {
33
private url;
44
private method;
5-
private readonly csrfToken;
5+
private csrfToken;
66
constructor(url: string, method?: 'get' | 'post', csrfToken?: string | null);
77
buildRequest(props: any, actions: BackendAction[], updated: {
88
[key: string]: any;
@@ -15,4 +15,5 @@ export default class {
1515
fetchOptions: RequestInit;
1616
};
1717
private willDataFitInUrl;
18+
updateCsrfToken(csrfToken: string): void;
1819
}

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2052,6 +2052,9 @@ class Component {
20522052
return response;
20532053
}
20542054
this.processRerender(html, backendResponse);
2055+
if (this.element.dataset.liveCsrfValue) {
2056+
this.backend.updateCsrfToken(this.element.dataset.liveCsrfValue);
2057+
}
20552058
this.backendRequest = null;
20562059
thisPromiseResolve(backendResponse);
20572060
if (this.isRequestPending) {
@@ -2325,6 +2328,9 @@ class RequestBuilder {
23252328
const urlEncodedJsonData = new URLSearchParams(propsJson + updatedJson + childrenJson + propsFromParentJson).toString();
23262329
return (urlEncodedJsonData + params.toString()).length < 1500;
23272330
}
2331+
updateCsrfToken(csrfToken) {
2332+
this.csrfToken = csrfToken;
2333+
}
23282334
}
23292335

23302336
class Backend {
@@ -2335,6 +2341,9 @@ class Backend {
23352341
const { url, fetchOptions } = this.requestBuilder.buildRequest(props, actions, updated, children, updatedPropsFromParent, files);
23362342
return new BackendRequest(fetch(url, fetchOptions), actions.map((backendAction) => backendAction.name), Object.keys(updated));
23372343
}
2344+
updateCsrfToken(csrfToken) {
2345+
this.requestBuilder.updateCsrfToken(csrfToken);
2346+
}
23382347
}
23392348

23402349
class StimulusElementDriver {

src/LiveComponent/assets/src/Backend/Backend.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface BackendInterface {
1515
updatedPropsFromParent: { [key: string]: any },
1616
files: { [key: string]: FileList }
1717
): BackendRequest;
18+
updateCsrfToken(csrfToken: string): void;
1819
}
1920

2021
export interface BackendAction {
@@ -52,4 +53,8 @@ export default class implements BackendInterface {
5253
Object.keys(updated)
5354
);
5455
}
56+
57+
updateCsrfToken(csrfToken: string) {
58+
this.requestBuilder.updateCsrfToken(csrfToken);
59+
}
5560
}

src/LiveComponent/assets/src/Backend/RequestBuilder.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { BackendAction, ChildrenFingerprints } from './Backend';
33
export default class {
44
private url: string;
55
private method: 'get' | 'post';
6-
private readonly csrfToken: string | null;
6+
private csrfToken: string | null;
77

88
constructor(url: string, method: 'get' | 'post' = 'post', csrfToken: string | null = null) {
99
this.url = url;
@@ -117,4 +117,8 @@ export default class {
117117
// if the URL gets remotely close to 2000 chars, it may not fit
118118
return (urlEncodedJsonData + params.toString()).length < 1500;
119119
}
120+
121+
updateCsrfToken(csrfToken: string) {
122+
this.csrfToken = csrfToken;
123+
}
120124
}

src/LiveComponent/assets/src/Component/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,11 @@ export default class Component {
329329

330330
this.processRerender(html, backendResponse);
331331

332+
// Store updated csrf token
333+
if (this.element.dataset.liveCsrfValue) {
334+
this.backend.updateCsrfToken(this.element.dataset.liveCsrfValue);
335+
}
336+
332337
// finally resolve this promise
333338
this.backendRequest = null;
334339
thisPromiseResolve(backendResponse);

src/LiveComponent/assets/test/controller/render.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,4 +630,23 @@ describe('LiveController rendering Tests', () => {
630630
// verify the selectedIndex of the select option 2 is 0
631631
expect(selectOption2.selectedIndex).toBe(0);
632632
});
633+
634+
it('backend will have a new csrf token', async () => {
635+
const test = await createTest(
636+
{},
637+
(data: any) => `
638+
<div ${initComponent(data)} data-live-csrf-value="${data.csrf}">
639+
</div>
640+
`
641+
);
642+
643+
test.expectsAjaxCall().serverWillChangeProps((data: any) => {
644+
// change csrf token
645+
data.csrf = 'Hello';
646+
});
647+
648+
await test.component.render();
649+
650+
expect(test.mockedBackend.csrfToken).toEqual('Hello');
651+
});
633652
});

src/LiveComponent/assets/test/tools.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ class FunctionalTest {
9898
class MockedBackend implements BackendInterface {
9999
private expectedMockedAjaxCalls: Array<MockedAjaxCall> = [];
100100

101+
public csrfToken: string | null = null;
102+
101103
addMockedAjaxCall(mock: MockedAjaxCall) {
102104
this.expectedMockedAjaxCalls.push(mock);
103105
}
@@ -139,6 +141,10 @@ class MockedBackend implements BackendInterface {
139141
return matchedMock.createBackendRequest();
140142
}
141143

144+
updateCsrfToken(csrfToken: string) {
145+
this.csrfToken = csrfToken;
146+
}
147+
142148
getExpectedMockedAjaxCalls(): Array<MockedAjaxCall> {
143149
return this.expectedMockedAjaxCalls;
144150
}

0 commit comments

Comments
 (0)