Skip to content

Commit

Permalink
fix sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
0age committed Dec 5, 2024
1 parent 290e2d2 commit af3c7ee
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 262 deletions.
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ A minimalistic server-based allocator for [The Compact](https://github.com/Unisw
1. **Get Session Payload**

```http
GET /session/:address
GET /session/:chainId/:address
```

Returns an EIP-4361 payload for signing. Example response:
Returns an EIP-4361 payload for signing. The chainId parameter specifies which blockchain network to authenticate for. Example response:

```json
{
Expand All @@ -33,11 +33,10 @@ A minimalistic server-based allocator for [The Compact](https://github.com/Unisw
"uri": "http://localhost:3000",
"statement": "Sign in to Smallocator",
"version": "1",
"chainId": 1,
"chainId": 10,
"nonce": "unique_nonce",
"issuedAt": "2024-12-03T12:00:00Z",
"expirationTime": "2024-12-03T13:00:00Z",
"resources": ["http://localhost:3000/resources"]
"expirationTime": "2024-12-03T13:00:00Z"
}
}
```
Expand Down Expand Up @@ -225,10 +224,10 @@ All compact operations require a valid session ID in the `x-session-id` header.
2. **Get Session Payload**

```http
GET /session/:address
GET /session/:chainId/:address
```

Returns an EIP-4361 payload for signing. Example response:
Returns an EIP-4361 payload for signing. The chainId parameter specifies which blockchain network to authenticate for. Example response:

```json
{
Expand All @@ -238,11 +237,10 @@ All compact operations require a valid session ID in the `x-session-id` header.
"uri": "http://localhost:3000",
"statement": "Sign in to Smallocator",
"version": "1",
"chainId": 1,
"chainId": 10,
"nonce": "unique_nonce",
"issuedAt": "2024-12-03T12:00:00Z",
"expirationTime": "2024-12-03T13:00:00Z",
"resources": ["http://localhost:3000/resources"]
"expirationTime": "2024-12-03T13:00:00Z"
}
}
```
Expand Down
63 changes: 27 additions & 36 deletions frontend/src/components/SessionManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,53 +14,44 @@ export function SessionManager({ onSessionCreated }: SessionManagerProps) {
const createSession = async () => {
if (!address || !chainId) return;

const nonce = crypto.randomUUID();
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
const domain = new URL(baseUrl).host;
const resources = [`${baseUrl}/resources`];

// Create timestamps once to ensure consistency
const issuedAt = new Date().toISOString();
const expirationTime = new Date(Date.now() + 30 * 60 * 1000).toISOString();

try {
// First, get the payload from server
const payloadResponse = await fetch(`/session/${address}`);
if (!payloadResponse.ok) {
throw new Error('Failed to get session payload');
}

const { payload } = await payloadResponse.json();

// Format the message according to EIP-4361
const message = [
`${payload.domain} wants you to sign in with your Ethereum account:`,
payload.address,
'',
payload.statement,
'',
`URI: ${payload.uri}`,
`Version: ${payload.version}`,
`Chain ID: ${payload.chainId}`,
`Nonce: ${payload.nonce}`,
`Issued At: ${payload.issuedAt}`,
`Expiration Time: ${payload.expirationTime}`,
].join('\n');

// Get signature from wallet
const signature = await signMessageAsync({
message: [
`${domain} wants you to sign in with your Ethereum account:`,
address,
'',
'Sign in to Smallocator',
'',
`URI: ${baseUrl}`,
`Version: 1`,
`Chain ID: ${chainId}`,
`Nonce: ${nonce}`,
`Issued At: ${issuedAt}`,
`Expiration Time: ${expirationTime}`,
'Resources:',
...resources,
].join('\n'),
message,
});

// Submit signature and payload to create session
const response = await fetch('/session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
signature,
payload: {
domain,
address,
uri: baseUrl,
statement: 'Sign in to Smallocator',
version: '1',
chainId,
nonce,
issuedAt,
expirationTime,
resources,
},
payload,
}),
});

Expand Down
54 changes: 44 additions & 10 deletions src/__tests__/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import { FastifyInstance } from 'fastify';
import {
createTestServer,
validPayload,
getFreshValidPayload,
getFreshCompact,
cleanupTestServer,
compactToAPI,
generateSignature,
} from './utils/test-server';
import { generateSignature } from '../crypto';
import {
graphqlClient,
AllocatorResponse,
Expand Down Expand Up @@ -110,11 +109,11 @@ describe('API Routes', () => {
});
});

