Skip to content

Commit 7552f67

Browse files
committed
fix: use CTI middleware for authentication
Main reasons for this change: - the FreePBX REST api can have restricted access by source IP - the same CTI API can be used both by Matrix2Acrobits and ctiapp-proxyauth
1 parent 5719ec5 commit 7552f67

File tree

17 files changed

+1222
-688
lines changed

17 files changed

+1222
-688
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ The proxy is configured via environment variables. Minimal required env:
1818
- `PROXY_PORT` (optional): port to listen on (default: `8080`)
1919
- `AS_USER_ID` (optional): the user ID of the Application Service bot (default: `@_acrobits_proxy:matrix.example`)
2020
- `PROXY_URL` (optional): public-facing URL of this proxy (e.g. `https://matrix.example.com`), if not specified, use the value of `MATRIX_HOMESERVER_URL`
21-
- `EXT_AUTH_URL` (optional): external HTTP endpoint used to validate extension+password for push token reports (eg: `https://voice.nethserver.org/freepbx/rest/testextauth`)
22-
- `EXT_AUTH_TIMEOUT_S` (optional): timeout in seconds for calls to `EXT_AUTH_URL` (default: `5`)
21+
- `EXT_AUTH_URL` (optional): base URL of the external authentication service (eg: `https://voice.nethserver.org/`). The proxy uses the CTI middleware to authentication, and automatically appends `/api/login` and `/api/chat?users=1` to this URL.
22+
- `EXT_AUTH_TIMEOUT_S` (optional): timeout in seconds for calls to `EXT_AUTH_URL` (default: `5`)
2323
- `LOGLEVEL` (optional): logging verbosity level - `DEBUG`, `INFO`, `WARNING`, `CRITICAL` (default: `INFO`)
2424
- `PUSH_TOKEN_DB_PATH` (optional): path to a database file for storing push tokens
2525
- `CACHE_TTL_SECONDS` (optional): time-to-live for in-memory cache entries (default: `3600` seconds)

docs/AUTHENTICATION.md

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,36 @@
22

33
All client API endpoints (`/api/client/fetch_messages`, `/api/client/send_message`, `/api/client/push_token_report`) require authentication via an external authentication service.
44

