Skip to content

feat: cookie authentication #246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 26, 2022
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This changelog covers all three packages, as they are (for now) updated as a who
- Add folders with list & grid views, allow drag & drop uploads #228
- Show icons in sidebar
- Add scoped search, funded by NGI NLnet Discovery #245
- Add cookie based authentication #241

## v0.32.1

Expand Down
5 changes: 4 additions & 1 deletion data-browser/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ const ErrBoundary = window.bugsnagApiKey

/** Initialize the agent from localstorage */
const agent = initAgentFromLocalStorage();
agent && store.setAgent(agent);

if (agent) {
store.setAgent(agent);
}

/** Fetch all the Properties and Classes - this helps speed up the app. */
store.fetchResource(urls.properties.getAll);
Expand Down
6 changes: 3 additions & 3 deletions data-browser/tests/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,9 @@ test.describe('data-browser', async () => {
]);
await fileChooser.setFiles(demoFile);
await page.click(`[data-test]:has-text("${demoFileName}")`);
await expect(
await page.locator('[data-test="image-viewer"]'),
).toBeVisible();
const image = await page.locator('[data-test="image-viewer"]');
await expect(image).toBeVisible();
await expect(image).toHaveScreenshot({ maxDiffPixelRatio: 0.1 });
});

test('chatroom', async ({ page, browser }) => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions data-browser/tests/test-config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { TestConfig } from './e2e.spec';
const demoFileName = 'logo.svg';
const demoFileName = 'testimage.svg';

export const testConfig: TestConfig = {
demoFileName,
demoFile: `./${demoFileName}`,
demoFile: `./tests/${demoFileName}`,
demoInviteName: 'document demo',
serverUrl: process.env.SERVER_URL || 'http://localhost:9883',
frontEndUrl: 'http://localhost:5173',
Expand Down
27 changes: 27 additions & 0 deletions data-browser/tests/testimage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 38 additions & 1 deletion lib/src/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Agent, getTimestampNow, HeadersObject, signToBase64 } from '.';
import { Agent, getTimestampNow, HeadersObject, signToBase64, Store } from '.';

/** Returns a JSON-AD resource of an Authentication */
export async function createAuthentication(subject: string, agent: Agent) {
Expand Down Expand Up @@ -68,3 +68,40 @@ export async function signRequest(

return headers as HeadersObject;
}

const ONE_DAY = 24 * 60 * 60 * 1000;

const setCookieExpires = (
name: string,
value: string,
store: Store,
expires_in_ms = ONE_DAY,
) => {
const expiry = new Date(Date.now() + expires_in_ms).toUTCString();
const encodedValue = encodeURIComponent(value);

const domain = new URL(store.getServerUrl()).hostname;

const cookieString = `${name}=${encodedValue};Expires=${expiry};Domain=${domain};SameSite=Lax;path=/`;
document.cookie = cookieString;
};

/** Sets a cookie for the current Agent, signing the Authentication. It expires after some default time. */
export const setCookieAuthentication = (store: Store, agent: Agent) => {
createAuthentication(store.getServerUrl(), agent).then(auth => {
setCookieExpires('atomic_session', btoa(JSON.stringify(auth)), store);
});
};

/** Returns false if the auth cookie is not set / expired */
export const checkAuthenticationCookie = (): boolean => {
const matches = document.cookie.match(
/^(.*;)?\s*atomic_session\s*=\s*[^;]+(.*)?$/,
);

if (!matches) {
return false;
}

return matches.length > 0;
};
24 changes: 14 additions & 10 deletions lib/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@

import {
AtomicError,
checkAuthenticationCookie,
Commit,
ErrorType,
parseCommit,
parseJsonADArray,
parseJsonADResource,
Resource,
serializeDeterministically,
setCookieAuthentication,
signRequest,
Store,
} from './index';

/** Works both in node and the browser */
import fetch from 'cross-fetch';
import { signRequest } from './authentication';

/**
* One key-value pair per HTTP Header. Since we need to support both browsers
Expand Down Expand Up @@ -55,8 +57,16 @@ export async function fetchResource(
// Sign the request if there is an agent present
const agent = store?.getAgent();

if (agent) {
await signRequest(subject, agent, requestHeaders);
if (agent && store) {
// Cookies only work for same-origin requests right now
// https://github.com/atomicdata-dev/atomic-data-browser/issues/253
if (subject.startsWith(window.location.origin)) {
if (!checkAuthenticationCookie()) {
setCookieAuthentication(store, agent);
}
} else {
await signRequest(subject, agent, requestHeaders);
}
}

let url = subject;
Expand Down Expand Up @@ -88,10 +98,7 @@ export async function fetchResource(
);
}
} else if (response.status === 401) {
throw new AtomicError(
`You don't have the rights to do view ${subject}. Are you signed in with the right Agent? More detailed error from server: ${body}`,
ErrorType.Unauthorized,
);
throw new AtomicError(body, ErrorType.Unauthorized);
} else if (response.status === 500) {
throw new AtomicError(body, ErrorType.Server);
} else if (response.status === 404) {
Expand Down Expand Up @@ -194,12 +201,9 @@ export async function uploadFiles(
throw new AtomicError(`No agent present. Can't sign the upload request.`);
}

const signedHeaders = await signRequest(uploadURL.toString(), agent, {});

const options = {
method: 'POST',
body: formData,
headers: signedHeaders,
};

const resp = await fetch(uploadURL.toString(), options);
Expand Down
1 change: 1 addition & 0 deletions lib/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function isUnauthorized(error?: Error): boolean {
export class AtomicError extends Error {
public type: ErrorType;

/** Creates an AtomicError. The message can be either a plain string, or a JSON-AD Error Resource */
public constructor(message: string, type = ErrorType.Client) {
super(message);
// https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript
Expand Down
5 changes: 5 additions & 0 deletions lib/src/store.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setCookieAuthentication } from './authentication';
import { EventManager } from './EventManager';
import {
Agent,
Expand Down Expand Up @@ -166,6 +167,9 @@ export class Store {
// Use WebSocket if available, else use HTTP(S)
const ws = this.getWebSocketForSubject(subject);

// TEMP!!
opts.noWebSocket = true;

if (!opts.noWebSocket && ws?.readyState === WebSocket.OPEN) {
fetchWebSocket(ws, subject);
} else {
Expand Down Expand Up @@ -403,6 +407,7 @@ export class Store {
this.agent = agent;

if (agent) {
setCookieAuthentication(this, agent);
this.webSockets.forEach(ws => {
authenticate(ws, this);
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"lint": "pnpm run -r lint",
"lint-fix": "pnpm run -r lint-fix",
"build": "pnpm run -r build",
"build-server": "pnpm run build && cp -r data-browser/dist/assets/ ../atomic-data-rust/server/app_assets/dist/ && cp data-browser/tests/e2e.spec.ts ../atomic-data-rust/server/e2e_tests/e2e-generated.spec.ts",
"build-server": "pnpm run build && cp -r data-browser/dist/assets/ ../atomic-data-rust/server/app_assets/dist/ && cp data-browser/tests/e2e.spec.ts ../atomic-data-rust/server/e2e_tests/e2e-generated.spec.ts && cp data-browser/tests/testimage.svg ../atomic-data-rust/server/e2e_tests/testimage.svg && cp -r data-browser/tests/e2e.spec.ts-snapshots/ ../atomic-data-rust/server/e2e_tests/e2e-generated.spec.ts-snapshots/",
"test": "pnpm run -r test",
"test-query": "pnpm run --filter @tomic/data-browser test-query",
"start": "pnpm run -r --parallel start",
Expand Down