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
8 changes: 7 additions & 1 deletion website/src/main/resources/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import './components/footer.js';
import './components/blog.js';
import './components/gallery.js';
import pubsub from './components/pubsub.js';
import { clearSharedFolderStreamingAuth } from './lib/shared-folder-streaming.js';
import {
clearSharedFolderStreamingAuth,
installSharedFolderAuthRecovery,
} from './lib/shared-folder-streaming.js';
import { getAuthToken } from './lib/util.js';

installSharedFolderAuthRecovery(getAuthToken);

/** Wire core layout and global auth behavior once DOM is ready. */
document.addEventListener('DOMContentLoaded', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ export function clearSharedFolderStreamingAuth() {
}
}

/** Let every signed-in page rehydrate the active worker after a page navigation. */
export function installSharedFolderAuthRecovery(
getToken,
serviceWorker = typeof navigator === 'undefined' ? null : navigator.serviceWorker,
) {
if (typeof getToken !== 'function' || !serviceWorker?.addEventListener) return;
serviceWorker.addEventListener('message', event => {
if (event.data?.type !== 'shared-folder-auth-request-token') return;
const controller = serviceWorker.controller;
if (event.source !== controller || !event.ports?.[0]) return;
event.ports[0].postMessage({
type: 'shared-folder-auth-recovery',
token: getToken() || null,
});
});
}

function isExpectedWorker(worker) {
return !!worker
&& new URL(worker.scriptURL).pathname === SHARED_FOLDER_AUTH_WORKER_PATH;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export async function respondToSharedFolderFetch({
downloadTokens,
nowFn(),
);
if (!token) token = requestBearerToken(request);
if (!token) token = clientTokens.get(clientId);
if (!token) {
token = await recoverClientToken({
Expand Down Expand Up @@ -64,6 +65,12 @@ export async function respondToSharedFolderFetch({
return response;
}

function requestBearerToken(request) {
const authorization = request.headers.get('Authorization') || '';
if (!authorization.startsWith('Bearer ')) return '';
return authorization.substring('Bearer '.length).trim();
}

/** Stage one exact short-lived download token without persisting it or putting it in the URL. */
export function stageSharedFolderDownloadAuthorization({
requestUrl,
Expand Down
26 changes: 26 additions & 0 deletions website/src/test/js/shared-folder-streaming.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import test from 'node:test';

import {
attachSharedFolderAuthorization,
installSharedFolderAuthRecovery,
isSharedFolderApiRequest,
sharedFolderDownloadRequestUrl,
prepareSharedFolderStreamingAuth,
Expand Down Expand Up @@ -79,6 +80,31 @@ test('download and binary-preview denial states are actionable without a rejecte
});
});

test('global auth recovery answers an existing worker after navigation to Back Office', () => {
let onMessage;
const replies = [];
const controller = { scriptURL: `${origin}/shared-folder-auth-sw.js` };
const serviceWorker = {
controller,
addEventListener(type, listener) {
assert.equal(type, 'message');
onMessage = listener;
},
};

installSharedFolderAuthRecovery(() => 'current-jwt', serviceWorker);
onMessage({
data: { type: 'shared-folder-auth-request-token' },
source: controller,
ports: [{ postMessage: reply => replies.push(reply) }],
});

assert.deepEqual(replies, [{
type: 'shared-folder-auth-recovery',
token: 'current-jwt',
}]);
});

test('shared-folder page starts native anchor and media requests without Blob buffering', () => {
const page = fs.readFileSync('website/src/main/resources/static/js/shared-folder.js', 'utf8');
const worker = fs.readFileSync('website/src/main/resources/static/shared-folder-auth-sw.js', 'utf8');
Expand Down
22 changes: 22 additions & 0 deletions website/src/test/js/shared-folder-worker-runtime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ test('lost worker token is rehydrated from the initiating controlled client befo
assert.equal(requests[0].options.cache, 'no-store');
});

test('an authenticated shared-folder API fetch keeps its bearer token across page navigation', async () => {
const requests = [];
const response = await respondToSharedFolderFetch({
request: new Request(apiUrl, {
headers: { Authorization: 'Bearer back-office-jwt' },
}),
clientId: 'back-office-client',
clientTokens: new Map(),
clients: {
get: async () => assert.fail('an explicit bearer token must not require client recovery'),
},
origin,
fetchFn: async request => {
requests.push(request);
return new Response('admin data', { status: 200 });
},
});

assert.equal(response.status, 200);
assert.equal(requests[0].headers.get('Authorization'), 'Bearer back-office-jwt');
});

test('a token-recovery timeout returns a controlled denial instead of falling through unauthenticated', async () => {
const tokens = new Map();
const messages = [];
Expand Down