5-
The external authentication service is implemented inside NethVoice FreePBX container [REST APIs](https://github.com/nethesis/ns8-nethvoice/tree/main/freepbx/var/www/html/freepbx/rest).
5+
The external authentication service is provided by NethCTI Middleware, which manages user credentials, chat capabilities, and Matrix homeserver configuration.
66

7-
### External Auth Flow
7+
### External Auth Flow (2-Step Process)
88

9-
When a client sends a request, the proxy:
10-
1. Extracts the `username` (extension) and `password` from the request.
11-
2. Calls `EXT_AUTH_URL` with a POST request containing JSON: `{"extension":"<username>","secret":"<password>"}`.
12-
3. On successful auth (200), parses the response for `main_extension`, `sub_extensions`, and `user_name`, which are converted into a mapping and saved.
13-
4. On failure (401 or other error), returns an authentication error and does NOT save the push token or create a mapping.
14-
5. Auth responses are cached in-memory for `CACHE_TTL_SECONDS` seconds to reduce external service load.
9+
When a client sends a request with `username` and `password`, the proxy performs a 2-step authentication:
10+
11+
**Step 1: Login (POST to `/api/login`)**
12+
1. Extract the username part from the `username` field (e.g., if `username` is `[email protected]`, extract `user`)
13+
2. POST to `{EXT_AUTH_URL}/api/login` with JSON payload: `{"username":"<user>","password":"<password>"}`
14+
3. On successful auth (200), parse the response to get the JWT `token` field
15+
4. Decode the JWT to extract claims (without signature verification)
16+
17+
**Step 2: Verify Chat Capability & Fetch Configuration (GET from `/api/chat?users=1`)**
18+
5. Verify that the JWT contains the `nethvoice_cti.chat` claim set to `true`
19+
6. If the claim is missing or false, authentication fails
20+
7. GET from `{EXT_AUTH_URL}/api/chat?users=1` with Bearer token in Authorization header
21+
8. Parse the response to extract:
22+
- Matrix homeserver configuration (`matrix.base_url`, `matrix.acrobits_url`)
23+
- User mappings from the `users` array (`user_name`, `main_extension`, `sub_extensions`)
24+
9. Convert user data into `MappingRequest` objects and cache them
25+
26+
**Error Handling**
27+
- On any failure (login error, missing claim, invalid JWT, chat endpoint error), returns authentication error
28+
- Does NOT save the push token or create a mapping on failure
29+
- Successful authentications are cached in-memory for `CACHE_TTL_SECONDS` seconds to reduce external service load
1530

1631
If any request is missing a `password`, it fails with authentication error.
1732

1833
### Environment variables related to auth
1934

20-
- `EXT_AUTH_URL`: external HTTP endpoint used to validate extension+password for push token reports (eg: `https://voice.nethserver.org/freepbx/rest/testextauth`)
35+
- `EXT_AUTH_URL`: Base URL of the external authentication service (eg: `https://cti.nethserver.org/`). The proxy automatically appends `/api/login` and `/api/chat?users=1` to this URL.
2136
- `EXT_AUTH_TIMEOUT_S`: timeout in seconds for calls to `EXT_AUTH_URL` (default: `5`)
2237
- `CACHE_TTL_SECONDS`: cache TTL for external auth responses (default: `3600` seconds)

docs/openapi.yaml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ paths:
1313
operationId: fetchMessages
1414
description: |
1515
Checks for new incoming messages by performing a Matrix /sync on behalf of the user.
16-
Authentication is handled by the Application Service backend; the `password` field is ignored.
16+
Authentication is performed against an external authentication service (2-step flow):
17+
1. POST /api/login with username and password to get JWT token
18+
2. GET /api/chat?users=1 with Bearer token to fetch user mappings and verify chat capability
1719
requestBody:
1820
required: true
1921
content:
@@ -26,10 +28,10 @@ paths:
2628
properties:
2729
username:
2830
type: string
29-
description: The full Matrix ID (e.g., @user:server.com) of the user to impersonate.
31+
description: The extension/username (format can be user@domain), which is sent to the external auth service.
3032
password:
3133
type: string
32-
description: Password used to authenticate the extension/user via the external auth service.
34+
description: Password used to authenticate via the external auth service.
3335
last_id:
3436
type: string
3537
description: The 'since' token from the last sync.
@@ -58,12 +60,14 @@ paths:
5860
5961
The `from` field can be:
6062
- A full Matrix ID (e.g., @user:server.com) - used directly
61-
- A phone number (e.g., +1234567890) - resolved to Matrix ID via -to-Matrix mapping if available
63+
- A phone number (e.g., +1234567890) - resolved to Matrix ID via phone-to-Matrix mapping if available
6264
6365
If a phone number is provided and a mapping exists, the message is sent on behalf of the mapped Matrix user.
6466
If a phone number is provided but no mapping exists, the phone number is used as the sender ID.
6567
66-
Authentication is handled by the Application Service backend; the `password` field is ignored.
68+
Authentication is performed against an external authentication service (2-step flow):
69+
1. POST /api/login with username and password to get JWT token
70+
2. GET /api/chat?users=1 with Bearer token to fetch user mappings and verify chat capability
6771
requestBody:
6872
required: true
6973
content:
@@ -118,6 +122,10 @@ paths:
118122
Receives and stores device push tokens from Acrobits clients.
119123
The tokens are persisted in a SQLite database for later use in push notification delivery.
120124
This endpoint follows the Acrobits Push Token Reporter API specification for POST JSON requests.
125+
126+
Authentication is performed against an external authentication service (2-step flow):
127+
1. POST /api/login with username and password to get JWT token
128+
2. GET /api/chat?users=1 with Bearer token to fetch user mappings and verify chat capability
121129
requestBody:
122130
required: true
123131
content:

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
filippo.io/edwards25519 v1.1.0 // indirect
1515
github.com/davecgh/go-spew v1.1.1 // indirect
1616
github.com/dustin/go-humanize v1.0.1 // indirect
17+
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
1718
github.com/google/uuid v1.6.0 // indirect
1819
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
1920
github.com/labstack/gommon v0.4.2 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
66
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
77
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
88
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
9+
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
10+
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
911
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
1012
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
1113
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=

main.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,6 @@ func main() {
6060
pushSvc := service.NewPushService(pushTokenDB)
6161
api.RegisterRoutes(e, svc, pushSvc, cfg.MatrixAsToken, pushTokenDB)
6262

63-
// Load mappings from file if MAPPING_FILE env var is set
64-
if cfg.MappingFile != "" {
65-
if err := svc.LoadMappingsFromFile(cfg.MappingFile); err != nil {
66-
logger.Error().Err(err).Str("file", cfg.MappingFile).Msg("failed to load mappings from file")
67-
}
68-
}
69-
7063
logger.Info().Str("port", cfg.ProxyPort).Msg("starting server")
7164
if err := e.Start(":" + cfg.ProxyPort); err != nil {
7265
logger.Fatal().Err(err).Msg("server stopped")

0 commit comments

Comments
 (0)