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
506 changes: 399 additions & 107 deletions backend/package-lock.json

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"name": "trustflow-backend",
"version": "1.0.0",
"description": "Node.js API for TrustFlow off-chain data.",
"main": "dist/index.js",
"main": "dist/main.js",
"license": "MIT",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"start": "node dist/main.js",
"dev": "ts-node-dev --respawn --transpile-only src/main.ts",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
Expand All @@ -25,7 +25,9 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.0.0",
"@sentry/node": "^8.55.2",
"@stellar/stellar-sdk": "^16.0.0",
"@stellar/stellar-sdk": "^12.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.4",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.0",
Expand All @@ -37,6 +39,7 @@
"@types/jest": "^29.5.0",
"@types/node": "^20.0.0",
"@types/passport-jwt": "^4.0.0",
"@types/stellar-sdk": "^0.11.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
Expand Down
243 changes: 243 additions & 0 deletions backend/src/auth/AUTH_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# JWT Authentication for Wallet Signatures - Implementation Documentation

## Overview

This implementation provides JWT-based authentication for the TrustFlow protocol using Stellar wallet signatures. Users authenticate by signing a nonce with their Freighter wallet, proving ownership of their Stellar address without exposing private keys.

## Architecture

### Components

1. **AuthModule** (`auth.module.ts`)
- Configures JWT and Passport modules
- Registers AuthService, AuthController, and JwtStrategy
- Exports AuthService for use in other modules

2. **AuthService** (`auth.service.ts`)
- Generates cryptographic challenges for wallet signing
- Verifies Stellar signatures using @stellar/stellar-sdk
- Issues JWT tokens upon successful authentication
- Validates JWT tokens for protected routes

3. **AuthController** (`auth.controller.ts`)
- `GET /auth/challenge` - Generates a challenge message for wallet signing
- `POST /auth/verify` - Verifies wallet signature and returns JWT token

4. **JwtStrategy** (`jwt.strategy.ts`)
- Passport strategy for JWT validation
- Extracts JWT from Authorization header
- Validates token signature and expiration

5. **JwtAuthGuard** (`auth.guard.ts`)
- Guards protected routes using JWT authentication
- Extends NestJS AuthGuard with custom error handling

6. **DTOs** (`dto/`)
- `ChallengeDto` - Validates wallet address format
- `VerifyDto` - Validates signature verification request
- `ChallengeResponseDto` - Challenge response schema
- `TokenResponseDto` - JWT token response schema

## Authentication Flow

### Step 1: Request Challenge
```bash
GET /auth/challenge?address=GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

**Response:**
```json
{
"challenge": "Sign this message to authenticate with TrustFlow: a1b2c3d4..."
}
```

### Step 2: Sign Challenge with Freighter Wallet
The user signs the challenge message using their Freighter wallet. The signature is returned as a base64-encoded string.

### Step 3: Verify Signature and Get JWT
```bash
POST /auth/verify
Content-Type: application/json

{
"address": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"signature": "SGVsbG8gV29ybGQh..."
}
```

**Response:**
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

### Step 4: Use JWT for Protected Routes
```bash
GET /escrows
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

## Security Features

1. **Challenge Expiration**: Challenges expire after 5 minutes to prevent replay attacks
2. **One-Time Use**: Each challenge can only be used once
3. **Stellar Signature Verification**: Uses @stellar/stellar-sdk for cryptographic verification
4. **JWT Expiration**: Tokens expire after 24 hours
5. **Address Validation**: Validates Stellar public key format (G-prefixed, 56 characters)
6. **Input Validation**: Uses class-validator for request validation

## Dependencies

### New Dependencies Added
- `@stellar/stellar-sdk@^16.0.1` - Stellar SDK for signature verification
- `class-validator@^0.14.4` - Input validation decorators
- `class-transformer@^0.5.1` - Object transformation

### Existing Dependencies Used
- `@nestjs/jwt@^10.0.0` - JWT token generation and validation
- `@nestjs/passport@^10.0.0` - Passport integration
- `passport-jwt@^4.0.1` - JWT strategy for Passport

## Environment Variables

Required environment variables:
```env
JWT_SECRET=your-secret-key-here # Secret for JWT signing
```

## Configuration

### JWT Configuration
- **Secret**: From `JWT_SECRET` environment variable (defaults to 'dev-secret-change-in-production')
- **Expiration**: 24 hours
- **Algorithm**: HS256

### Challenge Configuration
- **Expiration**: 5 minutes
- **Nonce Length**: 32 bytes (64 hex characters)

## Error Handling

### Common Errors

1. **Challenge Not Found**
- Status: 401
- Message: "Challenge not found or expired"
- Cause: Challenge was never requested or has expired

2. **Invalid Signature**
- Status: 401
- Message: "Invalid signature"
- Cause: Signature verification failed

3. **Invalid Address Format**
- Status: 400
- Message: "Invalid Stellar public key format"
- Cause: Address doesn't match Stellar public key pattern

4. **Missing Token**
- Status: 401
- Message: "Missing token"
- Cause: Authorization header not provided

5. **Invalid Token**
- Status: 401
- Message: "Invalid or expired token"
- Cause: Token is malformed, expired, or signature is invalid

## Testing

### Manual Testing

1. Start the development server:
```bash
npm run dev
```

2. Request a challenge:
```bash
curl "http://localhost:3001/auth/challenge?address=GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
```

3. Sign the challenge using Freighter wallet (client-side)

4. Verify signature and get token:
```bash
curl -X POST http://localhost:3001/auth/verify \
-H "Content-Type: application/json" \
-d '{"address":"G...","signature":"..."}'
```

