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: 2 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ jobs:
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: npm ci
working-directory: frontend
run: npm ci --include=optional

- name: Lint
run: npm run lint
Expand Down Expand Up @@ -62,11 +60,9 @@ jobs:
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: backend/package-lock.json

- name: Install dependencies
run: npm ci
working-directory: backend
run: npm ci --include=optional

- name: Generate Prisma Client
run: npx prisma generate
Expand Down
218 changes: 218 additions & 0 deletions backend/docs/AUTHENTICATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# Authentication Middleware (SEP-10)

FlowFi API uses Stellar signed transactions for authentication, following the SEP-10 (Stellar Web Authentication) pattern.

## Overview

The authentication middleware verifies that requests come from legitimate Stellar wallet owners by validating signed transactions. This provides secure, wallet-based authentication without traditional username/password schemes.

## How It Works

1. **Client Side**: The client creates a Stellar transaction, signs it with their private key, and encodes it as XDR
2. **Server Side**: The middleware extracts the Bearer token, decodes the XDR, and verifies the signature
3. **User Attachment**: If valid, the user's public key is attached to `req.user`

## Using the Middleware

### Protected Routes

Apply `authMiddleware` to any route that requires authentication:

```typescript
import { authMiddleware } from '../middleware/auth.middleware.js';
import { Router } from 'express';

const router = Router();

// Protected endpoint
router.get('/me', authMiddleware, getCurrentUser);
```

### Optional Authentication

Use `optionalAuthMiddleware` for routes where authentication is optional:

```typescript
import { optionalAuthMiddleware } from '../middleware/auth.middleware.js';

router.get('/streams', optionalAuthMiddleware, getStreams);
```

## Request Format

### Authorization Header

```
Authorization: Bearer <signed_transaction_xdr>
```

### Example

```bash
curl -X GET http://localhost:3001/v1/users/me \
-H "Authorization: Bearer AAAAAgAAAAC..."
```

## Transaction Requirements

The signed transaction must meet these requirements:

1. **Source Account**: Must be the user's Stellar public key
2. **Valid Signature**: Signature must be valid for the source account
3. **Time Bounds** (optional but recommended): Transaction should include time bounds to prevent replay attacks
4. **Network**: Must match the configured Stellar network (testnet or mainnet)

## Example: Creating a Signed Transaction (Client Side)

```javascript
import * as StellarSdk from '@stellar/stellar-sdk';

// Create a keypair (in real app, this comes from the user's wallet)
const keypair = StellarSdk.Keypair.fromSecret('S...');

// Get the current network passphrase
const networkPassphrase = StellarSdk.Networks.TESTNET;

// Create a simple transaction for authentication
const account = await server.loadAccount(keypair.publicKey());
const transaction = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase,
})
.addOperation(
StellarSdk.Operation.manageData({
name: 'auth',
value: crypto.randomBytes(32), // Random challenge
})
)
.setTimeout(300) // 5 minutes validity
.build();

// Sign the transaction
transaction.sign(keypair);

// Encode to XDR for the Bearer token
const xdr = transaction.toEnvelope().toXDR('base64');

// Use in Authorization header
const response = await fetch('http://localhost:3001/v1/users/me', {
headers: {
'Authorization': `Bearer ${xdr}`
}
});
```

## Error Responses

### 401 Unauthorized - Missing Token

```json
{
"error": "Unauthorized",
"message": "Missing or invalid Authorization header. Expected format: Bearer <signed_transaction>"
}
```

### 401 Unauthorized - Invalid Signature

```json
{
"error": "Unauthorized",
"message": "Invalid or expired signature"
}
```

## Security Features

1. **Signature Verification**: Cryptographically verifies the transaction was signed by the claimed public key
2. **Time Bounds**: Supports transaction time bounds to prevent replay attacks
3. **Network Validation**: Ensures transactions match the configured Stellar network
4. **No Password Storage**: No sensitive credentials stored on the server

## Configuration

### Environment Variables

```env
# Stellar Network (testnet or mainnet)
STELLAR_NETWORK=testnet

# For mainnet, use:
# STELLAR_NETWORK=mainnet
```

### Network Passphrases

- **Testnet**: `Test SDF Network ; September 2015`
- **Mainnet**: `Public Global Stellar Network ; September 2015`

## Independence from Database

Per issue #74 requirements, the middleware operates independently of the database:

- **User Exists**: Returns user data from database
- **User Missing**: Returns in-memory user object with `publicKey` and empty arrays
- **Never Blocks**: Authentication succeeds based solely on valid signature

Example in-memory response:

```json
{
"publicKey": "GABC123...",
"sentStreams": [],
"receivedStreams": [],
"inMemory": true
}
```

## TypeScript Types

### AuthUser

