From be44ecae30fa10901c16d203ccb2f30be805c07c Mon Sep 17 00:00:00 2001 From: oomokaro1 Date: Thu, 18 Jun 2026 10:55:42 +0100 Subject: [PATCH] feat: add issue templates + implement SEP-10 challenge-response auth - Add standard bug report and feature request templates - Add config.yml with security vulnerability link - Implement full SEP-10 challenge-response flow in auth.service.ts - Add RequestChallengeDto, VerifyChallengeDto, SignedTransactionDto - Add Stellar SDK signature verification with network passphrase support - Add challenge TTL, nonce generation, and expired challenge cleanup --- .github/ISSUE_TEMPLATE/bug_report.md | 102 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.md | 75 +++++++++ src/auth/auth.dto.ts | 38 ++++- src/auth/auth.service.ts | 195 +++++++++++++++++++++- 5 files changed, 408 insertions(+), 10 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8a9cf51 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,102 @@ +--- +name: "Bug Report" +about: "Report a bug in the OrbitStream Backend API" +title: "[BUG] " +labels: ["bug", "needs-triage"] +assignees: "" +--- + +# Bug Report + +## ๐Ÿ” Is this a regression? + + + +## ๐Ÿ“ Description + + + +## ๐Ÿ”„ Steps to Reproduce + +1. +2. +3. + +## โœ… Expected Behavior + + + +## โŒ Actual Behavior + + + +## ๐ŸŒ Environment + +- **OS**: [e.g., Ubuntu 22.04, macOS 14] +- **Node.js version**: [e.g., 20.11.0] +- **npm version**: [e.g., 10.2.4] +- **OrbitStream Backend version**: [e.g., commit hash or version tag] +- **Database**: [e.g., PostgreSQL 16] +- **Redis version**: [e.g., 7.2] +- **Stellar network**: [testnet / mainnet] + +## ๐Ÿ“‹ Configuration + + + +```env +NODE_ENV= +STELLAR_NETWORK= +PORT= +``` + +## ๐Ÿ“ฆ API Request/Response + + + +**Request:** +```bash +curl -X POST http://localhost:3001/v1/checkout/sessions \ + -H "Authorization: Bearer sk_test_..." \ + -H "Content-Type: application/json" \ + -d '{ "amount": 25.00, "asset": "USDC" }' +``` + +**Response:** +```json +{ + "statusCode": 500, + "message": "Internal server error" +} +``` + +## ๐Ÿ“‹ Database State + + + +## ๐Ÿ” Logs + + + +``` +[Paste logs here] +``` + +## ๐Ÿงช Test Case + + + +```typescript +// Minimal reproduction +``` + +## ๐Ÿ“Ž Additional Context + + + +## โœ… Checklist + +- [ ] I have searched existing issues and this is not a duplicate +- [ ] I am using the latest version of OrbitStream Backend +- [ ] I have included all relevant environment details +- [ ] I have redacted secrets and sensitive information diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..862b2a1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://github.com/your-org/OrbitStream/blob/main/orbitstream_docs/README.md + about: Read the OrbitStream documentation before opening an issue + - name: Security Vulnerability + url: https://github.com/your-org/OrbitStream/security/advisories/new + about: Report security vulnerabilities privately diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..d6bd84d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,75 @@ +--- +name: "Feature Request" +about: "Suggest a new feature or enhancement for the OrbitStream Backend" +title: "[FEAT] " +labels: ["enhancement", "needs-triage"] +assignees: "" +--- + +# Feature Request + +## ๐Ÿ“ Summary + + + +## ๐ŸŽฏ Problem Statement + + + +## ๐Ÿ’ก Proposed Solution + + + +## ๐Ÿ”„ Alternatives Considered + + + +## ๐Ÿ“‹ Acceptance Criteria + + + +- [ ] Criterion 1 +- [ ] Criterion 2 +- [ ] Criterion 3 + +## ๐Ÿ—๏ธ Implementation Notes + + + +### API Design (if applicable) + +```typescript +// Example API endpoint or SDK method +``` + +### Database Changes (if applicable) + +```sql +-- Example schema migration +``` + +### Affected Modules + +- [ ] Auth +- [ ] Merchants +- [ ] Checkout +- [ ] Payments +- [ ] Stellar +- [ ] Webhooks +- [ ] Monitoring +- [ ] Database Schema +- [ ] Other: ___ + +## ๐Ÿ“Ž Related Issues/PRs + + + +## ๐Ÿ“‹ Additional Context + + + +## โœ… Checklist + +- [ ] I have searched existing issues and this is not a duplicate +- [ ] I have clearly described the problem this feature solves +- [ ] I have provided acceptance criteria diff --git a/src/auth/auth.dto.ts b/src/auth/auth.dto.ts index ecbb417..e1cfd2e 100644 --- a/src/auth/auth.dto.ts +++ b/src/auth/auth.dto.ts @@ -1,6 +1,38 @@ -import { IsString, IsNotEmpty } from 'class-validator'; +import { IsString, IsNotEmpty, Matches, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class RequestChallengeDto { + @IsString() + @IsNotEmpty() + @Matches(/^G[A-Z0-9]{55}$/, { message: 'Must be a valid Stellar public key (starts with G, 56 chars)' }) + walletAddress: string; +} + +export class VerifyChallengeDto { + @IsString() + @IsNotEmpty() + @Matches(/^G[A-Z0-9]{55}$/, { message: 'Must be a valid Stellar public key (starts with G, 56 chars)' }) + walletAddress: string; + + @ValidateNested() + @Type(() => SignedTransactionDto) + @IsNotEmpty() + transaction: SignedTransactionDto; +} + +export class SignedTransactionDto { + @IsString() + @IsNotEmpty() + tx: string; + + @IsString() + @IsNotEmpty() + passphrase: string; +} export class WalletLoginDto { - @IsString() @IsNotEmpty() walletAddress: string; - @IsString() signature?: string; + @IsString() + @IsNotEmpty() + @Matches(/^G[A-Z0-9]{55}$/, { message: 'Must be a valid Stellar public key (starts with G, 56 chars)' }) + walletAddress: string; } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index f4593b9..e137e40 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,17 +1,198 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { Injectable, UnauthorizedException, BadRequestException, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { WalletLoginDto } from './auth.dto'; +import { RequestChallengeDto, VerifyChallengeDto } from './auth.dto'; +import * as StellarSdk from '@stellar/stellar-sdk'; +import * as crypto from 'crypto'; + +const CHALLENGE_TTL_MS = 5 * 60 * 1000; // 5 minutes +const NONCE_BYTES = 48; // 96-char hex nonce +const SERVER_ACCOUNT = + process.env.STELLAR_PLATFORM_ACCOUNT || process.env.PLATFORM_RECEIVING_ACCOUNT; + +interface PendingChallenge { + nonce: string; + serverAccountId: string; + createdAt: number; +} @Injectable() export class AuthService { - constructor(private readonly jwt: JwtService) {} + private readonly logger = new Logger(AuthService.name); + private readonly pendingChallenges = new Map(); + private serverKeypair: StellarSdk.Keypair | null = null; + + constructor(private readonly jwt: JwtService) { + this.initializeServerKeypair(); + } + + private initializeServerKeypair() { + const secret = process.env.STELLAR_PLATFORM_SECRET_KEY; + if (secret) { + try { + this.serverKeypair = StellarSdk.Keypair.fromSecret(secret); + this.logger.log('SEP-10 server keypair initialized from STELLAR_PLATFORM_SECRET_KEY'); + } catch { + this.logger.error('Invalid STELLAR_PLATFORM_SECRET_KEY โ€” SEP-10 challenges will fail'); + } + } + } + + private getServerAccountId(): string { + if (this.serverKeypair) { + return this.serverKeypair.publicKey(); + } + if (SERVER_ACCOUNT) { + return SERVER_ACCOUNT; + } + throw new BadRequestException( + 'SEP-10 not configured: set STELLAR_PLATFORM_SECRET_KEY or PLATFORM_RECEIVING_ACCOUNT', + ); + } - async walletLogin(dto: WalletLoginDto) { + private getServerKeypair(): StellarSdk.Keypair { + if (!this.serverKeypair) { + throw new BadRequestException( + 'SEP-10 not configured: set STELLAR_PLATFORM_SECRET_KEY environment variable', + ); + } + return this.serverKeypair; + } + + private getNetworkPassphrase(): string { + const network = process.env.STELLAR_NETWORK?.toUpperCase(); + if (network === 'MAINNET') { + return StellarSdk.Networks.PUBLIC; + } + return StellarSdk.Networks.TESTNET; + } + + private cleanExpiredChallenges() { + const now = Date.now(); + for (const [key, challenge] of this.pendingChallenges) { + if (now - challenge.createdAt > CHALLENGE_TTL_MS) { + this.pendingChallenges.delete(key); + } + } + } + + async requestChallenge(dto: RequestChallengeDto) { const { walletAddress } = dto; - if (!walletAddress?.startsWith('G')) { - throw new UnauthorizedException('Invalid Stellar wallet address'); + this.cleanExpiredChallenges(); + + const nonce = crypto.randomBytes(NONCE_BYTES).toString('hex'); + const serverAccountId = this.getServerAccountId(); + const now = Math.floor(Date.now() / 1000); + + try { + const serverAccount = await new StellarSdk.Horizon.Server( + process.env.STELLAR_HORIZON_URL || 'https://horizon-testnet.stellar.org', + ).loadAccount(serverAccountId); + + const transaction = new StellarSdk.TransactionBuilder(serverAccount, { + fee: StellarSdk.BASE_FEE, + networkPassphrase: this.getNetworkPassphrase(), + }) + .addOperation( + StellarSdk.Operation.manageData({ + source: walletAddress, + name: `${serverAccountId} auth`, + value: Buffer.from(nonce, 'hex'), + }), + ) + .setTimeout(CHALLENGE_TTL_MS / 1000) + .build(); + + const txEnvelope = transaction.toEnvelopeXDR('base64'); + + this.pendingChallenges.set(walletAddress, { + nonce, + serverAccountId, + createdAt: Date.now(), + }); + + return { + transaction: txEnvelope, + passphrase: this.getNetworkPassphrase(), + expiresAt: new Date(Date.now() + CHALLENGE_TTL_MS).toISOString(), + }; + } catch (err) { + this.logger.error(`Failed to create SEP-10 challenge: ${err.message}`); + throw new BadRequestException('Failed to create authentication challenge'); + } + } + + async verifyChallenge(dto: VerifyChallengeDto) { + const { walletAddress, transaction: txData } = dto; + const pending = this.pendingChallenges.get(walletAddress); + + if (!pending) { + throw new UnauthorizedException('No pending challenge for this wallet. Request a new one.'); + } + + if (Date.now() - pending.createdAt > CHALLENGE_TTL_MS) { + this.pendingChallenges.delete(walletAddress); + throw new UnauthorizedException('Challenge expired. Request a new one.'); + } + + try { + const transaction = StellarSdk.TransactionBuilder.fromXDR( + txData.tx, + txData.passphrase, + ); + + if (transaction.source != walletAddress) { + throw new UnauthorizedException('Transaction source does not match wallet address'); + } + + const networkPassphrase = this.getNetworkPassphrase(); + const serverKeypair = this.getServerKeypair(); + + const signatureHint = transaction.signatures[0].hint(); + const serverHint = serverKeypair.signatureHint(); + + if (Buffer.compare(signatureHint, serverHint) !== 0) { + throw new UnauthorizedException('Transaction not signed by server account'); + } + + const valid = transaction.verifySignatures(); + if (!valid) { + throw new UnauthorizedException('Transaction signature verification failed'); + } + + const operations = transaction.operations; + if (operations.length !== 1) { + throw new UnauthorizedException('Challenge transaction must have exactly one operation'); + } + + const op = operations[0] as StellarSdk.ManageDataOperation; + if (op.name !== `${pending.serverAccountId} auth`) { + throw new UnauthorizedException('Invalid manageData operation name'); + } + + const opNonce = Buffer.from(op.value as Buffer).toString('hex'); + if (opNonce !== pending.nonce) { + throw new UnauthorizedException('Nonce mismatch'); + } + + this.pendingChallenges.delete(walletAddress); + + const payload = { sub: walletAddress, walletAddress, authMethod: 'sep10' }; + return { + access_token: this.jwt.sign(payload), + wallet: walletAddress, + }; + } catch (err) { + if (err instanceof UnauthorizedException || err instanceof BadRequestException) { + throw err; + } + this.logger.error(`SEP-10 verification failed: ${err.message}`); + throw new UnauthorizedException('Transaction verification failed'); } - const payload = { sub: walletAddress, walletAddress }; + } + + async walletLogin(dto: { walletAddress: string }) { + const { walletAddress } = dto; + const payload = { sub: walletAddress, walletAddress, authMethod: 'wallet' }; return { access_token: this.jwt.sign(payload), wallet: walletAddress }; } }