API Authentication

Authentication Flow

TimeProof uses Sign-In with Ethereum (SIWE) — an open standard for wallet-based authentication. The entire flow happens in three steps, and the result is a session cookie that authenticates all subsequent requests.

Step 1: Request a Nonce

GET /api/auth/nonce

The server generates a random nonce (a one-time challenge string) and returns it. This nonce prevents replay attacks — each authentication attempt requires a fresh one.

{
  "nonce": "a1b2c3d4e5f6..."
}

Step 2: Create and Sign a SIWE Message

Construct a SIWE message that includes the nonce, your wallet address, the domain, and a timestamp. Then sign it with your wallet’s private key.

The SIWE message format follows EIP-4361:

app.timeprooflabs.com wants you to sign in with your Ethereum account:
0xYourWalletAddress

Sign in to TimeProof

URI: https://app.timeprooflabs.com
Version: 1
Chain ID: 137
Nonce: a1b2c3d4e5f6...
Issued At: 2026-02-03T12:00:00Z

Sign this message using your wallet (MetaMask, WalletConnect, or programmatically with ethers.js).

Step 3: Submit the Signature

POST /api/auth/verify
Content-Type: application/json
{
  "message": "app.timeprooflabs.com wants you to sign in...",
  "signature": "0x..."
}

The server verifies the signature matches the wallet address in the message. On success, it sets an HTTP-only session cookie and returns:

{
  "success": true,
  "address": "0xYourWalletAddress",
  "chainId": 80002
}

From this point, every request that includes the session cookie is authenticated as that wallet address.

Session Management

Checking Your Session

GET /api/auth/session

Returns the current session state. If the session is valid, you’ll see your wallet address and chain ID. If expired or not authenticated, you’ll get a response indicating no active session.

Signing Out

POST /api/auth/signout

Destroys the session and clears the cookie. The alias POST /api/auth/logout does the same thing.

Session Expiration

Sessions expire after a period of inactivity. When you receive a 401 Unauthorized response, re-authenticate by repeating the three-step flow. The session cookie is HTTP-only, meaning JavaScript cannot access it directly — this is a security feature that prevents XSS attacks from stealing sessions.

Code Examples

JavaScript (ethers.js v6)

import { BrowserProvider } from 'ethers';

// Step 1: Get nonce
const nonceRes = await fetch('/api/auth/nonce', {
  credentials: 'include'
});
const { nonce } = await nonceRes.json();

// Step 2: Create and sign SIWE message
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();

const message = `app.timeprooflabs.com wants you to sign in with your Ethereum account:
${address}

Sign in to TimeProof

URI: ${window.location.origin}
Version: 1
Chain ID: 137
Nonce: ${nonce}
Issued At: ${new Date().toISOString()}`;

const signature = await signer.signMessage(message);

// Step 3: Verify
const verifyRes = await fetch('/api/auth/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include',
  body: JSON.stringify({ message, signature })
});

const result = await verifyRes.json();
// { success: true, address: "0x...", chainId: 80002 }

Python (web3.py)

import requests
from web3 import Web3
from eth_account.messages import encode_defunct
from datetime import datetime

session = requests.Session()
base = "http://localhost:5050"

# Step 1: Get nonce
nonce = session.get(f"{base}/api/auth/nonce").json()["nonce"]

# Step 2: Sign message
w3 = Web3()
account = w3.eth.account.from_key("0xYOUR_PRIVATE_KEY")

message_text = f"""app.timeprooflabs.com wants you to sign in with your Ethereum account:
{account.address}

Sign in to TimeProof

URI: {base}
Version: 1
Chain ID: 137
Nonce: {nonce}
Issued At: {datetime.utcnow().isoformat()}Z"""

msg = encode_defunct(text=message_text)
signed = account.sign_message(msg)

# Step 3: Verify
result = session.post(f"{base}/api/auth/verify", json={
    "message": message_text,
    "signature": signed.signature.hex()
}).json()

# Now all requests via 'session' are authenticated

Security Considerations

Why SIWE Is Secure

Your private key never leaves your wallet. The server receives only a signature — a mathematical proof that you control the wallet. Even if the signature is intercepted, it cannot be reused because:

  • The nonce is single-use
  • The message includes a timestamp and expiration
  • The signature is bound to a specific domain

The session cookie is configured with:

AttributeValuePurpose
HttpOnlytruePrevents JavaScript access (XSS protection)
Securetrue (production)Cookie sent only over HTTPS
SameSiteStrictPrevents CSRF attacks
Path/apiCookie sent only to API routes

Protecting Your Session

  • Don’t share your session cookie with untrusted code
  • Sign out when you’re done (especially on shared devices)
  • If you suspect compromise, sign out immediately — this invalidates the session server-side

Troubleshooting Authentication

ProblemSolution
401 on every requestSession expired — re-authenticate
Signature verification failsEnsure the message text matches exactly what was signed
Nonce expiredRequest a fresh nonce — they’re single-use and time-limited
CORS errorsEnsure credentials: 'include' is set in fetch requests
Cookie not sentCheck that SameSite policy matches your domain setup

Use the live product for timestamping and verification.

The company site owns the technical reference. The app handles runtime workflows.