diff --git a/code/API_definitions/click-to-dial.yaml b/code/API_definitions/click-to-dial.yaml index 44bc01a..2258d57 100644 --- a/code/API_definitions/click-to-dial.yaml +++ b/code/API_definitions/click-to-dial.yaml @@ -119,10 +119,13 @@ paths: parameters: - $ref: "../common/CAMARA_common.yaml#/components/parameters/x-correlator" security: + - {} - notificationsBearerAuth: [] description: | Event notification delivered to the sink URL when the call status changes (CloudEvents structured mode). + The sink uses HTTPS and may accept notifications without Bearer authentication when no `sinkCredential` was provided. + If `sinkCredential` was provided, the provider uses the specified authentication mechanism. requestBody: required: true content: @@ -134,7 +137,7 @@ paths: $ref: '#/components/examples/CALL_STATUS_CHANGED_EXAMPLE' responses: '2XX': - description: Event successfully received by the sink. + description: Event successfully received by the sink, including 204 No Content. headers: x-correlator: $ref: "../common/CAMARA_common.yaml#/components/headers/x-correlator" @@ -144,6 +147,10 @@ paths: $ref: '../common/CAMARA_common.yaml#/components/responses/Unauthenticated401' '403': $ref: '../common/CAMARA_common.yaml#/components/responses/PermissionDenied403' + '410': + $ref: '../common/CAMARA_event_common.yaml#/components/responses/SinkGone410' + '429': + $ref: '../common/CAMARA_common.yaml#/components/responses/TooManyRequests429' /calls/{callId}: get: tags: @@ -294,6 +301,13 @@ components: $ref: '#/components/schemas/CallStatus' createdAt: $ref: '../common/CAMARA_common.yaml#/components/schemas/DateTime' + sink: + description: | + The HTTPS notification endpoint registered for this call, if any. + When provided and accepted during call creation, the same sink MUST be returned in the 201 response and subsequent GET responses. + Notification credentials and other secrets are not returned in the Call resource. + allOf: + - $ref: '../common/CAMARA_event_common.yaml#/components/schemas/Sink' recordingEnabled: type: boolean description: Whether recording is enabled for this call. @@ -308,7 +322,10 @@ components: description: | Request to create a new Click to Dial call session. If a `sink` URL is provided, status notifications (CloudEvents in structured mode) will be delivered to it. + Without `sink`, no implicit event subscription is created. `sinkCredential` is optional; without it, the HTTPS sink does not require Bearer authentication. If a `sinkCredential` of type `ACCESSTOKEN` is provided, the access token is used as a Bearer token in the `Authorization` header during event delivery. + For `ACCESSTOKEN`, `sinkCredential.accessTokenExpiresUtc` is the absolute UTC timestamp at which the access token shall be considered expired. Token expiration SHOULD occur after the end of the call's lifetime, allowing the consumer to receive notifications throughout the call. + If the token expires while the call is still active, the consumer will stop receiving notifications. Callback delivery failure does not roll back or cancel a successfully created call session; API consumers can query `GET /calls/{callId}` to reconcile state. type: object additionalProperties: false @@ -336,7 +353,7 @@ components: sinkCredential: credentialType: ACCESSTOKEN accessToken: "ya29.A0AR...exampletoken" - accessTokenExpiresUtc: "2026-01-01T00:00:00Z" + accessTokenExpiresUtc: "2027-12-31T23:59:59Z" accessTokenType: bearer recordingEnabled: true diff --git a/code/Test_definitions/click-to-dial-createCall.feature b/code/Test_definitions/click-to-dial-createCall.feature index 2e3f0c5..afc0ca2 100644 --- a/code/Test_definitions/click-to-dial-createCall.feature +++ b/code/Test_definitions/click-to-dial-createCall.feature @@ -3,6 +3,8 @@ Feature: CAMARA Click to Dial API, vwip - Operation createCall # # Implementation indications: # * apiRoot: API root of the server URL + # * A notification-capable provider and monitored HTTPS sinks with and without Bearer authentication are needed for event scenarios. + # * These are declarative test definitions; subscription inspection and new absence assertions require implementation-specific step bindings. # # References to OAS spec schemas refer to schemas specified in click-to-dial.yaml @@ -22,6 +24,8 @@ Feature: CAMARA Click to Dial API, vwip - Operation createCall Scenario: Common validations for createCall success scenario Given the request property "$.caller" is set to a valid caller number in E.164 format And the request property "$.callee" is set to a valid callee number in E.164 format + And the request property "$.sink" is removed from the request body + And the request property "$.sinkCredential" is removed from the request body When the request "createCall" is sent Then the response status code is 201 And the response header "Content-Type" is "application/json" @@ -30,23 +34,47 @@ Feature: CAMARA Click to Dial API, vwip - Operation createCall And the response property "$.callId" exists And the response property "$.caller" has the same value as createCall request property "$.caller" And the response property "$.callee" has the same value as createCall request property "$.callee" + And the response property "$.sink" does not exist + And no implicit event subscription is created for the call @createcall_event_notification Scenario: Events are received when sink and access token credential are provided Given the request property "$.caller" is set to a valid caller number in E.164 format And the request property "$.callee" is set to a valid callee number in E.164 format - And the request property "$.sink" is set to a URL where events can be monitored + And the request property "$.sink" is set to a valid HTTPS URL where events can be monitored And the request property "$.sinkCredential.credentialType" is set to "ACCESSTOKEN" And the request property "$.sinkCredential.accessTokenType" is set to "bearer" And the request property "$.sinkCredential.accessToken" is set to a valid access token accepted by the events receiver And the request property "$.sinkCredential.accessTokenExpiresUtc" is set to a future RFC3339 timestamp When the request "createCall" is sent Then the response status code is 201 + And the response body complies with the OAS schema at "/components/schemas/Call" + And the response property "$.sink" has the same value as createCall request property "$.sink" + And the response property "$.sinkCredential" does not exist + And the response property "$.accessToken" does not exist + And the response body does not contain the sink credential access token And an event is received at the address of the request property "$.sink" And the event header "Authorization" is set to "Bearer " + the value of the request property "$.sinkCredential.accessToken" And the event header "Content-Type" is "application/cloudevents+json" And the event body complies with the OAS schema at "#/components/schemas/CallStatusChangedEvent" + @createcall_event_notification_without_credential + Scenario: Events are received when sink is provided without sinkCredential + Given the request property "$.caller" is set to a valid caller number in E.164 format + And the request property "$.callee" is set to a valid callee number in E.164 format + And the request property "$.sink" is set to a valid HTTPS URL where events can be monitored without Bearer authentication + And the request property "$.sinkCredential" is removed from the request body + When the request "createCall" is sent + Then the response status code is 201 + And the response body complies with the OAS schema at "/components/schemas/Call" + And the response property "$.sink" has the same value as createCall request property "$.sink" + And the response property "$.sinkCredential" does not exist + And the response property "$.accessToken" does not exist + And an event is received at the address of the request property "$.sink" + And the event header "Authorization" does not exist + And the event header "Content-Type" is "application/cloudevents+json" + And the event body complies with the OAS schema at "#/components/schemas/CallStatusChangedEvent" + @createcall_event_notification Scenario: Event body fields follow ClickToDial status rules Given the request property "$.caller" is set to a valid caller number in E.164 format diff --git a/code/Test_definitions/click-to-dial-getCall.feature b/code/Test_definitions/click-to-dial-getCall.feature index 71f6403..5875fe7 100644 --- a/code/Test_definitions/click-to-dial-getCall.feature +++ b/code/Test_definitions/click-to-dial-getCall.feature @@ -3,6 +3,8 @@ Feature: CAMARA Click to Dial API, vwip - Operation getCall # # Implementation indications: # * apiRoot: API root of the server URL + # * The registered-sink scenario needs a call created with a monitored HTTPS sink and an accepted ACCESSTOKEN sinkCredential. + # * These are declarative test definitions; the registered-sink setup and new absence assertions require implementation-specific step bindings. # # References to OAS spec schemas refer to schemas specified in click-to-dial.yaml @@ -26,6 +28,21 @@ Feature: CAMARA Click to Dial API, vwip - Operation getCall And the response header "x-correlator" has the same value as the request header "x-correlator" And the response body complies with the OAS schema at "/components/schemas/Call" + @getcall_success_registered_sink + Scenario: Get call details preserves the registered sink without exposing credentials + Given a valid call identifier obtained from a previous createCall response + And that call was created with a registered HTTPS sink and an accepted ACCESSTOKEN sinkCredential + And the request path parameter "callId" is set to that identifier + When the request "getCall" is sent + Then the response status code is 200 + And the response header "Content-Type" is "application/json" + And the response header "x-correlator" has the same value as the request header "x-correlator" + And the response body complies with the OAS schema at "/components/schemas/Call" + And the response property "$.sink" has the same value as createCall request property "$.sink" + And the response property "$.sinkCredential" does not exist + And the response property "$.accessToken" does not exist + And the response body does not contain the sink credential access token + @getcall_failure_malformed_callid Scenario: Fail to get call details due to malformed callId path parameter Given the request path parameter "callId" is set to a value that does not comply with the CallId schema diff --git a/documentation/API_documentation/click-to-dial_API.md b/documentation/API_documentation/click-to-dial_API.md index 4f18d9b..4d73c6c 100644 --- a/documentation/API_documentation/click-to-dial_API.md +++ b/documentation/API_documentation/click-to-dial_API.md @@ -33,7 +33,7 @@ curl -X POST "{apiRoot}/calls" \ "sinkCredential": { "credentialType": "ACCESSTOKEN", "accessToken": "{sink_access_token}", - "accessTokenExpiresUtc": "2025-12-31T23:59:59Z", + "accessTokenExpiresUtc": "2027-12-31T23:59:59Z", "accessTokenType": "bearer" }, "recordingEnabled": true @@ -49,6 +49,7 @@ curl -X POST "{apiRoot}/calls" \ "callee": { "number": "+447700900123" }, "status": "initiating", "createdAt": "2025-12-11T12:00:00Z", + "sink": "https://yourapp.example.com/clicktodialstatusnotify", "recordingEnabled": true } ``` @@ -153,7 +154,7 @@ The SinkCredential is a discriminator-based object defined by CAMARA Commonaliti | --------------------- | --------------------------------------------- | -------- | ---------------------- | | credentialType | Must be `ACCESSTOKEN` (discriminator) | Yes | `ACCESSTOKEN` | | accessToken | Access token used to authenticate event POSTs | Yes | `sink_token` | -| accessTokenExpiresUtc | UTC expiry timestamp for the access token | Yes | `2025-12-31T23:59:59Z` | +| accessTokenExpiresUtc | UTC expiry timestamp for the access token | Yes | `2027-12-31T23:59:59Z` | | accessTokenType | Token type (OAuth token type) | Yes | `bearer` | ##### Click to Dial Status Notify (callback payload) @@ -250,11 +251,16 @@ Example CloudEvent (structured mode) — `CALL_STATUS_CHANGED_EXAMPLE` from the ### Callback Authentication, Retries, and Resilience -* **Authentication**: When delivering events to a client's `sink` URL that requires authentication, the provider MUST use the provided `sinkCredential`. If the credential type is `ACCESSTOKEN`, the provider MUST set the HTTP `Authorization` header to `Authorization: Bearer ` (using the value of the `accessToken` field from the request). +* **Optional callback authentication**: `sinkCredential` is optional in the CAMARA implicit subscription model. Without it, notifications can be delivered to an HTTPS `sink` that does not require Bearer authentication. When a credential is provided, the provider MUST use the specified authentication mechanism; for `ACCESSTOKEN`, it MUST set `Authorization: Bearer ` using the supplied token. Authenticated callbacks are recommended for production. The callback runs from Provider to Consumer; creating a call still requires the Consumer's OAuth/OpenID access token for the Provider API. +* **Token lifetime (`ACCESSTOKEN`)**: `accessTokenExpiresUtc` is the absolute UTC timestamp at which the access token shall be considered expired. Token expiration SHOULD occur after the end of the call's lifetime, allowing the consumer to receive notifications throughout the call. If the token expires while the call is still active, the consumer will stop receiving notifications, as specified in [Commonalities r4.4 Event Guide §3.4](https://github.com/camaraproject/Commonalities/blob/r4.4/documentation/CAMARA-API-Event-Subscription-and-Notification-Guide.md#34-subscription-ended-event). +* **Subscription and sink echo**: Without `sink`, no implicit notification subscription is created. If a supplied `sink` is accepted, the 201 response MUST echo the same URL, and subsequent `GET /calls/{callId}` responses return the registered sink. Call responses do not return `sinkCredential`, access tokens, or other credential secrets. If notification delivery is unsupported, the existing `422 EVENT_NOTIFICATIONS_NOT_SUPPORTED` response applies and no call is created. +* **Consumer verification**: Callbacks without authentication risk forged events and spoofed call state. Consumers SHOULD validate the CloudEvent `id`, `time`, `source`, `type`, and event schema, and handle duplicates and replay according to their policy. Bearer authentication alone does not prevent all replay attacks. Do not trigger sensitive or irreversible actions solely from an unverified callback; use `GET /calls/{callId}` to confirm authoritative state when needed. * **Delivery Retries**: Providers SHOULD ensure event `id` uniqueness. Transient callback delivery failures (e.g., connection timeouts, DNS resolution failures, or receiving `5xx` server error responses from the client's `sink` URL) may be retried by the provider according to its internal retry and backoff policy. * **Session Resilience**: A callback delivery failure does **not** roll back or cancel a successfully created call session. The call session will continue to be processed and connected normally. * **State Reconciliation**: API consumers can actively reconcile the call session state at any time by calling `GET /calls/{callId}` to retrieve the current status of the call. This is particularly useful if status notification events are delayed or fail to be delivered. +The shared r4.4 `AccessTokenCredential.accessTokenExpiresUtc` schema description still refers to explicit-subscription `ACCESS_TOKEN_EXPIRED` termination notifications. CTD uses the call-specific implicit-subscription guidance above; the shared credential schema is reused unchanged. + ## Call flow (sequence diagram) The following sequence diagram shows the typical flow when creating a Click-to-Dial session, delivering events, and terminating the call. This is a logical flow; implementers may optimize or parallelise steps. @@ -297,7 +303,7 @@ curl -X POST "{apiRoot}/calls" \ "sinkCredential": { "credentialType": "ACCESSTOKEN", "accessToken": "{sink_access_token}", - "accessTokenExpiresUtc": "2025-12-31T23:59:59Z", + "accessTokenExpiresUtc": "2027-12-31T23:59:59Z", "accessTokenType": "bearer" } }'