Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/node-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
CYPRESS_INSTALL_BINARY: 0
run: |
npm ci
# Install browsers for tests
npx playwright install chromium
npm run build --if-present

- name: Test
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Tests
.vitest*
__screenshots__/

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

Expand Down
2 changes: 1 addition & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SPDX-PackageSupplier = "Nextcloud GmbH <https://nextcloud.com/impressum/>"
SPDX-PackageDownloadLocation = "https://github.com/nextcloud-libraries/nextcloud-auth"

[[annotations]]
path = ["package.json", "package-lock.json", "tsconfig.json"]
path = ["package.json", "package-lock.json", "tsconfig.json", "test/tsconfig.json"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2019 Nextcloud GmbH and Nextcloud contributors"
SPDX-License-Identifier = "GPL-3.0-or-later"
Expand Down
3 changes: 3 additions & 0 deletions lib/globals.d.ts → globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*/

declare global {
// eslint-disable-next-line camelcase
var _nc_auth_requestToken: string | undefined

interface Window {
_oc_isadmin?: boolean
}
Expand Down
4 changes: 2 additions & 2 deletions lib/csp-nonce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { getRequestToken } from './requesttoken.ts'
import { getRequestToken } from './requestToken.ts'

/**
* Get the CSP nonce for script loading
Expand All @@ -18,7 +18,7 @@ import { getRequestToken } from './requesttoken.ts'
*/
export function getCSPNonce(): string | undefined {
const meta = document?.querySelector<HTMLMetaElement>('meta[name="csp-nonce"]')
// backwards compatibility with older Nextcloud versions
// backwards compatibility with older Nextcloud versions (before Nextcloud 30)
if (!meta) {
const token = getRequestToken()
return token ? btoa(token) : undefined
Expand Down
1 change: 1 addition & 0 deletions lib/eventbus.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { NextcloudUser } from './user.ts'

declare module '@nextcloud/event-bus' {
export interface NextcloudEvents {
'csrf-token-update': { token: string, _internal?: true }
// mapping of 'event name' => 'event type'
'user:info:changed': NextcloudUser
}
Expand Down
11 changes: 11 additions & 0 deletions lib/guest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ export function setGuestNickname(nickname: string): void {
getGuestUser().displayName = nickname
}

/**
* Reset the guest user state.
*
* @internal
*/
export function resetGuestUser(): void {
currentUser = undefined
browserStorage.removeItem('guestUid')
browserStorage.removeItem('guestNickname')
}

/**
* Generate a random UUID (version 4) if the crypto API is not available.
* If the crypto API is available, it uses the less secure `randomUUID` method.
Expand Down
4 changes: 2 additions & 2 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/

export type { CsrfTokenObserver } from './requesttoken.ts'
export type { CsrfTokenObserver } from './requestToken.ts'
export type { NextcloudUser } from './user.ts'

export { getCSPNonce } from './csp-nonce.ts'
export { getGuestNickname, getGuestUser, setGuestNickname } from './guest.ts'
export { getRequestToken, onRequestTokenUpdate } from './requesttoken.ts'
export { fetchRequestToken, getRequestToken, onRequestTokenUpdate, setRequestToken } from './requestToken.ts'
export { getCurrentUser } from './user.ts'
115 changes: 115 additions & 0 deletions lib/requestToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

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

export interface CsrfTokenObserver {
(token: string): void
}

_subscribeToTokenUpdates() // TODO: remove once we drop support for Nextcloud 33 and before

/**
* Get current request token
*
* @return Current request token or null if not set
*/
export function getRequestToken(): string | null {
if (globalThis._nc_auth_requestToken) {
return globalThis._nc_auth_requestToken
}

if (globalThis.document) {
// for service workers or other contexts without DOM we need to safeguard this
return document.head.dataset.requesttoken ?? null
}
return null
}
Comment thread
susnux marked this conversation as resolved.

/**
* Set a new CSRF token (e.g. because of session refresh).
* This also emits an event bus event for the updated token.
*
* @param token - The new token
* @throws {Error} - If the passed token is not a potential valid token
*/
export function setRequestToken(token: string): void {
if (!token || typeof token !== 'string') {
throw new Error('Invalid CSRF token given', { cause: { token } })
}

if (globalThis._nc_auth_requestToken === token) {
// token is the same as before, no need to update and especially no need to notify the observers
return
}

globalThis._nc_auth_requestToken = token
if (globalThis.document) {
// For DOM environments we also set the token to the DOM, so it is available for legacy code
document.head.dataset.requesttoken = token
}

emit('csrf-token-update', { token, _internal: true })
}

/**
* Fetch the request token from the API.
* This does also set it on the current context, see `setRequestToken`.
*
* @throws {Error} - If the request failed
*/
export async function fetchRequestToken(): Promise<string> {
const url = generateUrl('/csrftoken')

const response = await fetch(url)
if (!response.ok) {
throw new Error('Could not fetch CSRF token from API', { cause: response })
}

try {
const { token } = await response.json()
setRequestToken(token)
return token
} catch (error) {
throw new Error('Could not parse CSRF token from API response', { cause: error })
}
}

/**
* Add an observer which is called when the CSRF token changes
*
* @param observer The observer
* @return A function to unsubscribe the observer
*/
export function onRequestTokenUpdate(observer: CsrfTokenObserver): () => void {
const wrapper = async ({ token }: { token: string }) => {
try {
observer(token)
} catch (error) {
// we cannot use the logger as the logger uses this library = circular dependency
// eslint-disable-next-line no-console
console.error('Error updating CSRF token observer', error)
}
}

subscribe('csrf-token-update', wrapper)
return () => unsubscribe('csrf-token-update', wrapper)
}

/**
* Subscribe to token update events from server.
*
* @todo - This is legacy and not needed once all supported server versions use `setRequestToken` of this library.
*/
function _subscribeToTokenUpdates(): void {
// Listen to server event and keep token in sync
subscribe('csrf-token-update', ({ token, _internal }) => {
if (!_internal) {
// Only update the token if the event is not emitted from this library, otherwise we would end in a loop
setRequestToken(token)
}
})
}
50 changes: 0 additions & 50 deletions lib/requesttoken.ts

This file was deleted.

Loading
Loading