lumnik. / scenarios All scenarios · Français

SOAP WCF and WS-Security: the password rotated, the Fault says so

Goal Read a WCF service protected by a UsernameToken, survive the rotation of its password, and be told when an order is confirmed past its promised date.

A distributor exposes its open orders through a WCF service: HTTPS under the company's CA, a UsernameToken inside the envelope.

orders-20261105.tsv
OrderNo	Customer	OrderDate	PromisedDate	ConfirmedDate	Warehouse	Amount
CV-26-0001	Auto Bérard	2026-10-02	2026-10-24	2026-10-23	Valence	509.25
CV-26-0002	Pneus du Sud	2026-10-03	2026-10-26	2026-10-24	Grenoble	898.50
CV-26-0003	Carrosserie Lefèvre	2026-10-04	2026-10-28	2026-10-25	Lyon	1287.75
CV-26-0004	Moto Évasion	2026-10-05	2026-10-30	2026-10-30	Valence	1676.00
CV-26-0005	Cycles Gaëtan	2026-10-06	2026-11-01	2026-10-31	Grenoble	2065.25
CV-26-0006	Dépannage Noël	2026-10-07	2026-11-03	2026-11-01	Lyon	2454.50
CV-26-0007	Agri-Méca	2026-10-08	2026-11-05	2026-11-02	Lyon	2843.75
CV-26-0008	Poids Lourds Rhône	2026-10-09	2026-11-07	2026-11-07	Grenoble	3232.00
CV-26-0009	Taxi Côte	2026-10-10	2026-10-31	2026-10-30	Lyon	3621.25
CV-26-0010	Location Hébert	2026-10-11	2026-11-02	2026-10-31	Valence	4010.50
CV-26-0011	Électro-Diesel	2026-10-12	2026-11-04	2026-11-01	Grenoble	399.75
CV-26-0012	Garage Martin	2026-10-13	2026-11-06	2026-11-06	Lyon	788.00
CV-26-0013	Auto Bérard	2026-10-14	2026-11-08	2026-11-07	Valence	1177.25
CV-26-0014	Pneus du Sud	2026-10-15	2026-11-10	2026-11-08	Grenoble	1566.50
CV-26-0015	Carrosserie Lefèvre	2026-10-16	2026-11-12	2026-11-09	Lyon	1955.75
CV-26-0016	Moto Évasion	2026-10-17	2026-11-14	2026-11-14	Lyon	2344.00
CV-26-0017	Cycles Gaëtan	2026-10-18	2026-11-16	2026-11-15	Grenoble	2733.25
CV-26-0018	Dépannage Noël	2026-10-19	2026-11-09	2026-11-07	Lyon	3122.50
CV-26-0019	Agri-Méca	2026-10-20	2026-11-11	2026-11-08	Valence	3511.75

… 11 more lines