```typescript
interface AuthUser {
publicKey: string;
id?: string;
}
```

### AuthenticatedRequest

```typescript
interface AuthenticatedRequest extends Request {
user: AuthUser;
}
```

### Usage in Controllers

```typescript
import type { AuthenticatedRequest } from '../types/auth.types.js';

export const getCurrentUser = async (req: Request, res: Response) => {
const authReq = req as AuthenticatedRequest;
const { publicKey } = authReq.user;

// Use publicKey...
};
```

## Testing

Test the middleware with a valid signed transaction:

```bash
# 1. Create a test transaction (use stellar-sdk)
# 2. Sign it with a test keypair
# 3. Encode to XDR
# 4. Send request

curl -X GET http://localhost:3001/v1/users/me \
-H "Authorization: Bearer <your_signed_transaction_xdr>"
```

## Related Documentation

- [Stellar SEP-10 Specification](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md)
- [Stellar SDK Documentation](https://stellar.github.io/js-stellar-sdk/)
- [Swagger API Docs](http://localhost:3001/api-docs)
3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@
"license": "ISC",
"dependencies": {
"@prisma/adapter-pg": "^7.4.1",
"@stellar/stellar-sdk": "^13.1.0",
"@stellar/stellar-sdk": "^14.5.0",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"express-rate-limit": "^8.2.1",
"pg": "^8.18.0",
"stellar-sdk": "^13.3.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"winston": "^3.11.0",
Expand Down
11 changes: 8 additions & 3 deletions backend/src/config/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@ export interface SandboxConfig {
export function getSandboxConfig(): SandboxConfig {
const enabled = process.env.SANDBOX_MODE_ENABLED === 'true';
const databaseUrl = process.env.SANDBOX_DATABASE_URL;
return {

const config: SandboxConfig = {
enabled,
databaseUrl: databaseUrl || undefined,
allowHeader: process.env.SANDBOX_ALLOW_HEADER !== 'false', // Default: true
allowQueryParam: process.env.SANDBOX_ALLOW_QUERY_PARAM !== 'false', // Default: true
headerName: process.env.SANDBOX_HEADER_NAME || 'X-Sandbox-Mode',
queryParamName: process.env.SANDBOX_QUERY_PARAM_NAME || 'sandbox',
};

if (databaseUrl) {
config.databaseUrl = databaseUrl;
}

return config;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions backend/src/config/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ See [Sandbox Mode Documentation](../docs/SANDBOX_MODE.md) for details.`,
},
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'Stellar Signed Transaction (XDR)',
description: 'Stellar SEP-10 authentication. Provide a signed transaction envelope in XDR format.'
}
},
schemas: {
User: {
type: 'object',
Expand Down
44 changes: 43 additions & 1 deletion backend/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Request, Response, NextFunction } from 'express';
import { prisma } from '../lib/prisma.js';
import logger from '../logger.js';
import { registerUserSchema } from '../validators/user.validator.js';
import type { AuthenticatedRequest } from '../types/auth.types.js';

/**
* Register a new wallet public key
Expand Down Expand Up @@ -37,7 +38,7 @@ export const registerUser = async (req: Request, res: Response, next: NextFuncti
*/
export const getUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const { publicKey } = req.params;
const publicKey = req.params.publicKey as string;

const user = await prisma.user.findUnique({
where: { publicKey },
Expand Down Expand Up @@ -90,3 +91,44 @@ export const getUserEvents = async (req: Request, res: Response, next: NextFunct
next(error);
}
};

/**
* Get current authenticated user
* Requires authMiddleware to be applied
*/
export const getCurrentUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const authReq = req as AuthenticatedRequest;
const { publicKey } = authReq.user;

// Try to get user from database
let user = await prisma.user.findUnique({
where: { publicKey },
include: {
sentStreams: {
take: 10,
orderBy: { createdAt: 'desc' }
},
receivedStreams: {
take: 10,
orderBy: { createdAt: 'desc' }
}
}
});

// If user doesn't exist in database, create in-memory user object
if (!user) {
logger.info(`User ${publicKey} authenticated but not in database, returning in-memory user`);
return res.status(200).json({
publicKey,
sentStreams: [],
receivedStreams: [],
inMemory: true
});
}

return res.status(200).json(user);
} catch (error) {
next(error);
}
};
11 changes: 3 additions & 8 deletions backend/src/lib/prisma-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,10 @@ export function getSandboxPrisma(): PrismaClient {
}

const sandboxPrisma = new PrismaClient({
datasources: {
db: {
url: databaseUrl,
},
},
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
} as any);

if (process.env.NODE_ENV !== 'production') {
globalForSandboxPrisma.sandboxPrisma = sandboxPrisma;
Expand Down
Loading
Loading