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
98 changes: 97 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 11 additions & 11 deletions services/stellar-wallet/.env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Horizon endpoint for the Stellar network
# Server Configuration
PORT=3000

# Stellar Network Configuration
HORIZON_URL=https://horizon-testnet.stellar.org
# Soroban RPC endpoint (futurenet/testnet)
SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
# Port for the local Express server
PORT=3001
# 32-byte encryption key for AES-256-GCM
ENCRYPTION_KEY=

# Rate limiting settings
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
# Stellar Account Configuration
STELLAR_SECRET_KEY=your_stellar_secret_key_here

# Soroban Contract Configuration
SOROBAN_CONTRACT_ID=your_soroban_contract_id_here

STELLAR_SECRET_KEY=SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
SOROBAN_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# JWT Authentication
JWT_SECRET=your_secure_jwt_secret_key_at_least_32_characters_long
2 changes: 2 additions & 0 deletions services/stellar-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@
},
"dependencies": {
"@stellar/stellar-sdk": "^14.0.0-rc.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/supertest": "^6.0.3",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"express-rate-limit": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"sqlite3": "^5.1.7",
"supertest": "^7.1.4",
"zod": "^4.1.1"
Expand Down
82 changes: 82 additions & 0 deletions services/stellar-wallet/src/auth/jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import jwt from 'jsonwebtoken'
import { type Request, type Response, type NextFunction } from 'express'
import envs from '../config/envs'

export interface JwtPayload {
user_id: string
role: string
iat?: number
exp?: number
}

/**
* Generates a JWT token with user_id and role claims
* @param user_id - The user identifier
* @param role - The user role (defaults to 'user')
* @returns JWT token string
*/
export const generateToken = (user_id: string, role: string = 'user'): string => {
const payload: JwtPayload = {
user_id,
role,
}

return jwt.sign(payload, envs.JWT_SECRET, {
expiresIn: '24h',
algorithm: 'HS256',
})
}

/**
* Verifies and decodes a JWT token
* @param token - The JWT token to verify
* @returns Decoded JWT payload
* @throws Error if token is invalid or expired
*/
export const verifyToken = (token: string): JwtPayload => {
try {
const decoded = jwt.verify(token, envs.JWT_SECRET) as JwtPayload
return decoded
} catch (error) {
throw new Error(`Invalid or expired token: ${error}`)
}
}

/**
* Express middleware to validate JWT tokens in Authorization header
* @param req - Express request object
* @param res - Express response object
* @param next - Express next function
*/
export const jwtMiddleware = (req: Request, res: Response, next: NextFunction): void => {
const authHeader = req.headers.authorization

if (!authHeader) {
res.status(401).json({ error: 'Authorization header required' })
return
}

const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader

if (!token) {
res.status(401).json({ error: 'Bearer token required' })
return
}

try {
const decoded = verifyToken(token)
req.user = decoded
next()
} catch (error) {
res.status(401).json({ error: `Invalid or expired token: ${error}` })
}
}

// Extend Express Request interface to include user
declare global {
namespace Express {
interface Request {
user?: JwtPayload
}
}
}
87 changes: 87 additions & 0 deletions services/stellar-wallet/src/auth/webauthn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* WebAuthn verification module
* This is a simplified implementation for demonstration purposes.
* In a production environment, you would use a proper WebAuthn library
* like @simplewebauthn/server or similar.
*/

export interface WebAuthnCredential {
id: string
publicKey: string
user_id: string
counter: number
}

export interface WebAuthnAuthenticationResponse {
id: string
rawId: string
response: {
authenticatorData: string
clientDataJSON: string
signature: string
userHandle?: string
}
type: 'public-key'
}

/**
* Verifies a WebAuthn authentication response
* @param user_id - The user identifier
* @param authResponse - The WebAuthn authentication response
* @param credentials - Array of stored credentials for the user
* @returns Promise<boolean> - true if verification succeeds
*/
export const verifyWebAuthnAuthentication = async (
user_id: string,
authResponse: WebAuthnAuthenticationResponse,
credentials: WebAuthnCredential[],
): Promise<boolean> => {
try {
// Find the credential for this user
const credential = credentials.find((cred) => cred.user_id === user_id)
if (!credential) {
throw new Error('No credential found for user')
}

// In a real implementation, you would:
// 1. Parse the clientDataJSON
// 2. Verify the challenge
// 3. Parse the authenticatorData
// 4. Verify the signature using the stored public key
// 5. Check the counter to prevent replay attacks

// For this demo, we'll do basic validation
if (!authResponse.id || !authResponse.response.signature) {
throw new Error('Invalid authentication response')
}

// Mock verification - in production, this would be real cryptographic verification
// For now, we'll accept any response with valid structure
return true
} catch (error) {
console.error('WebAuthn verification failed:', error)
return false
}
}

/**
* Mock function to get user credentials from database
* In a real implementation, this would query the credentials table
* @param user_id - The user identifier
* @returns Promise<WebAuthnCredential[]> - Array of user credentials
*/
export const getUserCredentials = async (user_id: string): Promise<WebAuthnCredential[]> => {
// Mock implementation - in production, this would query the database
// For now, return a mock credential if user_id exists
if (user_id && user_id.length > 0) {
return [
{
id: 'mock-credential-id',
publicKey: 'mock-public-key',
user_id,
counter: 1,
},
]
}
return []
}
4 changes: 4 additions & 0 deletions services/stellar-wallet/src/config/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const envSchema = z.object({
process.env.NODE_ENV === 'test'
? z.string().default('CTEST_MOCK_CONTRACT_FOR_TESTING')
: z.string().min(1, 'SOROBAN_CONTRACT_ID is required'),
JWT_SECRET:
process.env.NODE_ENV === 'test'
? z.string().default('test-jwt-secret-key-for-testing')
: z.string().min(32, 'JWT_SECRET must be at least 32 characters long'),
})

// Validate and parse environment variables
Expand Down
5 changes: 5 additions & 0 deletions services/stellar-wallet/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import cors from 'cors'
import express, { type NextFunction, type Request, type Response } from 'express'
import envs from './config/envs'
import { authLimiter, kycLimiter, walletLimiter } from './middlewares/rate-limit'
import { authLoginRouter } from './routes/auth-login'
import { kycRouter } from './routes/kyc'
import { kycVerifyRouter } from './routes/kyc-verify'
import { walletRouter } from './routes/wallet'
Expand All @@ -21,6 +22,10 @@ app.post('/auth', authLimiter, (_req: Request, res: Response) => {
res.status(200).json({})
})

// Mount auth login routes
app.use('/auth', authLoginRouter)

// Protected routes - require JWT authentication
app.use('/kyc', kycLimiter, kycRouter)
app.use('/kyc', kycLimiter, kycVerifyRouter)

Expand Down
Loading