Browse Source
fix(OC): validate request token and move logic to one place
fix(OC): validate request token and move logic to one place
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>pull/53326/head
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
9 changed files with 594 additions and 74 deletions
-
4core/src/OC/index.js
-
56core/src/OC/requesttoken.ts
-
2core/src/globals.js
-
4core/src/jquery/requesttoken.js
-
44core/src/tests/OC/requesttoken.spec.js
-
147core/src/tests/OC/requesttoken.spec.ts
-
407package-lock.json
-
1package.json
-
3tsconfig.json
@ -1,44 +0,0 @@ |
|||
/** |
|||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors |
|||
* SPDX-License-Identifier: AGPL-3.0-or-later |
|||
*/ |
|||
|
|||
import { beforeEach, describe, expect, test, vi } from 'vitest' |
|||
import { manageToken, setToken } from '../../OC/requesttoken.ts' |
|||
|
|||
const eventbus = vi.hoisted(() => ({ emit: vi.fn() })) |
|||
vi.mock('@nextcloud/event-bus', () => eventbus) |
|||
|
|||
describe('request token', () => { |
|||
|
|||
let emit |
|||
let manager |
|||
const token = 'abc123' |
|||
|
|||
beforeEach(() => { |
|||
emit = vi.fn() |
|||
const head = window.document.getElementsByTagName('head')[0] |
|||
head.setAttribute('data-requesttoken', token) |
|||
|
|||
manager = manageToken(window.document, emit) |
|||
}) |
|||
|
|||
test('reads the token from the document', () => { |
|||
expect(manager.getToken()).toBe('abc123') |
|||
}) |
|||
|
|||
test('remembers the updated token', () => { |
|||
manager.setToken('bca321') |
|||
|
|||
expect(manager.getToken()).toBe('bca321') |
|||
}) |
|||
|
|||
describe('@nextcloud/auth integration', () => { |
|||
test('fires off an event for @nextcloud/auth', () => { |
|||
setToken('123') |
|||
|
|||
expect(eventbus.emit).toHaveBeenCalledWith('csrf-token-update', { token: '123' }) |
|||
}) |
|||
}) |
|||
|
|||
}) |
@ -0,0 +1,147 @@ |
|||
/** |
|||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors |
|||
* SPDX-License-Identifier: AGPL-3.0-or-later |
|||
*/ |
|||
|
|||
import { setupServer } from 'msw/node' |
|||
import { http, HttpResponse } from 'msw' |
|||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' |
|||
import { fetchRequestToken, getRequestToken, setRequestToken } from '../../OC/requesttoken.ts' |
|||
|
|||
const eventbus = vi.hoisted(() => ({ emit: vi.fn() })) |
|||
vi.mock('@nextcloud/event-bus', () => eventbus) |
|||
|
|||
const server = setupServer() |
|||
|
|||
describe('getRequestToken', () => { |
|||
it('can read the token from DOM', () => { |
|||
mockToken('tokenmock-123') |
|||
expect(getRequestToken()).toBe('tokenmock-123') |
|||
}) |
|||
|
|||
it('can handle missing token', () => { |
|||
mockToken(undefined) |
|||
expect(getRequestToken()).toBeUndefined() |
|||
}) |
|||
}) |
|||
|
|||
describe('setRequestToken', () => { |
|||
beforeEach(() => { |
|||
vi.resetAllMocks() |
|||
}) |
|||
|
|||
it('does emit an event on change', () => { |
|||
setRequestToken('new-token') |
|||
expect(eventbus.emit).toBeCalledTimes(1) |
|||
expect(eventbus.emit).toBeCalledWith('csrf-token-update', { token: 'new-token' }) |
|||
}) |
|||
|
|||
it('does set the new token to the DOM', () => { |
|||
setRequestToken('new-token') |
|||
expect(document.head.dataset.requesttoken).toBe('new-token') |
|||
}) |
|||
|
|||
it('does remember the new token', () => { |
|||
mockToken('old-token') |
|||
setRequestToken('new-token') |
|||
expect(getRequestToken()).toBe('new-token') |
|||
}) |
|||
|
|||
it('throws if the token is not a string', () => { |
|||
// @ts-expect-error mocking
|
|||
expect(() => setRequestToken(123)).toThrowError('Invalid CSRF token given') |
|||
}) |
|||
|
|||
it('throws if the token is not valid', () => { |
|||
expect(() => setRequestToken('')).toThrowError('Invalid CSRF token given') |
|||
}) |
|||
|
|||
it('does not emit an event if the token is not valid', () => { |
|||
expect(() => setRequestToken('')).toThrowError('Invalid CSRF token given') |
|||
expect(eventbus.emit).not.toBeCalled() |
|||
}) |
|||
}) |
|||
|
|||
describe('fetchRequestToken', () => { |
|||
const successfullCsrf = http.get('/index.php/csrftoken', () => { |
|||
return HttpResponse.json({ token: 'new-token' }) |
|||
}) |
|||
const forbiddenCsrf = http.get('/index.php/csrftoken', () => { |
|||
return HttpResponse.json([], { status: 403 }) |
|||
}) |
|||
const serverErrorCsrf = http.get('/index.php/csrftoken', () => { |
|||
return HttpResponse.json([], { status: 500 }) |
|||
}) |
|||
const networkErrorCsrf = http.get('/index.php/csrftoken', () => { |
|||
return new HttpResponse(null, { type: 'error' }) |
|||
}) |
|||
|
|||
beforeAll(() => { |
|||
server.listen() |
|||
}) |
|||
|
|||
beforeEach(() => { |
|||
vi.resetAllMocks() |
|||
}) |
|||
|
|||
it('correctly parses response', async () => { |
|||
server.use(successfullCsrf) |
|||
|
|||
mockToken('oldToken') |
|||
const token = await fetchRequestToken() |
|||
expect(token).toBe('new-token') |
|||
}) |
|||
|
|||
it('sets the token', async () => { |
|||
server.use(successfullCsrf) |
|||
|
|||
mockToken('oldToken') |
|||
await fetchRequestToken() |
|||
expect(getRequestToken()).toBe('new-token') |
|||
}) |
|||
|
|||
it('does emit an event', async () => { |
|||
server.use(successfullCsrf) |
|||
|
|||
await fetchRequestToken() |
|||
expect(eventbus.emit).toHaveBeenCalledOnce() |
|||
expect(eventbus.emit).toBeCalledWith('csrf-token-update', { token: 'new-token' }) |
|||
}) |
|||
|
|||
it('handles 403 error due to invalid cookies', async () => { |
|||
server.use(forbiddenCsrf) |
|||
|
|||
mockToken('oldToken') |
|||
await expect(() => fetchRequestToken()).rejects.toThrowError('Could not fetch CSRF token from API') |
|||
expect(getRequestToken()).toBe('oldToken') |
|||
}) |
|||
|
|||
it('handles server error', async () => { |
|||
server.use(serverErrorCsrf) |
|||
|
|||
mockToken('oldToken') |
|||
await expect(() => fetchRequestToken()).rejects.toThrowError('Could not fetch CSRF token from API') |
|||
expect(getRequestToken()).toBe('oldToken') |
|||
}) |
|||
|
|||
it('handles network error', async () => { |
|||
server.use(networkErrorCsrf) |
|||
|
|||
mockToken('oldToken') |
|||
await expect(() => fetchRequestToken()).rejects.toThrow() |
|||
expect(getRequestToken()).toBe('oldToken') |
|||
}) |
|||
}) |
|||
|
|||
/** |
|||
* Mock the request token directly so we can test reading it. |
|||
* |
|||
* @param token - The CSRF token to mock |
|||
*/ |
|||
function mockToken(token?: string) { |
|||
if (token === undefined) { |
|||
delete document.head.dataset.requesttoken |
|||
} else { |
|||
document.head.dataset.requesttoken = token |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue