Skip to content

Commit 4c149d1

Browse files
authored
Sip Hooks forward headers (#16)
* handle query params as well as sip hooks * fixing checks * adding extra events
1 parent 4adaf4f commit 4c149d1

27 files changed

+808
-77
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ See existing implementations for patterns:
226226
### WebSocket
227227
- `/ws` - Real-time voice processing
228228
- Receives: Config, audio data, speak commands, **clear command**, send_message, sip_transfer
229-
- Sends: Ready, STT results, TTS audio, messages, participant events
229+
- Sends: Ready, STT results, TTS audio, messages, participant_connected, participant_disconnected, track_subscribed, tts_playback_complete, vad_event, error, sip_transfer_error
230230
- **Clear command**: Immediately stops TTS and clears audio buffers (fire-and-forget, respects `allow_interruption` setting)
231231

232232
### Webhooks

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ This mode is useful for:
8282

8383
## Authentication (Optional)
8484

85-
Sayna supports customer-based authentication that delegates token validation to an external authentication service. When enabled, protected API endpoints require a valid bearer token.
85+
Sayna supports customer-based authentication that delegates token validation to an external authentication service. When enabled, protected API endpoints require a valid token provided either as `Authorization: Bearer <token>` or as `?api_key=<token>`.
8686

8787
### Enabling Authentication
8888

@@ -106,10 +106,16 @@ openssl rsa -in auth_private_key.pem -pubout -out auth_public_key.pem
106106
### Making Authenticated Requests
107107

108108
```bash
109+
# Authorization header
109110
curl -X POST http://localhost:3001/speak \
110111
-H "Authorization: Bearer your-token-here" \
111112
-H "Content-Type: application/json" \
112113
-d '{"text": "Hello world"}'
114+
115+
# Query parameter alternative
116+
curl -X POST "http://localhost:3001/speak?api_key=your-token-here" \
117+
-H "Content-Type: application/json" \
118+
-d '{"text": "Hello world"}'
113119
```
114120

115121
For complete authentication setup and architecture details, see [docs/authentication.md](docs/authentication.md).

docs/authentication.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Sayna supports two authentication methods for protecting API endpoints:
1010

1111
Both methods can be configured independently, and you can choose the approach that best fits your deployment requirements.
1212

13+
Token transport methods for protected HTTP endpoints:
14+
- `Authorization: Bearer <token>`
15+
- `?api_key=<token>` query parameter
16+
17+
If both are present, the `Authorization` header is used first.
18+
1319
## Quick Start
1420

1521
### API Secret (Simplest)
@@ -23,6 +29,9 @@ AUTH_API_SECRETS_JSON='[{"id":"default","secret":"sk_test_default_123"},{"id":"p
2329

2430
# 3. Use
2531
curl -H "Authorization: Bearer sk_test_default_123" http://localhost:3001/speak
32+
33+
# 3b. Or use query parameter
34+
curl "http://localhost:3001/speak?api_key=sk_test_default_123"
2635
```
2736

2837
Legacy single-secret support is still available via `AUTH_API_SECRET` with optional `AUTH_API_SECRET_ID`.
@@ -118,7 +127,7 @@ Delegated validation with external auth service for advanced use cases.
118127

119128
1. **Authentication Middleware** (`src/middleware/auth.rs`)
120129
- Intercepts HTTP requests to protected endpoints
121-
- Extracts and validates Authorization header format
130+
- Extracts token from `Authorization: Bearer ...` or `?api_key=...`
122131
- **API Secret Mode**: Direct token comparison with configured secret list; matched id stored in `AuthContext`
123132
- **JWT Mode**: Buffers request body/headers and calls AuthClient
124133
- Priority: API secret checked first if configured
@@ -413,7 +422,7 @@ The following API endpoints require authentication when `AUTH_REQUIRED=true`:
413422

414423
### Making Authenticated Requests
415424

416-
The client authentication flow is identical for both methods - just send a bearer token in the Authorization header.
425+
The client authentication flow is identical for both methods. You can pass the token in the Authorization header or as an `api_key` query parameter.
417426

418427
#### With API Secret Authentication
419428

@@ -427,6 +436,9 @@ curl -X POST http://localhost:3001/speak \
427436
# List available voices
428437
curl -X GET http://localhost:3001/voices \
429438
-H "Authorization: Bearer sk_test_default_123"
439+
440+
# Same request via query parameter
441+
curl -X GET "http://localhost:3001/voices?api_key=sk_test_default_123"
430442
```
431443

432444
The matched secret id is attached to `AuthContext` and logged as `api_secret_id` for auditing.
@@ -445,6 +457,11 @@ curl -X POST http://localhost:3001/speak \
445457
-H "Authorization: Bearer user-jwt-token-xyz789" \
446458
-H "Content-Type: application/json" \
447459
-d '{"text": "Different user"}'
460+
461+
# Or via query parameter
462+
curl -X POST "http://localhost:3001/speak?api_key=user-jwt-token-xyz789" \
463+
-H "Content-Type: application/json" \
464+
-d '{"text": "Different user"}'
448465
```
449466

450467
#### Without Authentication (will fail if auth is enabled)
@@ -453,7 +470,7 @@ curl -X POST http://localhost:3001/speak \
453470
curl -X POST http://localhost:3001/speak \
454471
-H "Content-Type: application/json" \
455472
-d '{"text": "Hello world"}'
456-
# Returns: 401 Unauthorized - Missing Authorization header
473+
# Returns: 401 Unauthorized - Missing authentication token
457474
```
458475

459476
## Error Responses
@@ -471,8 +488,8 @@ Authentication errors return JSON responses with the following structure:
471488

472489
| Error Code | HTTP Status | Description |
473490
|------------|-------------|-------------|
474-
| `missing_auth_header` | 401 Unauthorized | Authorization header is missing from the request |
475-
| `invalid_auth_header` | 401 Unauthorized | Authorization header format is invalid (not "Bearer {token}") |
491+
| `missing_auth_header` | 401 Unauthorized | No token provided in either Authorization header or `api_key` query parameter |
492+
| `invalid_auth_header` | 401 Unauthorized | Authorization header format is invalid and no valid `api_key` query parameter was provided |
476493
| `unauthorized` | 401 Unauthorized | Token validation failed (auth service returned 401) |
477494
| `auth_service_error` | 401 or 502 | Auth service returned an error (see below) |
478495
| `auth_service_unavailable` | 503 Service Unavailable | Auth service is unreachable or timed out |
@@ -493,14 +510,14 @@ Sayna maps auth service responses to HTTP status codes as follows:
493510

494511
### Example Error Responses
495512

496-
**Missing Authorization Header:**
513+
**Missing Token (header/query):**
497514
```json
498515
HTTP/1.1 401 Unauthorized
499516
Content-Type: application/json
500517

501518
{
502519
"error": "missing_auth_header",
503-
"message": "Missing Authorization header"
520+
"message": "Missing authentication token (Authorization header or api_key query parameter)"
504521
}
505522
```
506523

@@ -750,7 +767,7 @@ The `iat` claim is a standard JWT field that indicates when the JWT was created.
750767

751768
| Error | Cause | Solution |
752769
|-------|-------|----------|
753-
| 401 Unauthorized | Missing or invalid token | Include valid `Authorization: Bearer {token}` header |
770+
| 401 Unauthorized | Missing or invalid token | Include a valid `Authorization: Bearer {token}` header or `?api_key={token}` query parameter |
754771
| 401 "Invalid API secret" | Token doesn't match any configured API secret | Check token matches one of the configured secrets (case-sensitive) |
755772
| 500 "Auth required but no method configured" | AUTH_REQUIRED=true but no auth method set | Set either AUTH_API_SECRETS_JSON (or legacy AUTH_API_SECRET) or (AUTH_SERVICE_URL + AUTH_SIGNING_KEY_PATH) |
756773
| 503 Service Unavailable | Auth service unreachable (JWT mode) | Verify auth service is running and reachable |

docs/openapi.yaml

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ paths:
9898
schema:
9999
$ref: '#/components/schemas/RemoveParticipantErrorResponse'
100100
security:
101-
- bearer_auth: []
101+
- auth: []
102102
/livekit/participant/mute:
103103
post:
104104
tags:
@@ -157,7 +157,7 @@ paths:
157157
schema:
158158
$ref: '#/components/schemas/RemoveParticipantErrorResponse'
159159
security:
160-
- bearer_auth: []
160+
- auth: []
161161
/livekit/rooms:
162162
get:
163163
tags:
@@ -191,7 +191,7 @@ paths:
191191
'500':
192192
description: LiveKit service not configured or failed to list rooms
193193
security:
194-
- bearer_auth: []
194+
- auth: []
195195
/livekit/rooms/{room_name}:
196196
get:
197197
tags:
@@ -236,7 +236,7 @@ paths:
236236
'500':
237237
description: LiveKit service not configured or failed to get room details
238238
security:
239-
- bearer_auth: []
239+
- auth: []
240240
/livekit/token:
241241
post:
242242
tags:
@@ -282,7 +282,7 @@ paths:
282282
'500':
283283
description: LiveKit service not configured, room creation failed, or token generation failed
284284
security:
285-
- bearer_auth: []
285+
- auth: []
286286
/recording/{stream_id}:
287287
get:
288288
tags:
@@ -320,7 +320,7 @@ paths:
320320
'503':
321321
description: Recording storage not configured or unavailable
322322
security:
323-
- bearer_auth: []
323+
- auth: []
324324
/sip/call:
325325
post:
326326
tags:
@@ -360,7 +360,7 @@ paths:
360360
schema:
361361
$ref: '#/components/schemas/SIPCallErrorResponse'
362362
security:
363-
- bearer_auth: []
363+
- auth: []
364364
/sip/hooks:
365365
get:
366366
tags:
@@ -387,6 +387,8 @@ paths:
387387
application/json:
388388
schema:
389389
$ref: '#/components/schemas/SipHooksErrorResponse'
390+
security:
391+
- auth: []
390392
post:
391393
tags:
392394
- sip
@@ -439,6 +441,8 @@ paths:
439441
application/json:
440442
schema:
441443
$ref: '#/components/schemas/SipHooksErrorResponse'
444+
security:
445+
- auth: []
442446
delete:
443447
tags:
444448
- sip
@@ -490,6 +494,8 @@ paths:
490494
application/json:
491495
schema:
492496
$ref: '#/components/schemas/SipHooksErrorResponse'
497+
security:
498+
- auth: []
493499
/sip/transfer:
494500
post:
495501
tags:
@@ -552,7 +558,7 @@ paths:
552558
schema:
553559
$ref: '#/components/schemas/SIPTransferErrorResponse'
554560
security:
555-
- bearer_auth: []
561+
- auth: []
556562
/speak:
557563
post:
558564
tags:
@@ -586,7 +592,7 @@ paths:
586592
'500':
587593
description: TTS synthesis failed
588594
security:
589-
- bearer_auth: []
595+
- auth: []
590596
/voices:
591597
get:
592598
tags:
@@ -609,7 +615,7 @@ paths:
609615
'500':
610616
description: Internal server error
611617
security:
612-
- bearer_auth: []
618+
- auth: []
613619
components:
614620
schemas:
615621
DeleteSipHooksRequest:
@@ -2007,11 +2013,11 @@ components:
20072013
description: URL to sample audio
20082014
example: https://example.com/sample.mp3
20092015
securitySchemes:
2010-
bearer_auth:
2016+
auth:
20112017
type: http
20122018
scheme: bearer
2013-
bearerFormat: JWT
2014-
description: JWT token obtained from the authentication service. Required when AUTH_REQUIRED is enabled.
2019+
bearerFormat: Token
2020+
description: 'Authentication token for protected endpoints. Can be provided as `Authorization: Bearer <token>` or `?api_key=<token>`. Required when AUTH_REQUIRED is enabled.'
20152021
tags:
20162022
- name: health
20172023
description: Health check endpoints

0 commit comments

Comments
 (0)