Manage Auth Tokens Programmatically
This guide explains how to programmatically manage authentication tokens in Prividium using Sign-In With Ethereum (SIWE). Tokens are required for authenticated access to the Prividium API and RPC endpoints.
Token Lifecycle Overview
- Issue: Request a SIWE message, sign it with your wallet, and exchange it for a session token
- Check: Query the current session endpoint to verify token validity and expiration
- Refresh: When expired, repeat the SIWE flow to get a new token
Prerequisites
You need a private key and its derived address. You can use cast to generate a new keypair:
$ cast wallet new
Successfully created new keypair.
Address: 0x9317a78Dc814297Defe678918A84084C427a1e59
Private key: 0xd9ad2e2da24635777b184f591ba8cf6aaf428879034030022f088a68f2390b05The private key is used to sign messages. Before authenticating, the address must be associated with a user account.
To link an address to a user, log in to the Admin Panel and add the address to the user account that will be used for programmatic access.
Step 1: Request a SIWE Message
To get a token, first request a SIWE message from the API.
Endpoint
POST /api/siwe-messages
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
address | string | Yes | The wallet address (hex format, e.g., 0x...) |
domain | string | Yes | Your user panel domain (i.e. user-panel.prividium.com) |
Example Request
curl -X POST http://localhost:8000/api/siwe-messages \
-H "Content-Type: application/json" \
-d '{
"address": "0xYourWalletAddress",
"domain": "localhost:3000"
}'Example Response
{
"nonce": "abc123xyz",
"msg": "localhost:3000 wants you to sign in with your Ethereum account:\n0xYourWalletAddress\n\nLogin to Prividium™ chain\n\nURI: prividium:access\nVersion: 1\nChain ID: 260\nNonce: abc123xyz\nIssued At: 2024-01-15T10:00:00.000Z\nExpiration Time: 2024-01-15T10:05:00.000Z",
"nonceToken": "eyJhbGciOiJIUzI1NiJ9..."
}Capture both msg and nonceToken — you need both in Step 2.
Step 2: Sign and Exchange for Token
Sign the message with your wallet and send it to the login endpoint.
Endpoint
POST /api/auth/login/crypto-native
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | The SIWE message from Step 1 |
signature | string | Yes | The wallet signature (hex format) |
nonceToken | string | Yes | The nonce token from Step 1 response |
Example Request
curl -X POST http://localhost:8000/api/auth/login/crypto-native \
-H "Content-Type: application/json" \
-d '{
"message": "<siwe_message_from_step_1>",
"signature": "0xYourSignature...",
"nonceToken": "<nonce_token_from_step_1>"
}'Example Response
{
"token": "your-session-token-here",
"expiresAt": "2024-01-15T18:00:00.000Z"
}The token is your session token for authenticated API calls. The expiresAt field indicates when the token expires.
Step 3: Refresh Expired Tokens
When a token expires, repeat the SIWE flow (Steps 1-2) to get a new token. There is no dedicated refresh endpoint; the full authentication flow must be repeated.
Best Practice
Check the token expiration before each API interaction. The expiration time can be safely cached in memory, and can be
verified again at any time using the GET /api/auth/current-session endpoint.
- Save expiration time locally near the token value.
- If the token is expired or about to expire, initiate a new SIWE flow.
- Update your stored token with the new one.
Check Token Expiration
Verify token validity and expiration at any time using the current-session endpoint:
Endpoint
GET /api/auth/current-session
Headers
| Header | Value |
|---|---|
Authorization | Bearer <your-session-token> |
Example Request
curl -X GET http://localhost:8000/api/auth/current-session \
-H "Authorization: Bearer <your-session-token>"Example Response
{
"type": "user",
"expiresAt": "2024-01-15T18:00:00.000Z"
}| Field | Type | Description |
|---|---|---|
type | string | Session type: user (or legacy tenant / service when those features are enabled) |
expiresAt | string | ISO 8601 timestamp when the token expires |
Response Status Codes
Prividium follows standard HTTP status code conventions for the REST API:
| Status | Description |
|---|---|
| 200-299 | Success. No action required. |
| 401 | Authentication invalid. Invalidate the token and re-authenticate. |
| 403 | User lacks access to the requested resource. Token remains valid. |
The JSON-RPC standard does not use HTTP 401 and 403 for authentication errors. Prividium uses custom RPC error codes instead:
| RPC Error Code | Description |
|---|---|
| -32090 | Authentication failed. Re-authenticate to obtain a new token. |
| -32001 | User lacks access to the requested resource. Token is valid. |
Example: Complete Authentication Script
Using REST programmatically
import type { Hex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
const PRIVIDIUM_API_BASE_URL = process.env.PRIVIDIUM_API_BASE_URL ?? 'http://localhost:8000';
const DOMAIN = process.env.DOMAIN ?? 'localhost:3000';
const PRIVATE_KEY = process.env.PRIVATE_KEY;
let currentToken: string;
let tokenExpiresAt = new Date(0);
async function apiCall(path: string, init = {}) {
const url = new URL(path, PRIVIDIUM_API_BASE_URL);
const response = await fetch(url, init);
if (!response.ok) {
const detail = await response.text();
throw new Error(`Request failed (${response.status}) ${path}: ${detail}`);
}
return response.json();
}
async function authenticate() {
if (!PRIVATE_KEY) throw new Error('Missing PRIVATE_KEY in environment');
const account = privateKeyToAccount(PRIVATE_KEY as Hex);
// 1. Request a SIWE message
const siweData = await apiCall('/api/siwe-messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
address: account.address,
domain: DOMAIN
})
});
// 2. Sign the returned message
const signature = await account.signMessage({
message: siweData.msg
});
// 3. Exchange for a session token
const loginData = await apiCall('/api/auth/login/crypto-native', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: siweData.msg,
signature,
nonceToken: siweData.nonceToken
})
});
// 4. Use the token in Authorization: Bearer <token> headers
currentToken = loginData.token;
tokenExpiresAt = new Date(loginData.expiresAt);
console.log('Token expires at:', tokenExpiresAt.toISOString());
return currentToken;
}
async function ensureAuthenticated() {
if (!currentToken || new Date() >= tokenExpiresAt) {
console.log('Session missing or expired. Authenticating...');
await authenticate();
}
}
async function fetchUserProfile() {
await ensureAuthenticated();
const user = await apiCall('/api/profiles/me', {
method: 'GET',
headers: {
Authorization: `Bearer ${currentToken}`
}
});
console.log('Authenticated user:', user.displayName ?? user.id);
return user;
}
async function callRpc(method: string, params = []) {
await ensureAuthenticated();
const rpcResponse = await apiCall('/rpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${currentToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method,
params
})
});
if (rpcResponse.error) throw new Error(`RPC ${method} failed: ${JSON.stringify(rpcResponse.error)}`);
return rpcResponse.result;
}
export async function main() {
const token = await authenticate();
console.log('Authorization header:', `Bearer ${token}`);
await fetchUserProfile();
const chainId = await callRpc('eth_chainId');
console.log('Authenticated RPC chain ID:', Number(chainId));
}
main().catch(console.error);Using Prividium™ SDK
The following Node.js example uses the /prividium/siwe entry point to authenticate, inspect the issued token, call the
profile API, and make an authenticated RPC request.
import { createPrividiumClient } from 'prividium';
import { createPrividiumSiweChain, type PrividiumSiweConfig } from 'prividium/siwe';
import { defineChain, type Hex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const PRIVIDIUM_API_BASE_URL = process.env.PRIVIDIUM_API_BASE_URL ?? 'http://localhost:8000';
const DOMAIN = process.env.DOMAIN ?? 'localhost:3000';
const CHAIN_ID = Number(process.env.CHAIN_ID ?? '6565');
export async function main() {
if (!PRIVATE_KEY) throw new Error('MISSING PRIVATE KEY');
const account = privateKeyToAccount(PRIVATE_KEY as Hex) as unknown as PrividiumSiweConfig['account'];
const chain = defineChain({
id: CHAIN_ID,
name: 'Prividium Local',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: [] } }
}) as unknown as PrividiumSiweConfig['chain'];
const prividium = createPrividiumSiweChain({
account,
chain,
prividiumApiBaseUrl: PRIVIDIUM_API_BASE_URL,
domain: DOMAIN,
onReauthenticate: () => {
console.info('Prividium session refreshed');
}
});
const client = createPrividiumClient({
chain: prividium.chain,
transport: prividium.transport,
account: prividium.address
});
const session = await prividium.authorize();
console.log('Token expires at:', session.expiresAt.toISOString());
const user = await prividium.fetchUser();
console.log('Authenticated user:', user.displayName ?? user.id);
const headers = prividium.getAuthHeaders();
if (!headers) {
throw new Error('Expected auth headers after authorize()');
}
console.log('Authorization header:', headers.Authorization);
const chainId = await client.getChainId();
console.log('Authenticated RPC chain ID:', chainId);
return { prividium, client };
}
main().catch(console.error);Security Considerations
- Nonce expiration: SIWE messages expire after a configured time (default: 5 minutes)
- Single use: Each SIWE nonce can only be used once
- Rate limiting: Maximum 10 challenge requests per 5 minutes per address