> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.prividium.com/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

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.

:::info[IMPORTANT]
Admin accounts with passkey-based authentication enabled are not suitable for programmatic RPC access. Use an
account with
[one of the required system permissions](/developers/scripting/contract-deployment-scripting#required-system-permissions)
for automated scripts instead.
:::

## Token Lifecycle Overview

1. **Issue**: Request a SIWE message, sign it with your wallet, and exchange it for a session token
2. **Check**: Query the current session endpoint to verify token validity and expiration
3. **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:

```bash
$ cast wallet new
Successfully created new keypair.
Address:     0x9317a78Dc814297Defe678918A84084C427a1e59
Private key: 0xd9ad2e2da24635777b184f591ba8cf6aaf428879034030022f088a68f2390b05
```

The 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

```bash
curl -X POST http://localhost:8000/api/siwe-messages \
  -H "Content-Type: application/json" \
  -d '{
    "address": "0xYourWalletAddress",
    "domain": "localhost:3000"
  }'
```

### Example Response

```json
{
  "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.

:::note[NOTE]
The statement line (`Login to Prividium™ chain` above) reflects the deployment's configured brand name, so don't
match on its exact text. Sign the returned `msg` as-is.
:::

## Step 2: Sign and Exchange for Token

Sign the message with your wallet and send it to the login endpoint.

:::note[NOTE]
Non-admin automation accounts receive a token directly. Admin accounts may receive a WebAuthn MFA challenge instead of
a token and are therefore not suitable for unattended automation.
:::

### 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

```bash
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

```json
{
  "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.

1. Save expiration time locally near the token value.
2. If the token is expired or about to expire, initiate a new SIWE flow.
3. 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

```bash
curl -X GET http://localhost:8000/api/auth/current-session \
  -H "Authorization: Bearer <your-session-token>"
```

### Example Response

```json
{
  "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

**rest-authentication.ts**

```typescript
import { pathToFileURL } from 'node:url';
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));
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
    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.

**complete-authentication.ts**

```typescript
import { pathToFileURL } from 'node:url';
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 ?? '506');

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 };
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
    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
