Skip to content

Commit fd6a835

Browse files
joepioThom van Kalkeren
authored and
Thom van Kalkeren
committed
#241 refactor cookie authentication, fix expiration
1 parent 7bff95b commit fd6a835

File tree

7 files changed

+59
-33
lines changed

7 files changed

+59
-33
lines changed

data-browser/src/App.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
urls,
99
} from '@tomic/react';
1010

11-
import { setCookieAuthentication } from './helpers/cookieAuthentication';
1211
import { GlobalStyle, ThemeWrapper } from './styling';
1312
import { AppRoutes } from './routes/Routes';
1413
import { NavWrapper } from './components/Navigation';
@@ -63,7 +62,6 @@ const agent = initAgentFromLocalStorage();
6362

6463
if (agent) {
6564
store.setAgent(agent);
66-
setCookieAuthentication(store, agent);
6765
}
6866

6967
/** Fetch all the Properties and Classes - this helps speed up the app. */

data-browser/src/helpers/cookieAuthentication.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

data-browser/src/routes/SettingsAgent.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { useState } from 'react';
3-
import { Agent, useStore } from '@tomic/react';
3+
import { Agent } from '@tomic/react';
44
import { FaCog, FaEye, FaEyeSlash, FaUser } from 'react-icons/fa';
55

66
import { useSettings } from '../helpers/AppSettings';
@@ -12,7 +12,6 @@ import {
1212
import { ButtonInput, Button } from '../components/Button';
1313
import { Margin } from '../components/Card';
1414
import Field from '../components/forms/Field';
15-
import { setCookieAuthentication } from '../helpers/cookieAuthentication';
1615
import { ResourceInline } from '../views/ResourceInline';
1716
import { ContainerNarrow } from '../components/Containers';
1817
import { AtomicLink } from '../components/AtomicLink';
@@ -29,7 +28,6 @@ const SettingsAgent: React.FunctionComponent = () => {
2928
const [advanced, setAdvanced] = useState(false);
3029
const [secret, setSecret] = useState<string | undefined>(undefined);
3130
const navigate = useNavigate();
32-
const store = useStore();
3331

3432
// When there is an agent, set the advanced values
3533
// Otherwise, reset the secret value
@@ -83,7 +81,6 @@ const SettingsAgent: React.FunctionComponent = () => {
8381
function setAgentIfChanged(oldAgent: Agent | undefined, newAgent: Agent) {
8482
if (JSON.stringify(oldAgent) !== JSON.stringify(newAgent)) {
8583
setAgent(newAgent);
86-
setCookieAuthentication(store, newAgent);
8784
}
8885
}
8986

lib/src/authentication.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Agent, getTimestampNow, HeadersObject, signToBase64 } from '.';
1+
import { Agent, getTimestampNow, HeadersObject, signToBase64, Store } from '.';
22

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

6969
return headers as HeadersObject;
7070
}
71+
72+
const ONE_DAY = 24 * 60 * 60 * 1000;
73+
74+
const setCookieExpires = (
75+
name: string,
76+
value: string,
77+
store: Store,
78+
expires_in_ms = ONE_DAY,
79+
) => {
80+
const expiry = new Date(Date.now() + expires_in_ms).toUTCString();
81+
const encodedValue = encodeURIComponent(value);
82+
83+
const domain = new URL(store.getServerUrl()).hostname;
84+
85+
const cookieString = `${name}=${encodedValue};Expires=${expiry};Domain=${domain};SameSite=Lax;path=/`;
86+
document.cookie = cookieString;
87+
};
88+
89+
/** Sets a cookie for the current Agent, signing the Authentication. It expires after some default time. */
90+
export const setCookieAuthentication = (store: Store, agent: Agent) => {
91+
createAuthentication(store.getServerUrl(), agent).then(auth => {
92+
setCookieExpires('atomic_session', btoa(JSON.stringify(auth)), store);
93+
});
94+
};
95+
96+
/** Returns false if the auth cookie is not set / expired */
97+
export const checkAuthenticationCookie = (): boolean => {
98+
const matches = document.cookie.match(
99+
/^(.*;)?\s*atomic_session\s*=\s*[^;]+(.*)?$/,
100+
);
101+
102+
if (!matches) {
103+
return false;
104+
}
105+
106+
return matches.length > 0;
107+
};

lib/src/client.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@
33

44
import {
55
AtomicError,
6+
checkAuthenticationCookie,
67
Commit,
78
ErrorType,
89
parseCommit,
910
parseJsonADArray,
1011
parseJsonADResource,
1112
Resource,
1213
serializeDeterministically,
14+
setCookieAuthentication,
15+
signRequest,
1316
Store,
1417
} from './index';
1518

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

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

58-
if (agent) {
59-
await signRequest(subject, agent, requestHeaders);
60+
if (agent && store) {
61+
// Cookies only work for same-origin requests right now
62+
// https://github.com/atomicdata-dev/atomic-data-browser/issues/253
63+
if (subject.startsWith(window.location.origin)) {
64+
if (!checkAuthenticationCookie()) {
65+
setCookieAuthentication(store, agent);
66+
}
67+
} else {
68+
await signRequest(subject, agent, requestHeaders);
69+
}
6070
}
6171

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

197-
const signedHeaders = await signRequest(uploadURL.toString(), agent, {});
198-
199204
const options = {
200205
method: 'POST',
201206
body: formData,
202-
headers: signedHeaders,
203207
};
204208

205209
const resp = await fetch(uploadURL.toString(), options);

lib/src/error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function isUnauthorized(error?: Error): boolean {
2828
export class AtomicError extends Error {
2929
public type: ErrorType;
3030

31+
/** Creates an AtomicError. The message can be either a plain string, or a JSON-AD Error Resource */
3132
public constructor(message: string, type = ErrorType.Client) {
3233
super(message);
3334
// https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript

lib/src/store.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { setCookieAuthentication } from './authentication';
12
import { EventManager } from './EventManager';
23
import {
34
Agent,
@@ -166,6 +167,9 @@ export class Store {
166167
// Use WebSocket if available, else use HTTP(S)
167168
const ws = this.getWebSocketForSubject(subject);
168169

170+
// TEMP!!
171+
opts.noWebSocket = true;
172+
169173
if (!opts.noWebSocket && ws?.readyState === WebSocket.OPEN) {
170174
fetchWebSocket(ws, subject);
171175
} else {
@@ -403,6 +407,7 @@ export class Store {
403407
this.agent = agent;
404408

405409
if (agent) {
410+
setCookieAuthentication(this, agent);
406411
this.webSockets.forEach(ws => {
407412
authenticate(ws, this);
408413
});

0 commit comments

Comments
 (0)