manifests/10-orders.yaml
# A distributor's order service on WCF: BasicHttpBinding over HTTPS (TransportWithMessageCredential), the caller# authenticated by a WS-Security UsernameToken inside the envelope. The certificate is signed by the company's own# CA, trusted for this connector only. The password lives in lm secret, never in the manifest.apiVersion: connectors.lumnik.io/v1kind: Connectormetadata:  name: scenario-wcf-a  connector_type: rest-generic  scopes: [sales-wcf-a]spec:  base_url: https://wcf-orders:8443  tls:    ca_secret: SCENARIO_WCF_CA_A          # lm secret set SCENARIO_WCF_CA_A - < ca.crt  auth:    kind: none                            # no HTTP auth: the credential travels in the envelope  endpoints:    - id: open-orders      kind: soap      path: /OrderService.svc      soap_action: "http://distrib.example/orders/IOrderService/GetOpenOrders"      header: |        <wsse:Security soap:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">          <wsse:UsernameToken>            <wsse:Username>lumnik-reader</wsse:Username>            <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">{{secret:SCENARIO_WCF_PASSWORD_A}}</wsse:Password>          </wsse:UsernameToken>        </wsse:Security>      body: |        <GetOpenOrders xmlns="http://distrib.example/orders">          <page>{{page}}</page>          <pageSize>{{page_size}}</pageSize>        </GetOpenOrders>      pagination:        kind: soap        page_size: 10        last_page_path: "$.Body.GetOpenOrdersResponse.GetOpenOrdersResult.IsLastPage"        response_path: "$.Body.GetOpenOrdersResponse.GetOpenOrdersResult.Orders.Order"      target:        kind: hybrid        table: ext.scenario_wcf_a        unique_key: "$.OrderNo"        promote:          - { name: order_no, path: "$.OrderNo", type: text }          - { name: customer, path: "$.Customer", type: text }          - { name: promised_date, path: "$.PromisedDate", type: date }          - { name: confirmed_date, path: "$.ConfirmedDate", type: date }          - { name: warehouse, path: "$.Warehouse", type: text }          - { name: amount, path: "$.Amount", type: decimal }
manifests/20-confirmed-late.yaml
apiVersion: apps.lumnik.io/v1kind: Processmetadata:  name: scenario-wcf-late-aspec:  scope: sales-wcf-a  on: { event: data.row.changed, table: ext.scenario_wcf_a }  when:    - "confirmed_date>{promised_date}"     # the warehouse confirms a ship date past the customer's promise  emit:    type: decision.confirmed-late-a    message: "{order_no} {customer}: promised {promised_date}, confirmed {confirmed_date} by {warehouse} — call the customer before they call"    key: order_no
$ lm source rest test scenario-wcf-a
ENDPOINT     METHOD  STATUS  ITEMS  VERDICT
open-orders  POST    -       0      I/O error (IOException)
Error: 1 of 1 enabled endpoint(s) did not answer with readable data

$ lm connector run scenario-wcf-a
run scheduled: f3eb3ee1-c4ee-4993-a2eb-b9da719be202

$ lm run get f3eb3ee1-c4ee-4993-a2eb-b9da719be202
ID        CONNECTOR       ENDPOINT     STATUS  IN  OUT  SKIPPED  DEAD-LETTERS  STARTED           ENDED             EVALUATED
f3eb3ee1  scenario-wcf-a  open-orders  Failed  0   0    0        0             2026-11-07 00:16  2026-11-07 00:17  yes
✖ run failed: I/O error (IOException)
manifests/11-orders-http11.yaml
# The same connector, speaking HTTP/1.1: the service answers a refused credential with its Fault, then# resets the HTTP/2 stream, and over HTTP/2 the answer is lost on the way (#1859).apiVersion: connectors.lumnik.io/v1kind: Connectormetadata:  name: scenario-wcf-a  connector_type: rest-generic  scopes: [sales-wcf-a]spec:  base_url: https://wcf-orders:8443  http_version: "1.1"                     # Kestrel resets an HTTP/2 stream after a Fault: ask for HTTP/1.1  tls:    ca_secret: SCENARIO_WCF_CA_A          # lm secret set SCENARIO_WCF_CA_A - < ca.crt  auth:    kind: none                            # no HTTP auth: the credential travels in the envelope  endpoints:    - id: open-orders      kind: soap      path: /OrderService.svc      soap_action: "http://distrib.example/orders/IOrderService/GetOpenOrders"      header: |        <wsse:Security soap:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">          <wsse:UsernameToken>            <wsse:Username>lumnik-reader</wsse:Username>            <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">{{secret:SCENARIO_WCF_PASSWORD_A}}</wsse:Password>          </wsse:UsernameToken>        </wsse:Security>      body: |        <GetOpenOrders xmlns="http://distrib.example/orders">          <page>{{page}}</page>          <pageSize>{{page_size}}</pageSize>        </GetOpenOrders>      pagination:        kind: soap        page_size: 10        last_page_path: "$.Body.GetOpenOrdersResponse.GetOpenOrdersResult.IsLastPage"        response_path: "$.Body.GetOpenOrdersResponse.GetOpenOrdersResult.Orders.Order"      target:        kind: hybrid        table: ext.scenario_wcf_a        unique_key: "$.OrderNo"        promote:          - { name: order_no, path: "$.OrderNo", type: text }          - { name: customer, path: "$.Customer", type: text }          - { name: promised_date, path: "$.PromisedDate", type: date }          - { name: confirmed_date, path: "$.ConfirmedDate", type: date }          - { name: warehouse, path: "$.Warehouse", type: text }          - { name: amount, path: "$.Amount", type: decimal }
orders-20261106.tsv
OrderNo	Customer	OrderDate	PromisedDate	ConfirmedDate	Warehouse	Amount
CV-26-0001	Auto Bérard	2026-10-02	2026-10-24	2026-10-23	Valence	509.25
CV-26-0002	Pneus du Sud	2026-10-03	2026-10-26	2026-10-24	Grenoble	898.50
CV-26-0004	Moto Évasion	2026-10-05	2026-10-30	2026-10-30	Valence	1676.00
CV-26-0005	Cycles Gaëtan	2026-10-06	2026-11-01	2026-10-31	Grenoble	2065.25
CV-26-0006	Dépannage Noël	2026-10-07	2026-11-03	2026-11-01	Lyon	2454.50
CV-26-0007	Agri-Méca	2026-10-08	2026-11-05	2026-11-14	Lyon	2843.75
CV-26-0008	Poids Lourds Rhône	2026-10-09	2026-11-07	2026-11-07	Grenoble	3232.00
CV-26-0009	Taxi Côte	2026-10-10	2026-10-31	2026-10-30	Lyon	3621.25
CV-26-0010	Location Hébert	2026-10-11	2026-11-02	2026-10-31	Valence	4010.50
CV-26-0012	Garage Martin	2026-10-13	2026-11-06	2026-11-06	Lyon	788.00
CV-26-0013	Auto Bérard	2026-10-14	2026-11-08	2026-11-07	Valence	1177.25
CV-26-0014	Pneus du Sud	2026-10-15	2026-11-10	2026-11-08	Grenoble	1566.50
CV-26-0015	Carrosserie Lefèvre	2026-10-16	2026-11-12	2026-11-09	Lyon	1955.75
CV-26-0016	Moto Évasion	2026-10-17	2026-11-14	2026-11-18	Lyon	2344.00
CV-26-0017	Cycles Gaëtan	2026-10-18	2026-11-16	2026-11-15	Grenoble	2733.25
CV-26-0018	Dépannage Noël	2026-10-19	2026-11-09	2026-11-07	Lyon	3122.50
CV-26-0019	Agri-Méca	2026-10-20	2026-11-11	2026-11-08	Valence	3511.75
CV-26-0020	Poids Lourds Rhône	2026-10-21	2026-11-13	2026-11-13	Grenoble	3900.00
CV-26-0021	Taxi Côte	2026-10-22	2026-11-15	2026-11-14	Lyon	289.25

… 9 more lines

Limits

  • Replayed against CoreWCF on Kestrel. A .NET Framework WCF on IIS was not replayed: whether it accepts a UsernameToken without a Timestamp is not verified here.
  • The password travels in clear inside the envelope (PasswordText), protected by HTTPS. A PasswordDigest needs a nonce and a date on every call: no placeholder makes them.
  • The pre-flight reads HTTP 500 where the run names the Fault (#1861).
  • SOAP 1.1 only: a SOAP 1.2 service (wsHttpBinding) is not read.
  • Orders, customers and passwords synthetic.

Sales · WCF

The orders live behind a WCF service.

SOAP, HTTPS, a UsernameToken inside the envelope.

<wsse:UsernameToken>

The problem

The password rotated. The hub said: I/O error.

  • The service had answered its Fault, then reset the HTTP/2 stream.
  • Three attempts for one refused credential.

What is not changed

The service stays as it is.

  • Its binding, its CA, its contract.
  • No proxy, no .NET code.

What had to be written

http_version: "1.1"

One line in the connector.

  • The Fault arrives whole.
  • It is not retried.

What was fixed

The HTTP version is chosen per connector (#1859).

  • New secret, same manifest: the read resumes.
  • Three late orders: three decisions.

Try it

The scenario replays locally.

Every step is published with its output.

This scenario runs on lumnik Open, free and open source.

Try lumnik Open →