Skip to content

Commit

Permalink
feat: Add CSP nonce handling
Browse files Browse the repository at this point in the history
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Aug 13, 2024
1 parent aaff9b2 commit 6e04700
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 10 deletions.
14 changes: 5 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,23 @@ Nextcloud helpers related to authentication and the current user
## Install

```sh
yarn add @nextcloud/auth
npm install @nextcloud/auth --save
```

```sh
npm install @nextcloud/auth --save
yarn add @nextcloud/auth
```

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

One example usage to get the current user:
```ts
import {
getRequestToken,
getCurrentUser,
onRequestTokenUpdate,
} from '@nextcloud/auth'
import { getCurrentUser } from '@nextcloud/auth'

const user = getCurrentUser()

if (user.isAdmin) {
// do something
}
```

For more information check [nextcloud-libraries.github.io/nextcloud-auth](https://nextcloud-libraries.github.io/nextcloud-auth/index.html)
27 changes: 27 additions & 0 deletions lib/csp-nonce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { getRequestToken } from './requesttoken'

/**
* Get the CSP nonce for script loading
*
* @return Current nonce if set

Check warning on line 11 in lib/csp-nonce.ts

View workflow job for this annotation

GitHub Actions / NPM lint

Missing JSDoc @return type
* @example When using webpack this can be used to allow webpack to dynamically load additional modules:
* ```js
* import { getCSPNonce } from '@nextcloud/auth'
*
* __webpack_nonce__ = getCSPNonce()
* ```
*/
export function getCSPNonce(): string | undefined {
const meta = document?.querySelector<HTMLMetaElement>('meta[name="csp-nonce"]')
// backwards compatibility with older Nextcloud versions
if (!meta) {
const token = getRequestToken()
return token ? btoa(token) : undefined
}
return meta.nonce
}
3 changes: 2 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export type { CsrfTokenObserver } from './requesttoken'
export type { NextcloudUser } from './user'

export { getCSPNonce } from './csp-nonce'
export { getGuestNickname, setGuestNickname } from './guest'
export { getRequestToken, onRequestTokenUpdate } from './requesttoken'
export { getCurrentUser } from './user'
export { getGuestNickname, setGuestNickname } from './guest'
54 changes: 54 additions & 0 deletions test/csp-nonce.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { randomBytes } from 'crypto'
import { beforeEach, describe, expect, test, vi } from 'vitest'

/**
* Mock `<meta>` element with nonce
*/
function mockNonce() {
const nonce = randomBytes(16).toString('base64')
const el = document.createElement('meta')
el.name = 'csp-nonce'
el.nonce = nonce
document.head.appendChild(el)
return nonce
}

describe('CSP nonce', () => {
beforeEach(() => {
vi.resetModules()
// reset document
document.head.innerHTML = ''
delete document.head.dataset.requesttoken
})

test('read nonce from meta element', async () => {
const { getCSPNonce } = await import('../lib')
const nonce = mockNonce()
expect(getCSPNonce()).toBe(nonce)
})

test('prefer nonce over csrf token', async () => {
const { getCSPNonce } = await import('../lib')

const nonce = mockNonce()
document.head.dataset.requesttoken = 'csrf-token'
expect(getCSPNonce()).toBe(nonce)
})

test('fall back to csrf token for legacy Nextcloud versions', async () => {
const { getCSPNonce } = await import('../lib')

document.head.dataset.requesttoken = 'csrf-token'
expect(getCSPNonce()).toBe(btoa('csrf-token'))
})

test('return undefined if neither csp nonce nor csrf token is set', async () => {
const { getCSPNonce } = await import('../lib')

expect(getCSPNonce()).toBe(undefined)
})
})

0 comments on commit 6e04700

Please sign in to comment.