5. Use token for protected routes:
```bash
curl http://localhost:3001/escrows \
-H "Authorization: Bearer <token>"
```

## Integration with Freighter Wallet

### Client-Side Implementation Example

```typescript
import * as freighter from '@stellar/freighter-api';

// 1. Get user's public key
const address = await freighter.getPublicKey();

// 2. Request challenge from backend
const response = await fetch(`/auth/challenge?address=${address}`);
const { challenge } = await response.json();

// 3. Sign challenge with Freighter
const signature = await freighter.signMessage(challenge, address);

// 4. Verify signature and get JWT
const verifyResponse = await fetch('/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, signature }),
});
const { token } = await verifyResponse.json();

// 5. Store token and use for authenticated requests
localStorage.setItem('jwt', token);
```

## Future Enhancements

1. **Rate Limiting**: Add per-wallet rate limiting on auth endpoints
2. **Token Refresh**: Implement refresh token mechanism
3. **Multi-Factor Authentication**: Add optional 2FA support
4. **Session Management**: Add token revocation and session tracking
5. **Auditing**: Log all authentication attempts for security monitoring

## Migration Notes

### Breaking Changes
- None - this is a new feature

### API Changes
- Added `/auth/challenge` endpoint
- Added `/auth/verify` endpoint
- Updated JWT token format (now uses standard JWT instead of custom format)

### Database Changes
- None - uses in-memory challenge storage (consider Redis for production)

## Production Considerations

1. **Challenge Storage**: Use Redis or similar for distributed challenge storage
2. **JWT Secret**: Use a strong, randomly generated secret
3. **HTTPS**: Always use HTTPS in production
4. **Rate Limiting**: Implement rate limiting to prevent abuse
5. **Monitoring**: Monitor authentication failures for security incidents
6. **Key Rotation**: Implement JWT secret rotation strategy

## Support

For issues or questions about this implementation, please refer to:
- TrustFlow API Documentation: http://localhost:3001/api/docs
- Stellar SDK Documentation: https://stellar.github.io/js-stellar-sdk/
- Freighter API Documentation: https://github.com/Credera-Freighter/freighter-api
50 changes: 11 additions & 39 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
ApiResponse,
ApiQuery,
ApiBody,
ApiBearerAuth,

Check warning on line 8 in backend/src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / Lint · TypeCheck · Test · Build

'ApiBearerAuth' is defined but never used
} from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { ChallengeDto } from './dto/challenge.dto';

Check warning on line 11 in backend/src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / Lint · TypeCheck · Test · Build

'ChallengeDto' is defined but never used
import { VerifyDto } from './dto/verify.dto';
import { ChallengeResponseDto } from './dto/challenge-response.dto';
import { TokenResponseDto } from './dto/token-response.dto';

@ApiTags('Authentication')
@Controller('auth')
Expand All @@ -29,18 +33,10 @@
@ApiResponse({
status: 200,
description: 'Challenge generated successfully',
schema: {
type: 'object',
properties: {
challenge: {
type: 'string',
example: 'Sign this message to authenticate with TrustFlow: 1234567890',
},
},
},
type: ChallengeResponseDto,
})
@ApiResponse({ status: 400, description: 'Address parameter required' })
getChallenge(@Query('address') address: string) {
getChallenge(@Query('address') address: string): ChallengeResponseDto {
if (!address) throw new Error('address required');
return { challenge: this.authService.generateChallenge(address) };
}
Expand All @@ -52,42 +48,18 @@
'Verifies the signed challenge and returns a JWT token for authenticated API access.',
})
@ApiBody({
type: VerifyDto,
description: 'Signature verification details',
schema: {
type: 'object',
required: ['address', 'signature'],
properties: {
address: {
type: 'string',
description: 'Stellar wallet address',
example: 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
},
signature: {
type: 'string',
description: 'Base64-encoded signature of the challenge message',
example: 'SGVsbG8gV29ybGQh...',
},
},
},
})
@ApiResponse({
status: 200,
description: 'Signature verified, JWT token generated',
schema: {
type: 'object',
properties: {
token: {
type: 'string',
description: 'JWT token for API authentication',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
},
type: TokenResponseDto,
})
@ApiResponse({ status: 401, description: 'Invalid signature' })
verify(@Body() body: { address: string; signature: string }) {
const valid = this.authService.verifySignature(body.address, body.signature);
verify(@Body() verifyDto: VerifyDto): TokenResponseDto {
const valid = this.authService.verifySignature(verifyDto.address, verifyDto.signature);
if (!valid) throw new Error('Invalid signature');
return { token: this.authService.generateToken(body.address) };
return { token: this.authService.generateToken(verifyDto.address) };
}
}
31 changes: 11 additions & 20 deletions backend/src/auth/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import * as crypto from 'crypto';
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) throw new UnauthorizedException('Missing token');
const token = auth.slice(7);
const [payload, sig] = token.split('.');
if (!payload || !sig) throw new UnauthorizedException('Invalid token format');
const expected = crypto
.createHmac('sha256', process.env.JWT_SECRET || 'dev')
.update(payload)
.digest('base64');
if (sig !== expected) throw new UnauthorizedException('Invalid signature');
try {
req.user = JSON.parse(Buffer.from(payload, 'base64').toString());
} catch {
throw new UnauthorizedException('Malformed token');
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}

handleRequest(err: any, user: any, info: any) {

Check warning on line 10 in backend/src/auth/auth.guard.ts

View workflow job for this annotation

GitHub Actions / Lint · TypeCheck · Test · Build

'info' is defined but never used. Allowed unused args must match /^_/u
if (err || !user) {
throw err || new UnauthorizedException('Invalid or expired token');
}
return true;
return user;
}
}
Loading
Loading