Skip to content

Commit 74f572d

Browse files
authored
Merge pull request #900 from nextcloud-libraries/feat/settoken
feat: add `setRequestToken` and `fetchRequestToken` methods
2 parents 3baf2ea + 669418f commit 74f572d

20 files changed

Lines changed: 1178 additions & 267 deletions

.github/workflows/node-test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ jobs:
5151
CYPRESS_INSTALL_BINARY: 0
5252
run: |
5353
npm ci
54+
# Install browsers for tests
55+
npx playwright install chromium
5456
npm run build --if-present
5557
5658
- name: Test

.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

REUSE.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-PackageSupplier = "Nextcloud GmbH <https://nextcloud.com/impressum/>"
66
SPDX-PackageDownloadLocation = "https://github.com/nextcloud-libraries/nextcloud-auth"
77

88
[[annotations]]
9-
path = ["package.json", "package-lock.json", "tsconfig.json"]
9+
path = ["package.json", "package-lock.json", "tsconfig.json", "test/tsconfig.json"]
1010
precedence = "aggregate"
1111
SPDX-FileCopyrightText = "2019 Nextcloud GmbH and Nextcloud contributors"
1212
SPDX-License-Identifier = "GPL-3.0-or-later"

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/csp-nonce.ts

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

6-
import { getRequestToken } from './requesttoken.ts'
6+
import { getRequestToken } from './requestToken.ts'
77

88
/**
99
* Get the CSP nonce for script loading
@@ -18,7 +18,7 @@ import { getRequestToken } from './requesttoken.ts'
1818
*/
1919
export function getCSPNonce(): string | undefined {
2020
const meta = document?.querySelector<HTMLMetaElement>('meta[name="csp-nonce"]')
21-
// backwards compatibility with older Nextcloud versions
21+
// backwards compatibility with older Nextcloud versions (before Nextcloud 30)
2222
if (!meta) {
2323
const token = getRequestToken()
2424
return token ? btoa(token) : undefined

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/guest.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ export function setGuestNickname(nickname: string): void {
7474
getGuestUser().displayName = nickname
7575
}
7676

77+
/**
78+
* Reset the guest user state.
79+
*
80+
* @internal
81+
*/
82+
export function resetGuestUser(): void {
83+
currentUser = undefined
84+
browserStorage.removeItem('guestUid')
85+
browserStorage.removeItem('guestNickname')
86+
}
87+
7788
/**
7889
* Generate a random UUID (version 4) if the crypto API is not available.
7990
* If the crypto API is available, it uses the less secure `randomUUID` method.

lib/index.ts

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

6-
export type { CsrfTokenObserver } from './requesttoken.ts'
6+
export type { CsrfTokenObserver } from './requestToken.ts'
77
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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
7+
import { generateUrl } from '@nextcloud/router'
8+
9+
export interface CsrfTokenObserver {
10+
(token: string): void
11+
}
12+
13+
_subscribeToTokenUpdates() // TODO: remove once we drop support for Nextcloud 33 and before
14+
15+
/**
16+
* Get current request token
17+
*
18+
* @return Current request token or null if not set
19+
*/
20+
export function getRequestToken(): string | null {
21+
if (globalThis._nc_auth_requestToken) {
22+
return globalThis._nc_auth_requestToken
23+
}
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
30+
}
31+
32+
/**
33+
* Set a new CSRF token (e.g. because of session refresh).
34+
* This also emits an event bus event for the updated token.
35+
*
36+
* @param token - The new token
37+
* @throws {Error} - If the passed token is not a potential valid token
38+
*/
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 })
56+
}
57+
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+
}
80+
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 }) => {
89+
try {
90+
observer(token)
91+
} catch (error) {
92+
// we cannot use the logger as the logger uses this library = circular dependency
93+
// eslint-disable-next-line no-console
94+
console.error('Error updating CSRF token observer', error)
95+
}
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+
}
114+
})
115+
}

lib/requesttoken.ts

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)