# SOAP Security Tutorial - WS-Security, body credentials and client certificates

UsernameToken, X.509 signing, SAML, body credentials and mutual TLS - each configured from the Dashboard with no code changes.

Production SOAP endpoints authenticate in three different places - the SOAP header, the message body and the TLS layer - and they often demand more than one at once. This tutorial configures each mechanism from the Dashboard and shows that the service code never changes: security is a property of the connection, not of your Python.

The running example continues from the [calling tutorial](https://zato.io/tutorials/soap/calling.html) - an immunization registry that requires the submitter's username and password inside the message body, over TLS. Along the way you will also set up UsernameToken, message signing and SAML, the mechanisms that other ecosystems require for the same connections.

## In this tutorial {#in-this-tutorial}

1. [Create a WS-Security definition](https://zato.io/tutorials/soap/security.html#create-a-ws-security-definition)
2. [Attach it to a connection](https://zato.io/tutorials/soap/security.html#attach-it-to-a-connection)
3. [Inject credentials into the body](https://zato.io/tutorials/soap/security.html#inject-credentials-into-the-body)
4. [Sign messages with X.509](https://zato.io/tutorials/soap/security.html#sign-messages-with-x509)
5. [Add SAML assertions](https://zato.io/tutorials/soap/security.html#add-saml-assertions)
6. [Present a client certificate](https://zato.io/tutorials/soap/security.html#present-a-client-certificate)
7. [Deploy with enmasse](https://zato.io/tutorials/soap/security.html#deploy-with-enmasse)

> **Remember:** you can [connect your AI copilot](https://zato.io/docs/ai/connect-your-copilot.html) to Zato documentation.

## Create a WS-Security definition {#create-a-ws-security-definition}

A [WS-Security](https://zato.io/docs/security/ws-security.html) definition holds the credentials and crypto material once - connections then reference it by name. One definition can serve many connections.

**Step 1.** In the Dashboard, navigate to **Security** > **WS-Security**, click **Create a new WS-Security definition** and fill in the form:

- **Name:** `Registry Credentials`
- **Username:** `my-registry-user`
- **Mode:** `Username token`
- Click **OK**

**Step 2.** The definition is created with a random password - click **Change password** in its row and set the real one. Passwords are never part of the create or edit forms, so they never linger in browser autofill or form history.

The mode determines which WS-Security elements the definition sends:

- **Username token** - a `wsse:UsernameToken` header with the username and password; the **Username token** tab has a **Password digest** switch for endpoints that require digest form with a nonce and timestamp
- **X.509** - signing and encryption, configured in the **Crypto material** tab
- **SAML** - a SAML 2.0 assertion built from the fields in the **SAML** tab

Every field has a **How does it work?** link explaining what it does.

## Attach it to a connection {#attach-it-to-a-connection}

Open your outgoing SOAP connection (**Connections** > **Outgoing** > **SOAP**), switch to the **Security** tab and pick `WS-Security/Registry Credentials` from the security dropdown. Click **OK**.

From this moment every `invoke` through this connection includes the WS-Security header. The service code is unchanged from the version without security:

```python
request = SOAPMessage()
request.namespace = 'urn:cdc:iisb:2011'
request.echoBack = 'Hello registry'

response = self.soap['Immunization Registry'].invoke('connectivityTest', request)
```

## Inject credentials into the body {#inject-credentials-into-the-body}

Immunization registries following the CDC WSDL do not read the SOAP header at all - they expect the username and password as the first elements of the business message:

```xml
<sub:submitSingleMessage>
    <sub:username>my-registry-user</sub:username>
    <sub:password>*******</sub:password>
    <sub:hl7Message>MSH|^~\&amp;|...</sub:hl7Message>
</sub:submitSingleMessage>
```

**Step 1.** Open the connection and switch to the **Body credentials** tab.

**Step 2.** Add two rows:

- **Element name:** `username`
- **Element name:** `password`

Rows without a position become the operation's first children in row order - which is exactly the layout above. For an endpoint that requires a credential elsewhere, set a 1-based **Position** on its row.

**Step 3.** Click **OK**.

The values come from the WS-Security definition attached to the connection, and injection happens inside `invoke`, after your service built the message. The service never receives, stores or logs the credentials:

```python
# Only business fields here - username and password are injected
# by the connection, per its body-credential mapping.
request = SOAPMessage()
request.namespace = 'urn:cdc:iisb:2011'
request.hl7Message = hl7_payload

response = self.soap['Registry Submit'].invoke('submitSingleMessage', request)
```

> **Try it:** Invoke the service and check the registry's reply - then change the definition's password through **Change password** and invoke again to watch the new value take effect without touching the connection or the code.

## Sign messages with X.509 {#sign-messages-with-x509}

For endpoints that verify message integrity, switch the definition's **Mode** to `X.509` and open the **Crypto material** tab:

- **Sign** - turn it on
- **Signing key** - your private key, PEM
- **Signing certificate chain** - your certificate (and any intermediates), PEM, sent along so the receiver can verify the signature

To also encrypt for the receiver:

- **Encrypt** - turn it on
- **Peer certificate** - the receiver's certificate, PEM

For the return direction, the **Decryption key** decrypts what comes back encrypted to you and **Trust anchors** holds the CA certificates that peers' signatures are validated against.

The signature and encryption are standard XML-DSig and XML-Encryption, applied to each outgoing envelope when `invoke` runs - once more, no service changes.

## Add SAML assertions {#add-saml-assertions}

Healthcare document exchanges (IHE XUA, TEFCA) identify the requesting user with a SAML 2.0 assertion in the WS-Security header, signed by the sending organization.

Switch the definition's **Mode** to `SAML` and fill in the **SAML** tab:

- **Issuer** - who vouches for the assertion, e.g. `https://my-hospital.example.com`
- **Subject** - the user or system the assertion is about
- **Audience** - who the assertion is meant for, e.g. the exchange's community identifier

To sign the assertion - which these profiles require - turn on **Sign** in the **Crypto material** tab and provide the **Signing key** and **Signing certificate chain**, the same fields the X.509 mode uses. The assertion is generated fresh for each request and includes an enveloped RSA-SHA256 signature.

## Present a client certificate {#present-a-client-certificate}

Endpoints that authenticate at the TLS layer need the connection itself to present a certificate. That is a connection-level setting, so it combines with everything above.

Open the connection's **Security** tab and fill in:

- **Client certificate** - a local path, e.g. `/opt/zato/certs/client-cert.pem`
- **Client key** - a local path, e.g. `/opt/zato/certs/client-key.pem`

The paths point to PEM files on the server. In containerized deployments, mount the certificates into the container and put the mount paths in these fields. If the certificate and key live in one combined PEM file, fill in only the certificate field.

A connection can now do all three at once - present the client certificate, inject body credentials and sign its messages - which is precisely the combination some registries and national networks require.

## Deploy with enmasse {#deploy-with-enmasse}

The complete setup - definition and connection - in YAML:

```yaml
security:

  - name: Registry Credentials
    username: my-registry-user
    password: Zato_Enmasse_Env.RegistryPassword
    type: wss
    mode: username_token

outgoing_soap:

  - name: Registry Submit
    host: https://registry.example.gov
    url_path: /iisb/services
    security: Registry Credentials
    soap_action: urn:cdc:iisb:2011:submitSingleMessage
    soap_version: "1.2"
    timeout: 30
    tls_client_cert: /opt/zato/certs/client-cert.pem
    tls_client_key: /opt/zato/certs/client-key.pem
    body_credentials:
      - name: username
      - name: password
```

The `Zato_Enmasse_Env.` prefix reads the password from an environment variable, so no secret is ever committed to the repository.

The other two modes are YAML too. An X.509 definition holds its switches and PEM material inline:

```yaml
security:

  - name: Registry Signing
    username: my-registry-user
    type: wss
    mode: x509
    sign: true
    encrypt: true
    signing_key: |-
      -----BEGIN PRIVATE KEY-----
      ...
      -----END PRIVATE KEY-----
    signing_certificate_chain: |-
      -----BEGIN CERTIFICATE-----
      ...
      -----END CERTIFICATE-----
    decryption_key: |-
      -----BEGIN PRIVATE KEY-----
      ...
      -----END PRIVATE KEY-----
    peer_certificate: |-
      -----BEGIN CERTIFICATE-----
      ...
      -----END CERTIFICATE-----
    trust_anchors: |-
      -----BEGIN CERTIFICATE-----
      ...
      -----END CERTIFICATE-----
```

And a SAML definition holds the assertion fields, plus the signing material when the assertion is to be signed:

```yaml
security:

  - name: Exchange Assertion
    username: my-registry-user
    type: wss
    mode: saml
    issuer: https://my-hospital.example.com
    subject: CN=Dr Smith,O=Example Hospital
    audience: urn:exchange:community
    sign: true
    signing_key: |-
      -----BEGIN PRIVATE KEY-----
      ...
      -----END PRIVATE KEY-----
    signing_certificate_chain: |-
      -----BEGIN CERTIFICATE-----
      ...
      -----END CERTIFICATE-----
```

Import it in the Dashboard under `System → Config → Import enmasse`, or mount the file under `/opt/hot-deploy/enmasse/enmasse.yaml` inside the container to have it imported on start.

Every field of all three modes is documented in the [enmasse reference](https://zato.io/docs/admin/enmasse-reference.html#wss).

## What you built {#what-you-built}

- A **WS-Security definition** holding credentials and crypto material once, referenced by name
- **UsernameToken** authentication with optional password digest
- **Body-credential mapping** - username and password injected into the message body by the connection, invisible to service code
- **X.509 signing and encryption** and **signed SAML assertions**, switched on per definition
- **Mutual TLS** through client certificate paths on the connection
- **Enmasse deployment** with secrets kept in environment variables

For the reference documentation of every mechanism, read [SOAP security](https://zato.io/docs/dev/soap/security/index.html).
