VeztaVezta

Authentication

Wallet-based auth, JWT tokens, and refresh flow

Vezta uses wallet-based authentication supporting both Solana and EVM chains. Instead of passwords, users sign a message with their wallet to prove ownership, and the backend issues JWT tokens.

Auth Flow

  1. Request nonce -- Client sends wallet address and chain, backend returns a unique nonce
  2. Sign message -- Client signs the nonce with their wallet (Phantom, MetaMask, etc.)
  3. Verify signature -- Backend verifies the signature, creates or finds the user, and issues tokens
  4. Use access token -- Client includes the JWT in the Authorization: Bearer header for subsequent requests
  5. Refresh when expired -- Access tokens expire after 15 minutes; use the refresh endpoint to get a new one

Endpoints

MethodPathAuthDescription
GET/api/v1/auth/wallet/noncePublicRequest a nonce for wallet signature (query: address, chain)
POST/api/v1/auth/walletPublicVerify wallet signature and receive JWT
POST/api/v1/auth/googlePublicSign in with a Google OAuth access token
POST/api/v1/auth/refreshPublicRefresh an expired access token
POST/api/v1/auth/activate-keyPublicActivate a beta access key
POST/api/v1/auth/logoutBearerLogout and invalidate all refresh tokens

Request Nonce

Generate a nonce that the user must sign with their wallet. Rate-limited to 20 req/min per IP.

GET /api/v1/auth/wallet/nonce?address=7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU&chain=solana
// Response 200
{
  "nonce": "vezta-auth-1711234567890-abc123def456"
}

Verify Wallet

Submit the signed nonce to authenticate. On success, the backend returns an access token in the response body and sets an httpOnly refresh_token cookie. A non-httpOnly logged_in=1 marker cookie is also set for frontend route protection. Rate-limited to 10 req/min per IP.

// POST /api/v1/auth/wallet
{
  "address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "chain": "solana",
  "signature": "3ASDf...base58signature",
  "nonce": "vezta-auth-1711234567890-abc123def456",
  "referralCode": "FRIEND123"
}
// Response 201
{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "clxyz...",
    "walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "chain": "solana",
    "username": null,
    "avatarUrl": null
  }
}

Refresh Token

Exchange a refresh token for a new access token. The backend accepts the refresh token either in the request body or from the httpOnly cookie. Token rotation is applied -- each refresh issues a new refresh token and invalidates the old one.

// POST /api/v1/auth/refresh
{
  "refreshToken": "previous-refresh-token-value"
}
// Response 200
{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "new-refresh-token-value",
  "user": {
    "id": "clxyz...",
    "walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "chain": "solana"
  }
}

Token Details

  • Access token: JWT, 15-minute expiry, stored in memory (not localStorage)
  • Refresh token: Opaque token, 90-day sliding expiry (each refresh pushes expiresAt forward), stored as httpOnly cookie with SameSite=None and Secure=true in production
  • JWT payload: signed as { sub, walletAddress, chain } (where sub is the user id); the strategy maps it to { id, walletAddress, chain } on validated requests

Notes

  • The referralCode field in the verify request is optional and only used on first-time sign-up
  • The chain field accepts solana or evm
  • All auth endpoints except logout are public (no JWT required)
  • /api/v1/auth/activate-key (rate-limited to 5 req/min) redeems a beta access key
  • /api/v1/auth/refresh is rate-limited to 60 req/min keyed by token; /api/v1/auth/google to 10 req/min
  • Failed signature verification returns a 401 Unauthorized error

On this page