Skip to main content

HandCash Authentication Integration Guide

Prerequisites

  1. Create an application in the Developer Dashboard
  2. Configure callback URLs in your application settings
  3. Get your credentials (AppId and AppSecret)

Configure Callback URLs

Configure callback URLs in dashboard.handcash.io:
  1. Select your application
  2. Go to Settings or Configuration
  3. Set:
    • Authorization Success URL: Where users redirect after authorization (e.g., https://yourapp.com/auth/success)
    • Authorization Failed URL: Where users redirect if they decline (e.g., https://yourapp.com/auth/failed)
Important: Use HTTPS in production. For local development, http://localhost is allowed.

Key Generation

Required Dependency

"@noble/secp256k1": "2.2.3"

Generate Key Pair

import * as secp256k1 from "@noble/secp256k1"

export function generateAuthenticationKeyPair() {
  const privateKey = secp256k1.utils.randomSecretKey()
  const publicKey = secp256k1.getPublicKey(privateKey, true)
  
  return {
    privateKey: Buffer.from(privateKey).toString("hex"),
    publicKey: Buffer.from(publicKey).toString("hex"),
  }
}

Authentication Flow

Step 1: Generate Keys

const { privateKey, publicKey } = generateAuthenticationKeyPair()

Step 2: Store Private Key

Store the private key securely: Client: localStorage, sessionStorage, or in-memory
Server: Database, Redis, session, or JWT
// Client example
sessionStorage.setItem('handcash_private_key', privateKey)

// Server example
await db.users.update(userId, { handcashPrivateKey: privateKey })

Step 3: Redirect to HandCash

const authUrl = `https://handcash.io/connect?appId=YOUR_APP_ID&publicKey=${publicKey}`
window.location.href = authUrl
The user will log in and authorize your app on HandCash.
Recommended: Use State Parameter for CSRF ProtectionFor enhanced security, include a state parameter to prevent CSRF attacks. See the State Parameter Security section below.

Step 4: Handle Callback

After authorization, HandCash redirects to your Authorization Success URL. Retrieve the stored private key and validate:
import { getInstance, Connect } from '@handcash/sdk'

const privateKey = sessionStorage.getItem('handcash_private_key') // Retrieve stored key

const sdk = getInstance({ appId: 'YOUR_APP_ID', appSecret: 'YOUR_APP_SECRET' })
const client = sdk.getAccountClient(privateKey)

// Validate by fetching user profile
const { data: profile } = await Connect.getCurrentUserProfile({ client })
console.log('User authenticated:', profile.publicProfile.handle)

Step 5: Store Private Key for Session

Associate the private key with the user’s session:
req.session.handcashPrivateKey = privateKey
// or
await db.users.update(userId, { handcashPrivateKey: privateKey })

How It Works

  1. Generate a private/public key pair
  2. Send the public key to HandCash during authorization
  3. User authorizes your app on HandCash
  4. Use the private key for all API calls - it serves as your authentication credential
  5. HandCash validates the private key matches the authorized public key

Check User Permissions

const { data } = await Connect.getPermissions({ client })
console.log('User granted permissions:', data?.items)

Error Handling

try {
  const client = sdk.getAccountClient(privateKey)
  const profile = await Connect.getCurrentUserProfile({ client })
} catch (error) {
  if (error.message.includes('Invalid token')) {
    // Invalid private key, redirect to re-authenticate
    const { publicKey } = generateAuthenticationKeyPair()
    window.location.href = `https://handcash.io/connect?appId=${appId}&publicKey=${publicKey}`
  }
}

State Parameter Security

Recommended: Use the state parameter to prevent CSRF attacks and ensure the callback originated from your app session.

What This Means in Practice

1️⃣ App Generates State When Starting Connect

Generate a random, unguessable string and store it in your app session:
import crypto from 'crypto'

// Generate random state
const state = crypto.randomBytes(32).toString('hex')

// Store in session (server-side example)
req.session.handcashState = state

// Or client-side (sessionStorage)
sessionStorage.setItem('handcash_state', state)
Include the state in the redirect URL:
const authUrl = `https://handcash.io/connect?appId=YOUR_APP_ID&publicKey=${publicKey}&state=${state}`
window.location.href = authUrl

2️⃣ Wallet Requires State

The HandCash wallet will:
  • Require state to be present in the authorization request
  • Echo the same state back in the callback URL
  • Treat state as proof that the key came from the same app session that initiated connect

3️⃣ Wallet Redirects Back with Same State

After authorization, HandCash redirects to your callback URL with the state included:
https://yourapp.com/auth/success?userId=...&publicKey=...&state=STATE

4️⃣ App Validates State

Validate that the state matches the one stored in your session:
// Server-side example
app.get('/auth/success', async (req, res) => {
  const callbackState = req.query.state
  const sessionState = req.session.handcashState
  
  // Validate state matches
  if (callbackState !== sessionState) {
    return res.status(403).send('Invalid state parameter - possible CSRF attack')
  }
  
  // Clear state from session
  delete req.session.handcashState
  
  // Continue with authentication...
  const privateKey = req.session.handcashPrivateKey
  // ... rest of authentication flow
})
// Client-side example
const urlParams = new URLSearchParams(window.location.search)
const callbackState = urlParams.get('state')
const sessionState = sessionStorage.getItem('handcash_state')

if (callbackState !== sessionState) {
  console.error('Invalid state parameter - possible CSRF attack')
  return
}

// Clear state
sessionStorage.removeItem('handcash_state')

// Continue with authentication...
If state mismatch → reject the request - This indicates a potential CSRF attack.

Security Best Practices

  • Use state parameter - Always include and validate state to prevent CSRF attacks
  • Store private keys securely - Use encrypted storage or secure sessions
  • Use HTTPS - Always use HTTPS for redirect URLs
  • Validate private keys - Check validity before making API calls
  • Protect the private key - Treat it as sensitive authentication data