Authentication & Roles

Overview

Private Channels includes an optional Auth Service that gates gateway access with JWT authentication and role-based access control (RBAC). When authentication is disabled, the gateway accepts all connections. When enabled, clients must present a valid JWT in every request.

This page covers both audiences:

  • Developers - registration, login, wallet verification, and making authenticated requests
  • Operators - enabling the auth service, configuring JWT_SECRET, and provisioning the operator role

Enabling Auth

Authentication is enabled by setting JWT_SECRET (non-empty) on both the gateway and the Auth Service. The Auth Service also requires AUTH_DATABASE_URL.

When JWT_SECRET is not set, the gateway operates in open mode; no token is required.

Docker Compose: The auth service is a Docker Compose profile and is not started by default. To include it, pass --profile auth to your docker compose command, including --env-file .env so secrets like JWT_SECRET and POSTGRES_PASSWORD actually resolve (Compose disables its automatic .env auto-load once any --env-file flag is passed):

docker compose -f docker-compose.devnet.yml --env-file versions.env --env-file .env.devnet --env-file .env --profile auth up -d

Auth Service API

All endpoints are under /auth. The auth service listens on AUTH_PORT (default 8903).

POST /auth/register

Create a new account. All users are registered with the user role.

{ "username": "alice", "password": "hunter2" }
  • Username: 5-32 characters, alphanumeric plus _ and -
  • Password: 6-128 characters
  • Returns the created user; password is never returned

POST /auth/login

Authenticate and receive a signed JWT valid for 24 hours.

{ "username": "alice", "password": "hunter2" }

Returns { "token": "<jwt>" }. Both wrong username and wrong password return 401 to prevent username enumeration.

POST /auth/challenge-wallet

Request a signing challenge to prove ownership of a Solana wallet. Requires a valid JWT.

Returns a message, nonce, and expiry. The challenge expires in 10 minutes.

{
"message": "PrivateChannel wallet verification\nuser: <uuid>\nnonce: <uuid>\nexpires: <unix>",
"nonce": "<uuid>",
"expires_at": "<iso8601>"
}

POST /auth/verify-wallet

Submit the signed challenge to register a wallet as verified. Requires a valid JWT.

{
"pubkey": "<base58 pubkey>",
"nonce": "<uuid from challenge>",
"signature": "<base58 Ed25519 signature>"
}

The service reconstructs the challenge message, verifies the Ed25519 signature, and stores the wallet. Each nonce can only be consumed once; replays are rejected.

Returns { "pubkey": "<base58>", "created_at": "<iso8601>" }.

GET /auth/wallets

List all verified wallets for the authenticated user. Requires a valid JWT.

DELETE /auth/wallets/{pubkey}

Remove a verified wallet from the authenticated user's account. Requires a valid JWT.

GET /health

Liveness check. Returns 200 ok. No authentication required.

JWT Structure

Tokens use the HS256 algorithm and expire 24 hours after issue.

ClaimValue
subUser UUID
role"user" or "operator"
iss"private-channel-auth"
aud"private-channel-gateway"
expUnix timestamp (24h from issue)

iss and aud are present in the JWT payload but are validated by the gateway's JWT configuration, not deserialized into the application claims struct. Application-layer code has access to sub, role, and exp only.

Pass the token in the Authorization header:

Authorization: Bearer <JWT_TOKEN>

Roles

user

Default role on registration.

  • Access is gated to the user's own verified wallets
  • Blocked from: getBlock, getTransaction, simulateTransaction
  • Can: call Deposit on the Escrow Program, initiate withdrawals via WithdrawFunds

operator

Elevated role. Must be provisioned directly; there is no self-service path to escalate from user to operator.

Grant the role, either via the Admin CLI (private-channel-auth-admin):

private-channel-auth-admin set-role --username alice --role operator

or with direct SQL:

This is a privileged database operation. Restrict access to the Auth Service database accordingly and audit any role changes.

UPDATE private_channel_auth.users SET role = 'operator' WHERE username = 'alice';

Register a wallet without the self-verify flow (Admin CLI, private-channel-auth-admin):

private-channel-auth-admin attach-wallet --username alice --pubkey <base58-pubkey>

This inserts a verified wallet directly into the verified_wallets table, bypassing the challenge/verify flow. Enforces a unique constraint on (user_id, pubkey). It does not grant the operator role by itself; use set-role or the SQL update above for that. This command is for attaching a wallet to an account (for example, a service account) without requiring the interactive challenge/verify flow.

Capabilities:

  • Bypasses all wallet ownership checks
  • Full access to all gateway RPC methods, including getBlock, getTransaction, simulateTransaction
  • Required for: ReleaseFunds, ResetSmtRoot

Full Authentication Flow

Making Authenticated Requests

const response = await fetch("http://localhost:8899/", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${jwtToken}`
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getBalance",
params: [walletAddress]
})
});
const data = await response.json();

Gateway Endpoints

These endpoints do not require authentication:

EndpointMethodDescriptionSuccessFailure
/healthGETLiveness check200 {"status":"ok"}-
/readyGETDeep readiness; probes write + read nodes200 {"status":"ready"}503 {"status":"degraded"}

For the JWT_SECRET and gateway environment variable reference, see the Configuration reference.

RPC Method Access Matrix

The following methods are recognized by the gateway. When JWT_SECRET is set, access depends on JWT role:

MethodRouteNo JWTuseroperator
sendTransactionWrite node
getLatestBlockhashRead node
getSlotRead node
getRecentBlockhashRead node
getSignatureStatusesRead node
getTransactionCountRead node
getFirstAvailableBlockRead node
getBlocksRead node
getEpochInfoRead node
getEpochScheduleRead node
getRecentPerformanceSamplesRead node
getBlockTimeRead node
getVoteAccountsRead node
getSupplyRead node
getSlotLeadersRead node
isBlockhashValidRead node
getAccountInfoRead node401ownership-gated¹
getTokenAccountBalanceRead node401ownership-gated¹
getSignaturesForAddressRead node401ownership-gated¹
getBlockRead node401403
getTransactionRead node401403
simulateTransactionRead node401403

¹ Ownership-gated: for an SPL Token account (owner field is TokenkegQ... or TokenzQ..., data of at least 165 bytes), the gateway checks that the owner or delegate field matches one of the authenticated user's verified wallets. For any other account type (a System Program wallet, or an unknown PDA), it instead checks whether the queried pubkey itself is one of the user's verified wallets, since such accounts have no owner/delegate field to inspect. Either check failing returns 403.

Is this page helpful?

© 2026 Solana Foundation. Alle Rechte vorbehalten.