Skip to content

Commit d4b57d2

Browse files
committed
feat: add setRequestToken and fetchRequestToken methods
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 3baf2ea commit d4b57d2

12 files changed

Lines changed: 936 additions & 90 deletions

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ logs
66
npm-debug.log*
77
yarn-debug.log*
88
yarn-error.log*
9-
109
# Runtime data
1110
pids
1211
*.pid
1312
*.seed
1413
*.pid.lock
1514

15+
# Tests
16+
.vitest*
17+
__screenshots__/
18+
1619
# Directory for instrumented libs generated by jscoverage/JSCover
1720
lib-cov
1821

lib/globals.d.ts renamed to globals.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
*/
55

66
declare global {
7+
// eslint-disable-next-line camelcase
8+
var _nc_auth_requesttoken: string | undefined
9+
710
interface Window {
811
_oc_isadmin?: boolean
912
}

lib/eventbus.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { NextcloudUser } from './user.ts'
77

88
declare module '@nextcloud/event-bus' {
99
export interface NextcloudEvents {
10+
'csrf-token-update': { token: string, _internal?: true }
1011
// mapping of 'event name' => 'event type'
1112
'user:info:changed': NextcloudUser
1213
}

lib/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ export type { NextcloudUser } from './user.ts'
88

99
export { getCSPNonce } from './csp-nonce.ts'
1010
export { getGuestNickname, getGuestUser, setGuestNickname } from './guest.ts'
11-
export { getRequestToken, onRequestTokenUpdate } from './requesttoken.ts'
11+
export { fetchRequestToken, getRequestToken, onRequestTokenUpdate, setRequestToken } from './requesttoken.ts'
1212
export { getCurrentUser } from './user.ts'

lib/requesttoken.ts

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,113 @@
33
* SPDX-License-Identifier: GPL-3.0-or-later
44
*/
55

6-
import { subscribe } from '@nextcloud/event-bus'
6+
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
7+
import { generateUrl } from '@nextcloud/router'
78

89
export interface CsrfTokenObserver {
910
(token: string): void
1011
}
1112

12-
let token: string | null | undefined
13-
const observers: CsrfTokenObserver[] = []
13+
_subscribeToTokenUpdates() // TODO: remove once we drop support for Nextcloud 33 and before
1414

1515
/**
1616
* Get current request token
1717
*
1818
* @return Current request token or null if not set
1919
*/
2020
export function getRequestToken(): string | null {
21-
if (token === undefined) {
22-
// Only on first load, try to get token from document
23-
token = document.head.dataset.requesttoken ?? null
21+
if (globalThis._nc_auth_requestToken) {
22+
return globalThis._nc_auth_requestToken
2423
}
25-
return token
24+
25+
if (globalThis.document) {
26+
// for service workers or other contexts without DOM we need to safeguard this
27+
return document.head.dataset.requesttoken ?? null
28+
}
29+
return null
2630
}
2731

2832
/**
29-
* Add an observer which is called when the CSRF token changes
33+
* Set a new CSRF token (e.g. because of session refresh).
34+
* This also emits an event bus event for the updated token.
3035
*
31-
* @param observer The observer
36+
* @param token - The new token
37+
* @throws {Error} - If the passed token is not a potential valid token
3238
*/
33-
export function onRequestTokenUpdate(observer: CsrfTokenObserver): void {
34-
observers.push(observer)
39+
export function setRequestToken(token: string): void {
40+
if (!token || typeof token !== 'string') {
41+
throw new Error('Invalid CSRF token given', { cause: { token } })
42+
}
43+
44+
if (globalThis._nc_auth_requestToken === token) {
45+
// token is the same as before, no need to update and especially no need to notify the observers
46+
return
47+
}
48+
49+
globalThis._nc_auth_requestToken = token
50+
if (globalThis.document) {
51+
// For DOM environments we also set the token to the DOM, so it is available for legacy code
52+
document.head.dataset.requesttoken = token
53+
}
54+
55+
emit('csrf-token-update', { token, _internal: true })
3556
}
3657

37-
// Listen to server event and keep token in sync
38-
subscribe('csrf-token-update', (e: unknown) => {
39-
token = (e as { token: string }).token
58+
/**
59+
* Fetch the request token from the API.
60+
* This does also set it on the current context, see `setRequestToken`.
61+
*
62+
* @throws {Error} - If the request failed
63+
*/
64+
export async function fetchRequestToken(): Promise<string> {
65+
const url = generateUrl('/csrftoken')
66+
67+
const response = await fetch(url)
68+
if (!response.ok) {
69+
throw new Error('Could not fetch CSRF token from API', { cause: response })
70+
}
71+
72+
try {
73+
const { token } = await response.json()
74+
setRequestToken(token)
75+
return token
76+
} catch (error) {
77+
throw new Error('Could not parse CSRF token from API response', { cause: error })
78+
}
79+
}
4080

41-
observers.forEach((observer) => {
81+
/**
82+
* Add an observer which is called when the CSRF token changes
83+
*
84+
* @param observer The observer
85+
* @return A function to unsubscribe the observer
86+
*/
87+
export function onRequestTokenUpdate(observer: CsrfTokenObserver): () => void {
88+
const wrapper = async ({ token }: { token: string }) => {
4289
try {
43-
observer(token!)
90+
observer(token)
4491
} catch (error) {
4592
// we cannot use the logger as the logger uses this library = circular dependency
4693
// eslint-disable-next-line no-console
4794
console.error('Error updating CSRF token observer', error)
4895
}
96+
}
97+
98+
subscribe('csrf-token-update', wrapper)
99+
return () => unsubscribe('csrf-token-update', wrapper)
100+
}
101+
102+
/**
103+
* Subscribe to token update events from server.
104+
*
105+
* @todo - This is legacy and not needed once all supported server versions use `setRequestToken` of this library.
106+
*/
107+
function _subscribeToTokenUpdates(): void {
108+
// Listen to server event and keep token in sync
109+
subscribe('csrf-token-update', ({ token, _internal }) => {
110+
if (!_internal) {
111+
// Only update the token if the event is not emitted from this library, otherwise we would end in a loop
112+
setRequestToken(token)
113+
}
49114
})
50-
})
115+
}

0 commit comments

Comments
 (0)