describe('GET /session/:address', () => {
describe('GET /session/:chainId/:address', () => {
it('should return a session payload for valid address', async () => {
const response = await server.inject({
method: 'GET',
url: '/session/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
url: '/session/1/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
});

expect(response.statusCode).toBe(200);
Expand All @@ -129,7 +128,7 @@ describe('API Routes', () => {
it('should reject invalid ethereum address', async () => {
const response = await server.inject({
method: 'GET',
url: '/session/invalid-address',
url: '/session/1/invalid-address',
});

expect(response.statusCode).toBe(400);
Expand All @@ -138,13 +137,21 @@ describe('API Routes', () => {

describe('POST /session', () => {
it('should create session with valid signature', async () => {
const payload = getFreshValidPayload();
const signature = await generateSignature(payload);
// First get a session request
const sessionResponse = await server.inject({
method: 'GET',
url: `/session/1/${validPayload.address}`,
});

expect(sessionResponse.statusCode).toBe(200);
const sessionRequest = JSON.parse(sessionResponse.payload);

const signature = await generateSignature(sessionRequest.session);
const response = await server.inject({
method: 'POST',
url: '/session',
payload: {
payload,
payload: sessionRequest.session,
signature,
},
});
Expand All @@ -158,11 +165,20 @@ describe('API Routes', () => {
});

it('should reject invalid signature', async () => {
// First get a session request
const sessionResponse = await server.inject({
method: 'GET',
url: `/session/1/${validPayload.address}`,
});

expect(sessionResponse.statusCode).toBe(200);
const sessionRequest = JSON.parse(sessionResponse.payload);

const response = await server.inject({
method: 'POST',
url: '/session',
payload: {
payload: validPayload,
payload: sessionRequest.session,
signature: 'invalid-signature',
},
});
Expand All @@ -175,8 +191,26 @@ describe('API Routes', () => {
let sessionId: string;

beforeEach(async () => {
// First get a session request
const address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
const sessionResponse = await server.inject({
method: 'GET',
url: `/session/1/${address}`,
});

expect(sessionResponse.statusCode).toBe(200);
const sessionRequest = JSON.parse(sessionResponse.payload);

// Normalize timestamps to match database precision
const payload = {
...sessionRequest.session,
issuedAt: new Date(sessionRequest.session.issuedAt).toISOString(),
expirationTime: new Date(
sessionRequest.session.expirationTime
).toISOString(),
};

// Create a valid session to use in tests
const payload = getFreshValidPayload();
const signature = await generateSignature(payload);
const response = await server.inject({
method: 'POST',
Expand Down
83 changes: 65 additions & 18 deletions src/__tests__/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {
createTestServer,
getFreshValidPayload,
cleanupTestServer,
generateSignature,
} from './utils/test-server';
import type { FastifyInstance } from 'fastify';
import { generateSignature } from '../crypto';

describe('Session Management', () => {
let server: FastifyInstance;
Expand All @@ -22,7 +22,26 @@ describe('Session Management', () => {

describe('Session Creation', () => {
it('should create a new session with valid payload', async () => {
const payload = getFreshValidPayload();
// First get a session request
const address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
const sessionResponse = await server.inject({
method: 'GET',
url: `/session/1/${address}`,
});

expect(sessionResponse.statusCode).toBe(200);
const sessionRequest = JSON.parse(sessionResponse.payload);

// Normalize timestamps to match database precision
const payload = {
...sessionRequest.session,
issuedAt: new Date(sessionRequest.session.issuedAt).toISOString(),
expirationTime: new Date(
sessionRequest.session.expirationTime
).toISOString(),
};

// Then create the session
const signature = await generateSignature(payload);
const response = await server.inject({
method: 'POST',
Expand All @@ -42,11 +61,12 @@ describe('Session Management', () => {
});

it('should reject invalid signature', async () => {
const payload = getFreshValidPayload();
const response = await server.inject({
method: 'POST',
url: '/session',
payload: {
payload: getFreshValidPayload(),
payload,
signature: 'invalid-signature',
},
});
Expand All @@ -55,13 +75,54 @@ describe('Session Management', () => {
const result = JSON.parse(response.payload);
expect(result).toHaveProperty('error');
});

it('should reject when message format does not match payload', async () => {
const payload = getFreshValidPayload();
const invalidPayload = {
...payload,
statement: 'Invalid statement',
};
const signature = await generateSignature(invalidPayload);

const response = await server.inject({
method: 'POST',
url: '/session',
payload: {
payload,
signature,
},
});

expect(response.statusCode).toBe(400);
const result = JSON.parse(response.payload);
expect(result).toHaveProperty('error');
});
});

describe('Session Verification', () => {
let sessionId: string;

beforeEach(async () => {
const payload = getFreshValidPayload();
// First get a session request
const address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
const sessionResponse = await server.inject({
method: 'GET',
url: `/session/1/${address}`,
});

expect(sessionResponse.statusCode).toBe(200);
const sessionRequest = JSON.parse(sessionResponse.payload);

// Normalize timestamps to match database precision
const payload = {
...sessionRequest.session,
issuedAt: new Date(sessionRequest.session.issuedAt).toISOString(),
expirationTime: new Date(
sessionRequest.session.expirationTime
).toISOString(),
};

// Then create the session
const signature = await generateSignature(payload);
const response = await server.inject({
method: 'POST',
Expand All @@ -72,24 +133,10 @@ describe('Session Management', () => {
},
});

if (response.statusCode !== 200) {
console.error(`Failed to create session: ${response.payload}`);
}
expect(response.statusCode).toBe(200);

const result = JSON.parse(response.payload);
if (!result.session) {
console.error(`Invalid response format: ${JSON.stringify(result)}`);
}
expect(result).toHaveProperty('session');

if (!result.session?.id) {
console.error(
`Session object missing ID: ${JSON.stringify(result.session)}`
);
}
expect(result.session).toHaveProperty('id');

sessionId = result.session.id;
});

Expand Down
Loading

0 comments on commit af3c7ee

Please sign in to comment.