Skip to content

Commit 6e04700

Browse files
committed
feat: Add CSP nonce handling
Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent aaff9b2 commit 6e04700

File tree

4 files changed

+88
-10
lines changed

4 files changed

+88
-10
lines changed

README.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,23 @@ Nextcloud helpers related to authentication and the current user
1313
## Install
1414

1515
```sh
16-
yarn add @nextcloud/auth
16+
npm install @nextcloud/auth --save
1717
```
1818

1919
```sh
20-
npm install @nextcloud/auth --save
20+
yarn add @nextcloud/auth
2121
```
2222

2323
## Usage
24+
For detailed information check [the package documentation](https://nextcloud-libraries.github.io/nextcloud-auth/index.html).
2425

26+
One example usage to get the current user:
2527
```ts
26-
import {
27-
getRequestToken,
28-
getCurrentUser,
29-
onRequestTokenUpdate,
30-
} from '@nextcloud/auth'
28+
import { getCurrentUser } from '@nextcloud/auth'
3129

3230
const user = getCurrentUser()
3331

3432
if (user.isAdmin) {
3533
// do something
3634
}
3735
```
38-
39-
For more information check [nextcloud-libraries.github.io/nextcloud-auth](https://nextcloud-libraries.github.io/nextcloud-auth/index.html)

lib/csp-nonce.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
import { getRequestToken } from './requesttoken'
7+
8+
/**
9+
* Get the CSP nonce for script loading
10+
*
11+
* @return Current nonce if set
12+
* @example When using webpack this can be used to allow webpack to dynamically load additional modules:
13+
* ```js
14+
* import { getCSPNonce } from '@nextcloud/auth'
15+
*
16+
* __webpack_nonce__ = getCSPNonce()
17+
* ```
18+
*/
19+
export function getCSPNonce(): string | undefined {
20+
const meta = document?.querySelector<HTMLMetaElement>('meta[name="csp-nonce"]')
21+
// backwards compatibility with older Nextcloud versions
22+
if (!meta) {
23+
const token = getRequestToken()
24+
return token ? btoa(token) : undefined
25+
}
26+
return meta.nonce
27+
}

lib/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
export type { CsrfTokenObserver } from './requesttoken'
66
export type { NextcloudUser } from './user'
77

8+
export { getCSPNonce } from './csp-nonce'
9+
export { getGuestNickname, setGuestNickname } from './guest'
810
export { getRequestToken, onRequestTokenUpdate } from './requesttoken'
911
export { getCurrentUser } from './user'
10-
export { getGuestNickname, setGuestNickname } from './guest'

test/csp-nonce.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
import { randomBytes } from 'crypto'
6+
import { beforeEach, describe, expect, test, vi } from 'vitest'
7+
8+
/**
9+
* Mock `<meta>` element with nonce
10+
*/
11+
function mockNonce() {
12+
const nonce = randomBytes(16).toString('base64')
13+
const el = document.createElement('meta')
14+
el.name = 'csp-nonce'
15+
el.nonce = nonce
16+
document.head.appendChild(el)
17+
return nonce
18+
}
19+
20+
describe('CSP nonce', () => {
21+
beforeEach(() => {
22+
vi.resetModules()
23+
// reset document
24+
document.head.innerHTML = ''
25+
delete document.head.dataset.requesttoken
26+
})
27+
28+
test('read nonce from meta element', async () => {
29+
const { getCSPNonce } = await import('../lib')
30+
const nonce = mockNonce()
31+
expect(getCSPNonce()).toBe(nonce)
32+
})
33+
34+
test('prefer nonce over csrf token', async () => {
35+
const { getCSPNonce } = await import('../lib')
36+
37+
const nonce = mockNonce()
38+
document.head.dataset.requesttoken = 'csrf-token'
39+
expect(getCSPNonce()).toBe(nonce)
40+
})
41+
42+
test('fall back to csrf token for legacy Nextcloud versions', async () => {
43+
const { getCSPNonce } = await import('../lib')
44+
45+
document.head.dataset.requesttoken = 'csrf-token'
46+
expect(getCSPNonce()).toBe(btoa('csrf-token'))
47+
})
48+
49+
test('return undefined if neither csp nonce nor csrf token is set', async () => {
50+
const { getCSPNonce } = await import('../lib')
51+
52+
expect(getCSPNonce()).toBe(undefined)
53+
})
54+
})

0 commit comments

Comments
 (0)