Problem Statement
Authenticate node administrative personnel passwordlessly. The current session management relies on shared API tokens that are long-lived, stored in plaintext configuration files, and cannot be revoked granularly. A compromised token gives an attacker full administrative access until manual rotation. Node operators need a passwordless authentication flow that uses their existing Stellar wallet keypair to sign a server-generated challenge, proving control of the identity without revealing the secret key.
Technical Bounds & Invariants
- Challenge nonce: 32 cryptographically random bytes, single-use, expires after 5 minutes
- Wallet signature scheme: Ed25519 (Stellar native); signature is 64 bytes
- JWT lifetime: 1 hour with refresh token (7 days, single-use rotation)
- Stellar StrKey encoding: uses
G... prefix for public keys (base32, 56 chars)
- Challenge must be bound to client IP and User-Agent to mitigate token theft
Codebase Navigation Guide
- Primary target:
/src/api/auth/challenge.ts
- Session middleware:
/src/api/auth/session.ts — verifySession() at line 30
- JWT manager:
/src/api/auth/jwt_manager.ts
- Stellar SDK key utils:
/node_modules/@stellar/stellar-sdk/lib/keypair.js
Step-by-Step Resolution Blueprint
- Implement
POST /api/v1/auth/challenge that generates a random 32-byte nonce via crypto.randomBytes(32), stores (nonce, ip, user_agent, expires_at) in Redis with 5-minute TTL (key: challenge:{nonce_hex}), and returns { nonce: base64(nonce), serverId: "VeriNode-Backend" }
- Implement
POST /api/v1/auth/verify that accepts { nonce, publicKey, signature }; verify: (a) nonce exists in Redis and not expired, (b) IP matches stored IP, (c) publicKey matches G... valid StrKey format (verify using StrKey.isValidEd25519PublicKey), (d) verify the Ed25519 signature of SHA256(nonce || serverId) using Keypair.fromPublicKey(publicKey).verify()
- On successful verification: delete the nonce from Redis (prevent replay), issue a JWT access token (1h) and a JWT refresh token (7d) signed with a server-side RS256 key pair; store the refresh token hash in Redis (key:
refresh:{token_hash}, TTL: 7d)
- Implement
POST /api/v1/auth/refresh that accepts { refreshToken }, verifies it against the stored hash, issues a new access token and a rotated refresh token (single-use rotation), and invalidates the old refresh token hash
- Add a
SessionMiddleware that extracts the JWT from the Authorization: Bearer <token> header, verifies the signature using the public RSA key, checks expiry, decodes the node_id claim, and attaches it to req.nodeId for downstream handlers
- Add rate limiting on the challenge endpoint (10/ip/minute) and verify endpoint (5/ip/minute) to prevent brute-force enumeration of valid nonces
- Write integration tests using
@stellar/stellar-sdk to: (a) generate a random keypair, (b) complete the challenge/verify flow, (c) use the JWT to call an authenticated endpoint, (d) verify refresh token rotation
Problem Statement
Authenticate node administrative personnel passwordlessly. The current session management relies on shared API tokens that are long-lived, stored in plaintext configuration files, and cannot be revoked granularly. A compromised token gives an attacker full administrative access until manual rotation. Node operators need a passwordless authentication flow that uses their existing Stellar wallet keypair to sign a server-generated challenge, proving control of the identity without revealing the secret key.
Technical Bounds & Invariants
G...prefix for public keys (base32, 56 chars)Codebase Navigation Guide
/src/api/auth/challenge.ts/src/api/auth/session.ts— verifySession() at line 30/src/api/auth/jwt_manager.ts/node_modules/@stellar/stellar-sdk/lib/keypair.jsStep-by-Step Resolution Blueprint
POST /api/v1/auth/challengethat generates a random 32-byte nonce viacrypto.randomBytes(32), stores(nonce, ip, user_agent, expires_at)in Redis with 5-minute TTL (key:challenge:{nonce_hex}), and returns{ nonce: base64(nonce), serverId: "VeriNode-Backend" }POST /api/v1/auth/verifythat accepts{ nonce, publicKey, signature }; verify: (a) nonce exists in Redis and not expired, (b) IP matches stored IP, (c)publicKeymatchesG...valid StrKey format (verify usingStrKey.isValidEd25519PublicKey), (d) verify the Ed25519 signature ofSHA256(nonce || serverId)usingKeypair.fromPublicKey(publicKey).verify()refresh:{token_hash}, TTL: 7d)POST /api/v1/auth/refreshthat accepts{ refreshToken }, verifies it against the stored hash, issues a new access token and a rotated refresh token (single-use rotation), and invalidates the old refresh token hashSessionMiddlewarethat extracts the JWT from theAuthorization: Bearer <token>header, verifies the signature using the public RSA key, checks expiry, decodes thenode_idclaim, and attaches it toreq.nodeIdfor downstream handlers@stellar/stellar-sdkto: (a) generate a random keypair, (b) complete the challenge/verify flow, (c) use the JWT to call an authenticated endpoint, (d) verify refresh token rotation