Skip to main content

Authentication Model

Two Types of Auth

Relay has two separate authentication systems:

  1. Dashboard Auth — for humans logging into the web dashboard
  2. WebSocket Auth — for apps and agents connecting to Relay's API

They use different methods because they have different needs.

Dashboard Authentication

Email + OTP (No Passwords)

The Relay dashboard uses email + one-time password (OTP) for all logins.

1. User enters email

2. Relay sends 6-digit OTP via AWS SES

3. User receives email and enters OTP

4. Relay verifies OTP

5. User is logged in, Relay issues JWT

Why No Passwords?

Passwords are:

  • Hard to remember
  • Easy to crack if weak
  • Prone to reuse across sites
  • Add complexity to account recovery

OTP is:

  • Simple and secure
  • Sent via email (which the user controls)
  • Time-limited (10 minutes)
  • Rate-limited (5 attempts max before lockout)

OTP Details

PropertyValue
Length6 digits
Expiry10 minutes
Max attempts5 incorrect entries
Lockout duration15 minutes (after 5 failed attempts)

After 5 incorrect OTP entries, you're locked out for 15 minutes. Request a new OTP to reset the counter.

JWT (JSON Web Token)

After entering a valid OTP, Relay issues a JWT token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoiam...

This JWT:

  • Is stored in your browser's session storage or cookies
  • Authenticates all dashboard API calls
  • Expires after a period of inactivity (typically 7 days)
  • Cannot be used on WebSocket connections (different auth)

WebSocket Authentication

Bearer Token on Handshake

Apps and agents authenticate to Relay's WebSocket using Bearer tokens on handshake.

WebSocket Connection Request:
URL: wss://api.relay.ckgworks.com/v1/ws/app
Headers:
Authorization: Bearer rlk_portal_x8k2m9p...

When the connection is established:

  1. Relay extracts the token from the Authorization header
  2. Relay verifies the token (checks bcrypt hash against DB)
  3. If valid → connection is accepted
  4. If invalid → connection is rejected immediately

Token Types

TypePrefixUsed ByExample
App tokenrlk_Applications sending eventsrlk_portal_x8k2m...
Agent tokenrla_Agents/plugins receiving eventsrla_athena_k9x2p...

One-Time Auth

Auth happens once per connection, on handshake. After that:

  • ✅ All messages on the connection are trusted
  • ✅ No per-message signing required
  • ✅ No HMAC headers needed

This is the Telegram model — authenticate on connect, then trust the channel.

Why No HMAC?

HMAC (Hash-based Message Authentication Code) is used when:

  • You have stateless HTTP connections
  • Each request must prove authenticity independently
  • You can't trust the channel itself

Example without HMAC (bad):

POST https://relay.example.com/event
Body: { agent_id: "athena", payload: {...} }
Headers:
Authorization: Bearer rlk_portal_...
X-HMAC-SHA256: 5d41402abc4b2a76b9719d911017c592...

Every request needs the HMAC header.

But Relay uses WebSocket (stateful, persistent connection):

  • ✅ Channel is authenticated once on handshake
  • ✅ All subsequent messages on the channel are trusted
  • ✅ No need to re-prove identity for each message
  • ✅ HMAC would add complexity with no benefit

Example with Relay (good):

WebSocket Connect:
Authorization: Bearer rlk_portal_... ← Authenticate once

Send event:
{ type: "event", agent_id: "athena", payload: {...} }
← No additional auth needed, channel is trusted

Token Management

Token Storage

Relay stores tokens as bcrypt hashes, not plaintext.

When you create a token:

Plaintext token: rlk_portal_x8k2m9p2d7q4v1r9z5k3j8h2m7n4q1...
Bcrypt hash: $2b$12$R9h.cIPz0gi.URNNGI3OXeRDz8P.dwOuq3D3/wLhBkWkGJmJCxByO
Storage: Hash is stored in DB, plaintext is never saved

When authenticating:

1. Client sends token
2. Relay hashes it
3. Relay compares hash with stored hash
4. If match → authentication succeeds

This means:

  • ✅ Even if the database is compromised, tokens can't be extracted
  • ✅ Plaintext tokens are shown only at creation time
  • ✅ Lost tokens cannot be recovered from Relay

Plaintext Display (One-Time Only)

When you create an app or agent token, it's shown once:

Token created! Copy it now — you won't see it again.

rlk_portal_x8k2m9p2d7q4v1r9z5k3j8h2m7n4q1...

[Copy] [Done]

After you click away:

  • ❌ The plaintext is not displayed again
  • ❌ It cannot be retrieved from the dashboard
  • ✅ You must store it securely (.env, secrets manager, etc.)

Per-Connection vs. Shared Tokens

Apps and Agents

When you register an app or agent, you get one token per resource.

App: Portal
Token: rlk_portal_x8k2m9p...

Can have multiple instances (load-balanced)
├── API Server 1 (uses same token)
├── API Server 2 (uses same token)
└── API Server 3 (uses same token)

Each instance maintains its own WebSocket connection

Similarly for agents:

Agent: Athena
Token: rla_athena_k9x2p...

OpenClaw plugin connects with this token
└── One WebSocket connection per token

Multiple Tokens (Token Rotation)

If you rotate a token:

Old token: rlk_portal_x8k2m9p... (1-hour grace period)
New token: rlk_portal_q8l2m9r...

Old connections can use old token for 1 hour
New connections must use new token

This allows:

  • ✅ Gradual migration from old to new token
  • ✅ No immediate service disruption
  • ✅ Time to deploy the new token to all instances

Security Implications

Token Exposure

If a token is exposed:

  • In code: Someone can authenticate as that app/agent

    • They can send events (if app token) or receive events (if agent token)
    • They can potentially see other apps' payloads (agent) or impersonate the app
    • Fix: Rotate the token immediately
  • In logs: Check if the token was logged anywhere

    • Grep your application logs: grep -r "rlk_portal" logs/
    • Clear any files that contain the token
    • Consider them compromised and rotate
  • In environment: If an environment variable is exposed

    • Rotate the token
    • Review who had access to that environment
    • Update your secrets management

Best Practice: Treat Tokens Like Passwords

  • ✅ Store in environment variables (.env, secrets manager)
  • ✅ Rotate periodically (monthly or quarterly)
  • ✅ Never commit to version control
  • ✅ Never share via email or chat
  • ✅ Use .gitignore for .env files
  • ❌ Don't log tokens
  • ❌ Don't embed in code
  • ❌ Don't share with contractors without a formal agreement

Organization Isolation in Auth

Apps Can't Escape Their Organization

An app token is bound to a specific organization:

Organization A:
App: Portal (rlk_portal_orgA_...)

Organization B:
App: Portal (rlk_portal_orgB_...)

Portal's token in Organization A cannot access Organization B's resources

When authenticating:

  1. Relay hashes the token
  2. Relay looks up which organization owns this token
  3. All subsequent operations are scoped to that organization
  4. Apps can only see agents in their own organization

This is enforced at the database level — impossible to escape.

Authentication Checklist

When integrating with Relay:

  • Store your app/agent token securely (env vars, secrets manager)
  • Use HTTPS/WSS for all connections (never HTTP/WS)
  • Verify the token works (test connection from your app/agent)
  • Don't log the token
  • Rotate the token if it's exposed or periodically
  • Use the correct endpoint URL (api.relay.ckgworks.com)
  • Set the Authorization header correctly (Bearer token)

Summary

  • Dashboard: Email + OTP, no passwords
  • WebSocket: Bearer token on handshake
  • Tokens: Stored as bcrypt hashes, shown once at creation
  • No HMAC: WebSocket channel is already authenticated
  • Organization-scoped: Tokens bound to their organization
  • Rotation: New token issued, old has 1-hour grace period

Next steps: