Securing Web Services

Note The Enforce* helper methods described in this topic require Process Director 6.2.100 or later. A manual workaround for earlier versions is provided at the end of this topic.

Purpose

This topic documents the supported, recommended way to protect a custom web service (a SOAP .asmx service built on the Process Director SDK base class bpWebService) with the same source-IP restriction that Process Director enforces for its built-in web services. It formalizes the approach that links custom web service IP restrictions to Process Director's core configuration.

It is written to be followed end to end by a developer who can write C# but is not assumed to be a Process Director administrator.

Overview

Process Director's built-in web services (for example, wsAdmin and wsContent) automatically enforce two checks on every call:

  1. Web services enabled: the global Web Service Enabled setting.
  2. Source-IP allowlist: the caller's IP must match the configured Web Service Restrictions allowlist.

For custom web services, these checks are intentionally skipped. Internally, the request pipeline guards them with a !CustomService condition, so a custom endpoint will run for any caller who knows its URL—even from an IP outside the allowlist. This is by design (it avoids breaking existing customer integrations), but it means the developer of a custom web service is responsible for opting in to these protections.

This topic covers how to go about implementing source-IP restriction. See the Authentication Scope section at the end for what it deliberately does not cover.

Step 1: Configure the Restriction in Process Director

Both checks read directly from the standard Process Director configuration, so you configure them once in the admin UI and every opted-in custom service inherits them.

Where: Administration > Installation Settings > Properties tab (the Process Director Properties screen, admin/properties.aspx). You must be a System Administrator to access this page.

On that screen, set the following fields (the italic text is the help shown on-screen next to each field):

FIELD ON SCREEN

PURPOSE

ON-SCREEN HELP

Web Service Enabled Master switch for all web service APIs. "Set to true to enable web service calls for the Process Director APIs."
Require authentication Requires SOAP-header authentication (see the Authentication Scope section below). Governs built-in services.
Web Service Restrictions The source-IP allowlist. "Optional comma-separated list of the only IP addresses that web services are allowed from."

After editing, save the Properties screen. The new values apply to subsequent web service calls; if a change doesn't appear to take effect, recycle the IIS application pool (or restart the Process Director site).

Important "Optional" is a trap. If Web Service Restrictions is left empty, there is no IP restriction at all—every IP is allowed. Calling EnforceIPRestriction() in your code does nothing useful until this field is populated. Always confirm it is set in production.

What You Can Type into "Web Service Restrictions"

The field is a comma-separated list. Each entry must be one of these three forms (matching is IPv4 only):

FORM

EXAMPLE

WHAT IT ALLOWS

Exact IP 203.0.113.5 Only that single host.
CIDR block 203.0.113.0/24 The whole 203.0.113.x subnet.
0-octet wildcard 203.0.113.0 The whole 203.0.113.x range—a 0 in any octet means "match anything here."

A combined example: 203.0.113.5, 198.51.100.0/24, 10.20.0.0

Important Two surprises worth memorizing are described in the two points below.

  1. * does not work. 203.0.113.* is not recognized and will silently match nothing. Use a 0 octet (203.0.113.0) or CIDR (203.0.113.0/24) instead.
  2. 0 is a wildcard, not a host. 203.0.113.0 does not mean "the .0 host"—it opens the entire 203.0.113.x subnet. To allow a single host, use its exact address.

Not supported: asterisk wildcards (*), ranges (a-b), hostnames (including localhost), and IPv6 addresses.

Local Requests Are Always Allowed

Important The IP check governs remote callers only. A call originating from the Process Director server itself—the same machine, 127.0.0.1, or any IP in the system's local IPs list—is always allowed, regardless of the Web Service Restrictions setting (and regardless of EnforceIPRestriction()). Consequently, you cannot test the restriction from the server; a call run on the PD box will always succeed. Always test rejection from a different machine (see Step 4).

If You Are Behind a Load Balancer or Reverse Proxy

This is the most common source of "it rejects everything" or "it's not actually restricting." Process Director evaluates the IP it sees on the incoming request:

  • By default, that is the IP of whatever connects to PD directly—which, behind a load balancer or proxy, is the load balancer's IP, not the end client's.
  • Process Director honors the X-Forwarded-For header only if the proxy is configured in PD's settings. When it is, PD uses the last address in X-Forwarded-For.

So you must do one of the following:

  • Add the load balancer's IP to Web Service Restrictions, or
  • Configure the proxy in PD so the real client IP is evaluated, then allowlist the client IPs.

When in doubt, use the verification step below to discover exactly which IP PD is seeing.

Step 2: Build the Custom Web Service

A custom web service is a single .asmx text file whose class inherits from the SDK base class bpWebService. You write the C# inline in the .asmx file—there is no separate project to compile; IIS compiles it on the first request.

Note Don't write yours like the built-in services. If you open a shipped service such as wsUser.asmx, it's a one-line file pointing at a class that is precompiled into the product assembly. Do not copy that form. Your custom service keeps its C# inline, as shown below.

2a. Name It Once: Three Places That Must Match

Choose one name for your service (for example, wsTestService) and use it in three places that must stay in sync:

WHERE

MUST BE

EXAMPLE

The file name <name>.asmx wsTestService.asmx
The directive's Class="…" The C# class name you declare below, exactly. Class="wsTestService"
The class declaration public class <name> : bpWebService public class wsTestService : bpWebService

Important Most common mistake: Class="…" is a C# class name, not a URL or a folder path. It must match the public class … you declare, character for character. If they differ, the service returns HTTP 500 on the very first call. Do not put a dotted namespace path here (for example, Company.Web.wsTestService) unless your class is actually declared inside that C# namespace.

This is different from the [WebService(Namespace = "http://…")] attribute below it: that one is the service's XML/SOAP namespace—a URI used in the WSDL—and stays a URL.

The file name drives the URL. wsTestService.asmx is called at {your PD base URL}/Services/wsTestService.asmx, regardless of the class name.

2b. Start with a Minimal Service That Just Responds

Get a service running before adding any logic. This is the smallest service that works:

<%@ WebService Language="C#" Class="wsTestService" %>

using System.Web.Services;
using BPLogix.WorkflowDirector.SDK;   // bpWebService base class

[WebService(Namespace = "http://www.bplogix.com/custom")]   // XML/SOAP namespace (a URI) - NOT your C# class name
public class wsTestService : bpWebService
{
    [WebMethod]
    public string Hello() => "Custom web service is running.";
}

Deploy it by placing the .asmx file in the Services folder of the Process Director web application (no build step—IIS compiles it on first request). Then confirm it responds with a browser GET (works from localhost):

{your PD base URL}/Services/wsTestService.asmx/Hello

You should get back <string …>Custom web service is running.</string>. If you get HTTP 500 instead, re-check the three names in the table above—they almost certainly don't match.

Note To see all operations or build a client, open the service's help page at {your PD base URL}/Services/wsTestService.asmx to list its methods, or fetch the WSDL at …/wsTestService.asmx?WSDL to generate a SOAP client (Postman, SoapUI, svcutil, and so on).

2c. Add SDK Logic and Release the Database Handle

Once it responds, add real logic. Any method that uses the SDK or database should call bpClose() before returning, so the database handle is released immediately instead of waiting for garbage collection:

[WebMethod]
public string GetEmailForUserID(string UserID)
{
    // `bp` is provided by the bpWebService base class
    
var user = User.GetUserByUserID(bp, UserID);   
    bpClose();  // release the DB handle
    return user?.Email;
}

Note Security enforcement is intentionally not shown here. Get the service working first, then add the IP restriction in Step 3.

Step 3: Add the Enforcement Call to Every Web Method

As of version 6.2.100, the SDK base class bpWebService exposes three opt-in enforcement helpers. Call the one you need as the first line of each web method, before any business logic:

METHOD

WHAT IT ENFORCES

ON FAILURE

EnforceIPRestriction() Caller's IP is in the Web Service Restrictions allowlist. Throws SoapException (ServerFaultCode).
EnforceWebServicesEnabled() Web services are globally enabled. Throws SoapException (ServerFaultCode).
EnforceWebServiceRestrictions() Both of the above, in order. Throws SoapException on first failure.

Taking the GetEmailForUserID method from Step 2c, add the enforcement call as its first line:

[WebMethod]
public string GetEmailForUserID(string UserID)
{
    // Enforce the source-IP allowlist BEFORE any logic.
    // If the caller's IP is not permitted, this throws a SOAP fault
    // and the method stops here.

    EnforceIPRestriction();

    var user = User.GetUserByUserID(bp, UserID);
    bpClose();
    return user?.Email;
}

For the common need—IP restriction only—EnforceIPRestriction() is sufficient. If you also want calls rejected when web services are globally disabled, use EnforceWebServiceRestrictions() instead (it runs both checks):

[WebMethod]
public string GetEmailForUserID(string UserID)
{
    // web-services-enabled check + source-IP allowlist check
    EnforceWebServiceRestrictions();
    var user = User.GetUserByUserID(bp, UserID);
    bpClose();
    return user?.Email;
}

Important Enforcement is per web method. There is no global switch for custom services. Every [WebMethod] you want protected needs its own enforcement call—a method without one is an open endpoint.

Step 4: Verify It Works (and Find the IP to Allowlist)

Important Test from a different machine. Calls from the Process Director server itself are always allowed (see "Local Requests Are Always Allowed" in Step 1), so a rejection test run on the server will never fail. Use a separate client machine whose IP is not in Web Service Restrictions.

  1. Confirm Web Service Restrictions is populated and the web method calls EnforceIPRestriction().
  2. From the non-allowlisted machine, call the method—for example, via a browser or SOAP client:

    {your PD base URL}/Services/wsTestService.asmx/GetEmailForUserID?UserID=test

  3. The call is rejected with a SOAP fault whose message is:

    BP_WS_INVALID_IP: Web service request rejected. Web service calls are not allowed from your IP Address: <the IP PD saw>

  4. The IP shown in that message is exactly what you add to Web Service Restrictions to permit that host—and it tells you immediately whether PD is seeing the real client or a load balancer (see the load-balancer note in Step 1).
  5. Add the intended IP(s), save, retry from an allowed host, and confirm the call now succeeds.

Note To check your version, note that the Enforce* helpers exist only in Process Director 6.2.100 and later. The product version is shown in the admin UI; if you're on an earlier build, use the workaround below.

Troubleshooting

SYMPTOM

LIKELY CAUSE

FIX

HTTP 500 on the very first call. Class="…" in the directive doesn't match your public class …. Make the file name, Class=, and class declaration all match (Step 2a).
Calls succeed even from outside the allowlist. (a) Web Service Restrictions is empty; (b) the method has no EnforceIPRestriction() call; (c) you're testing from the PD server (local bypass). Populate the allowlist; add the enforcement call; test from a different machine.
Every call is rejected, even ones that should be allowed. Behind a load balancer, PD sees the LB's IP, not the client's; or a hostname, *, or range was entered in the list. Allowlist the LB IP or configure the proxy (Step 1); use only exact IP, CIDR, or 0-octet forms.
localhost (or another hostname) in the list does nothing. Hostnames aren't supported—IPv4 only. Use the numeric IP; calls from the server are treated as local anyway.
GET test returns an error about the method or parameters. HTTP GET only supports simple-type parameters. Call via SOAP (Postman/SoapUI) using …/wsTestService.asmx?WSDL.

Compatibility with Earlier Versions (Manual Workaround)

On versions prior to 6.2.100, the Enforce* helpers are not available, but the underlying predicate methods (WebServiceValidSourceIP() and WebServicesEnabled()) are public on the SDK base class. Guard each method manually:

[WebMethod]
public string GetData(string id)
{
    if (!WebServiceValidSourceIP())
        throw new SoapException("Caller IP not permitted", SoapException.ServerFaultCode);

    // optional: also require web services to be globally enabled
    if (!WebServicesEnabled())
        throw new SoapException("Web services are disabled", SoapException.ServerFaultCode);

    return DoWork(id);
}

The Enforce* helpers simply package this exact pattern (matching the framework's own error messages and fault codes), so migrating to them later is a drop-in simplification.

Authentication Scope: What This Does Not Cover

The source-IP restriction limits where calls originate, not who makes them.

  • Process Director's built-in Require authentication setting (SOAP-header token authentication) governs the built-in services. Custom services do not currently have an exposed SDK helper to enforce SOAP-header authentication—the 6.2.100 helpers cover the enable and IP-restriction checks only.
  • Where a use case requires IP restriction only, IP restriction alone is sufficient and full authentication is not required.
  • If a custom service performs sensitive operations, treat IP restriction as a network control and plan for an additional authentication layer; exposing SOAP-header authentication to custom services is a possible future enhancement.

Quick Checklist

  • Web Service Enabled = true (Installation Settings > Properties).
  • Web Service Restrictions is populated (empty = no restriction).
  • Allowlist entries use exact IP, CIDR, or 0-octet wildcard—not *, ranges, hostnames, or IPv6.
  • Settings saved on the Properties screen (recycle the app pool if a change doesn't take effect).
  • Behind a load balancer: either the LB IP is allowlisted, or the proxy is configured so the real client IP is seen.
  • Custom service inherits bpWebService, names match (file, Class=, and class), and is deployed under /Services/.
  • Every [WebMethod] calls EnforceIPRestriction() (or EnforceWebServiceRestrictions()) as its first line.
  • bpClose() is called after SDK or database use.
  • Verified by calling from a non-local machine and confirming the BP_WS_INVALID_IP SOAP fault.