# Wallboard API Authentication

OAuth 2.0 Authorization Code Flow.

## Quick Start

1. Obtain access token via OAuth 2.0 flow
2. Include in requests: `Authorization: Bearer <access_token>`

## Endpoints

| Endpoint | URL |
|----------|-----|
| Authorization | `{server}/oauth/authorize` |
| Token | `{server}/oauth/token` |
| Auth server metadata | `{server}/.well-known/oauth-authorization-server` |
| Protected resource metadata | `{server}/.well-known/oauth-protected-resource`, `{server}/.well-known/oauth-protected-resource/mcp` |
| Dynamic client registration | `{server}/oauth/register` |

### Root-Level Aliases

For compatibility with OAuth clients that require root-level paths (e.g., Claude.ai MCP connector):

| Alias | Equivalent To |
|-------|---------------|
| `{server}/authorize` | `{server}/oauth/authorize` |
| `{server}/token` | `{server}/oauth/token` |

Both paths work identically.

## Scope

Use `FULL_ACCESS` for general API access.

---

## MCP OAuth Discovery

MCP resource: `{server}/mcp`.

OAuth-capable MCP clients should use Wallboard discovery metadata instead of hard-coding authentication behavior. Wallboard exposes authorization-server metadata, protected-resource metadata for MCP, and Dynamic Client Registration where enabled.

### Client Registration And Metadata

Supported client paths:
- Stored OAuth client created in Wallboard UI
- Dynamic Client Registration public PKCE client
- HTTPS Client ID Metadata Document (CIMD) when enabled by server policy

CIMD metadata must identify the client and redirect URIs from a public HTTPS document. If CIMD is unavailable, use DCR or a stored OAuth client.

Private-key client authentication is supported where enabled; use standard OAuth metadata to determine accepted methods.

### OAuth Resource

The OAuth `resource` parameter can bind an authorization to the Wallboard MCP resource. When supplied during both authorize and token exchange, values must match; omitting it leaves the authorization unbound to that resource.

### Redirects And MCP Writes

Loopback redirect URIs are supported for local OAuth clients when the registered localhost/loopback callback matches the requested callback except for the runtime port.

MCP `api_write` request bodies are JSON objects; batch writes still use `requests[]`.

---

## Creating OAuth Clients

Create OAuth clients in Wallboard UI:

| Location | Path |
|----------|------|
| System-wide | System Settings > OAuth Clients |
| Customer-specific | Customer Settings > Integrations > OAuth Clients |

**Settings:**
- `redirect_uri`: Allowed callback URLs (comma-separated)
- `Public`: Enable for PKCE-only clients (no secret)
- Grant types: authorization_code, refresh_token

---

## Public Client (PKCE)

For CLI tools or browser apps where client_secret cannot be kept secure.

### PKCE Setup

**Code Verifier**: Random 32 bytes, base64url encoded

**Code Challenge**: SHA-256 hash of verifier, base64url encoded
```
code_challenge = base64url(sha256(code_verifier))
```

### Authorization Request

```
GET {server}/oauth/authorize
  ?client_id={client_id}
  &response_type=code
  &redirect_uri={redirect_uri}
  &code_challenge={code_challenge}
  &scope=FULL_ACCESS
  &state={state}
```

`state`: Random string for CSRF protection (min 3 chars)

### Token Exchange

```
POST {server}/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code={authorization_code}
&redirect_uri={redirect_uri}
&code_verifier={code_verifier}
&client_id={client_id}
```

---

## Confidential Client (with Secret)

For server-side applications.

### Authorization Request

```
GET {server}/oauth/authorize
  ?client_id={client_id}
  &response_type=code
  &redirect_uri={redirect_uri}
  &scope=FULL_ACCESS
  &state={state}
```

Set `kmsi` on the **token request**, not the authorization request. The current default is `true` (30-day refresh token); `false` requests a one-hour refresh token. It applies to both code exchange and refresh.

### Token Exchange

```
POST {server}/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code={authorization_code}
&redirect_uri={redirect_uri}
&client_id={client_id}
&client_secret={client_secret}
```

---

## Token Refresh

```
POST {server}/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token={refresh_token}
&client_id={client_id}
```

For confidential clients, also include `client_secret` or use the client authentication method advertised by discovery.

Each successful refresh consumes the old refresh token and returns a replacement. Save the new `refresh_token` before the next refresh. Serialize refresh attempts for a session: reuse of an already consumed token returns HTTP 400 with `error="invalid_grant"`. Expired tokens also require a new authorization flow.

`kmsi` is evaluated on every token request; explicitly send `kmsi=false` on every refresh when a one-hour lifetime is required.

---

## Password Grant (Service Accounts)

For machine-to-machine integrations using a user explicitly enabled as a Service Account.

**Requirements:**
1. User must have "Service Account" enabled in Wallboard UI (User Settings)
2. Use default client credentials:

| Property | Value |
|----------|-------|
| client_id | `default-client` |
| client_secret | `76211db5d8ea` |
| Basic Auth | `Basic ZGVmYXVsdC1jbGllbnQ6NzYyMTFkYjVkOGVh` |

This built-in client supports password and refresh grants. The service account's own credentials are still required.

```
POST {server}/oauth/token
Authorization: Basic ZGVmYXVsdC1jbGllbnQ6NzYyMTFkYjVkOGVh
Content-Type: application/x-www-form-urlencoded

grant_type=password
&username={email}
&password={password}
```

Add `totp={code}` if 2FA enabled.

---

## Token Response

```json
{
  "access_token": "...",
  "refresh_token": "...",
  "token_type": "bearer",
  "expires_in": 1800,
  "refresh_total_validity_seconds": 2592000,
  "customerId": 123,
  "readOnly": false
}
```

## Token Lifetimes

| Token | Omitted `kmsi` / `kmsi=true` | `kmsi=false` |
|-------|---------|----------------|
| Access Token | 30 min | 30 min |
| Refresh Token | 30 days | 1 hour |

## Token Types

- **access_token**: OAuth2 access token (use this)
- **refresh_token**: Long-lived token for obtaining new access tokens

## Using the Token

```
GET /api/v2/device
Authorization: Bearer {access_token}
```

## Error Responses

| Status | Meaning | Action |
|--------|---------|--------|
| 400, `invalid_grant` | Refresh token expired, invalid, mismatched, or already consumed | Re-authorize; never reuse a consumed token |
| 400, `invalid_request` | Invalid token request | Correct the request |
| 401 | API access token expired/invalid | Refresh or re-authenticate |
| 403 | Insufficient permissions | Check user role |

Token-specific errors use `{"error":"invalid_grant","error_description":"..."}`. Other API failures use the standard Wallboard error envelope.

## Additional Security

- **TOTP**: Time-based One-Time Password for 2FA
- **SSO**: Google, Microsoft (system-wide), SAML (customer-level)
