LDAP Custom Authentication and Func Attribute Processing (Deployment Plan)¶
In the Deployment Plan, you can delegate LDAP authentication to a customer-maintained Func function, or keep the built-in LDAP authentication and have Func process only the user attributes after successful authentication. Both approaches reuse the existing LDAP login entry and the SSO account, workspace, and role mapping rules.
Before use, confirm that the Studio backend version supports LDAPClientSet.authMode, LDAPClientSet.external, and the response format of the native Func function API. Enabling secret encryption also requires Studio and Func core that support the brand-agnostic v2 protocol; updating only the downloaded scripts cannot make an older Studio support v2. If an existing deployment keeps authMode: builtin (or leaves it unset) and does not enable a non-empty server.hosts, it continues to use the original single-address flow; no Func or secret configuration is required for the upgrade.
Mode Selection¶
authMode |
Who verifies the password | What Func receives | What Func returns on success |
|---|---|---|---|
builtin (default) |
Studio built-in LDAP | Func is not called | Not applicable |
builtin_transform |
Studio built-in LDAP; Func is not called on failure | Username and the authenticated userinfo, without the password used for this login |
Top-level patch of user attributes |
external |
Customer Func | Username and password | Complete userinfo |
builtin_transform shares the existing LDAP connection, TLS, search, and mapping configuration, and can also be configured together with multiple domain controllers. It uses the new authentication execution flow; even if hosts is not configured, you should check SIMPLE user password verification, SAFE_SYNC, and the startup environment according to the new-flow requirements in the multiple domain controllers document.
In external mode, Studio no longer connects to the LDAP server in the Studio configuration; the customer Func is responsible for the actual authentication. Studio still uses LDAPClientSet.mapping to interpret the returned attributes; the function must not return Studio tokens, account UUIDs, or role UUIDs in place of user information.
Select between the two external modes as needed; each login calls only the function specified by external.url. When external points to authenticate, transform is not automatically called afterward. If you want the customer function to perform both authentication and attribute processing, it can process the complete userinfo after authentication succeeds and then return it. To keep Studio's built-in LDAP authentication and customize attributes only, use builtin_transform and point the URL to transform.
Preparing Func Scripts¶
The following examples contain two entry points: standard LDAP password verification and attribute processing. They can serve as a starting point for customer scripts:
- Download core.py: LDAP connection, protocol encoding/decoding, and attribute validation.
- Download main.py: Func function entry, including
authenticate(body="")andtransform(body=""). -
Download source and verification information. The two scripts share the same source as the Studio implementation; update them together when upgrading the protocol.
-
Create a script set in a customer-managed Func environment, for example
ldap_client_demo. - Create a sub-script with ID
coreand pastecore.py; then create a sub-script with IDmainand pastemain.py. Theimport __core as coreinmainrefers to thecoresub-script in the same script set; do not change it to download code from the internet. - Confirm that the Func Python environment has compatible versions of
ldap3,cryptography, andrequestsinstalled. Choose dependencies compatible with the Func Python version, and do not overwrite dependencies used by other scripts in the runtime environment; private CA files must be mounted into the Func execution container. - Configure the Func environment variables described below. Publish
corefirst, thenmain. Func invocation depends on published scripts; saving drafts alone is not sufficient. - Create a synchronous function API for the required functions: use
ldap_client_demo__main.authenticatefor external authentication andldap_client_demo__main.transformfor attribute processing. The function parameter name isbody, with type string. Copy the synchronous invocation URL; do not use the asynchronous task API.
For environment variables, script publishing, and function APIs, see Func Basic Concepts and Writing and Calling Functions.
Func Environment Variables¶
| Name | Description |
|---|---|
LDAP_CLIENT_DEMO_SECRET |
Optional; defaults to an empty string. When the request body needs to be encrypted, set the same non-empty value as Studio's external.secret. Use a securely generated random shared value and do not write it into scripts, URLs, or logs. |
LDAP_CLIENT_DEMO_CONFIG |
A JSON object or JSON string containing the LDAP configuration inside LDAPClientSet, without an additional LDAPClientSet layer. Directory configuration is required for external authentication; for attribute processing only, you should also configure a mapping consistent with Studio. |
Example LDAP_CLIENT_DEMO_CONFIG when using the standard LDAP authentication function:
{
"server": {
"hosts": ["ldaps://ldap-a.example.com:636", "ldaps://ldap-b.example.com:636"],
"connect_timeout": 6,
"use_ssl": true,
"get_info": "SCHEMA",
"tls": {"validate": "CERT_REQUIRED", "ca_certs_file": "/config/ldap/ca.pem"}
},
"connection": {"authentication": "SIMPLE", "client_strategy": "SAFE_SYNC", "receive_timeout": 5},
"startTls": false,
"authTimeoutSeconds": 20,
"baseDN": "dc=example,dc=com",
"bindDN": "cn=studio-reader,ou=service,dc=example,dc=com",
"bindPassword": "<service account password>",
"searchAttribute": "uid",
"personObjectClass": "inetOrgPerson",
"mapping": {"exterId": "uid", "username": "uid", "email": "mail", "name": "displayName"}
}
Replace the example with your actual directory configuration, keeping identity fields consistent with the original deployment. If the configuration contains bindPassword, save the entire JSON string as a Func environment variable of type password; the script parses this string, which prevents the service password from being echoed in the ordinary JSON variable list. LDAP_CLIENT_DEMO_SECRET should also use the password type. This configuration uses the same bounded failover rule as Studio's multiple domain controllers; after the user password is submitted, it does not automatically switch nodes and retry. For attribute processing only, you can configure just:
Both modes use _settings() in main.py, so attribute processing must also provide a valid JSON object. Do not accept service accounts, LDAP addresses, or secret from request parameters.
Function API Access Control¶
Both @DFF.API entry points in the example set require_api_auth=False, so you can create the function API without binding Func API authentication. Keep synchronous execution, the 20-second timeout, and cache_result=0; do not cache password authentication results.
Studio always sends the kwargs.body JSON request described below and does not attach custom authentication headers, so it can directly connect to the Func synchronous entry that is not bound to additional API authentication:
Expose the demo API only within the controlled internal network, restrict sources and request rates, and disable collection of authentication parameters and request bodies; use trusted HTTPS when transmitting real passwords. Disabling mandatory Func API authentication does not skip LDAP password verification inside the function, nor does it turn off Func global authentication. secret only controls optional encryption of the request body; it does not replace LDAP verification, TLS, or network access control.
If an existing function API is already bound to API authentication, updating the script will not automatically unbind it; the API administrator must handle it according to the actual access policy. If API authentication requiring an additional header is kept, it should be adapted by a controlled entry point deployed by the customer; this is not a required step for the example direct-connection mode. For optional configuration, see Func API Authentication.
There is no need to add headersFile, keyFile, or other Studio authentication fields, and do not use Func's MCP programming address or development Access-Token as the login business entry.
Configuring in Studio¶
Following the Application Configuration Guide, modify the Core configuration of Launcher forethought-core, and restart the relevant services.
External Authentication¶
LDAPClientSet:
enable: true
authMode: external
authTimeoutSeconds: 20
# Interprets attributes returned by Func; must be consistent with existing users and the Func configuration.
mapping:
exterId: uid
username: uid
email: mail
name: displayName
external:
# Full synchronous HTTP(S) function API URL for authenticate; must be accessible from the Studio service.
# Not an LDAP address, Func console page, or MCP programming address; redirects are not accepted.
url: "https://func.example.com/ldap/authenticate"
# Optional; if enabled, must match the value in the Func environment variable and is not sent with the request.
secret: ""
# Optional; path to the HTTPS private CA inside the Studio container; empty uses system trust.
caCertsFile: ""
In external mode, Studio does not use local LDAP parameters such as server, connection, and bindDN; you can keep the original configuration for rollback. The actual directory configuration is maintained by the Func environment variables.
Attribute Processing After Built-in Authentication¶
Keep the existing server, connection, startTls, directory search, and identity mapping configuration; change only the mode and the external entry to:
# The following is a configuration fragment to merge under LDAPClientSet; do not use it to overwrite the entire LDAP configuration.
LDAPClientSet:
authMode: builtin_transform
authTimeoutSeconds: 20
external:
# Synchronous HTTP(S) API URL for the transform function.
url: "https://func.example.com/ldap/transform"
secret: ""
caCertsFile: ""
external.url is required in both external modes. The old builtin mode ignores this setting, so keeping the default empty URL does not affect existing deployments. The URL must not contain account passwords or fragments; use trusted HTTPS. HTTP does not provide transport-layer protection and should not be used to transmit real login passwords.
authTimeoutSeconds is the total time limit for authentication and attribute preparation in the new flow. It is an integer from 1 to 60 seconds and defaults to 20 seconds. It does not time each step separately, nor does it cover subsequent account transactions. The execution timeout of the Func example is 20 seconds. Configure it together with the total time limits of LDAP, gateway, and Studio to avoid unbounded waits. If Func fails, times out, or encounters a protocol error, the login fails and does not fall back to another authentication mode.
body Request and Response Protocol¶
Func entries are always defined as def authenticate(body="") or def transform(body=""). Studio sends JSON via POST, with the outer structure fixed as:
The logical request object for external authentication is:
When neither side has secret configured, body is the JSON string obtained by serializing that object once. Example of the actual HTTP request:
When both sides configure the same non-empty secret, body is an encrypted string of the form v2:.... The downloaded core.py handles encryption and decryption, so you do not need to implement it manually. Requests are rejected if only one side is configured, if the two sides are inconsistent, or if ciphertext validation fails; there is no fallback to plaintext. Spaces are part of the secret value, so both sides must match exactly.
The protocol uses HKDF-SHA256 to derive a 32-byte key and AES-256-GCM for encryption. The HKDF salt is empty, the key derivation context is ldap-forward/v2, and the AAD is ldap-forward/v2/request. The transport format is v2: plus standard Base64, and the encoded content is, in order, a 12-byte random nonce, the ciphertext, and a 16-byte authentication tag. Only the body in the request is encrypted; the return value is still JSON. This protocol does not provide request expiration or replay deduplication, so HTTPS and entry access control are still required.
The AES-GCM authentication tag here is used to verify the integrity of the encrypted request body; it is not an additional digital signature. Studio does not add sign, signature, signature headers, or AK/SK; customers do not need to prepare signature key files. When neither side has secret configured, only the JSON string is passed through, without additional signatures or encrypted authentication tags.
Brand-agnostic protocol: Encryption uses only the secret agreed on by both sides and the brand-agnostic protocol parameters described above. It does not read product names, brand identifiers, or brand domain names. Page copy and help URLs are replaced by brand and do not participate in key derivation or ciphertext validation.
The logical request object for attribute processing is:
{
"username": "user1",
"userinfo": {"uid": ["user1"], "mail": ["user1@example.com"], "memberOf": ["cn=readers,dc=example,dc=com"]}
}
The password used for this login is not sent. The function simply returns a business object. Example of successful external authentication:
return {"status": "success", "userinfo": {
"uid": ["user1"],
"mail": ["user1@example.com"],
"displayName": ["Example User"]
}}
A wrong password or a disallowed login returns {"status": "denied"}; a directory, configuration, or processing exception returns {"status": "error"}. Do not include passwords, exception stack traces, or internal configuration. The native Func synchronous API returns the business object directly, so the function itself does not need to wrap it in ok or data. Studio is also compatible with the legacy entry that uses the following wrapper:
{"ok": true, "data": {"result": {"status": "success", "userinfo": {"uid": ["user1"], "mail": ["user1@example.com"]}}}}
Studio requires HTTP 200 and validates the native business object or the business result in the wrapper above. As long as the outer layer contains ok, the wrapper is strictly validated; if the outer layer fails or is malformed, Studio does not fall back to native success. Asynchronous task IDs, redirects, HTML pages, or arbitrary text are not valid authentication results. The decrypted request object, the user attribute object, and the Func business result are each limited to 256 KiB, and the HTTP response body is limited to 1 MiB. Do not return large batches of unrelated attributes.
Modifying Authentication or Attribute Processing Logic¶
Custom Authentication¶
By default, authenticate in main.py calls the same-source LDAP executor to verify the real username and password. If you integrate with another identity service, replace the actual verification step in the authentication branch of _run(): first call the customer's trusted identity service and confirm that the password or authentication credential is valid, then construct userinfo; return denied if authentication is rejected, and error if the service has an exception. Do not return success solely because the username exists.
The field names in userinfo are determined by mapping. For example, with mapping.exterId: uid, you must return a non-empty uid and at least a mappable username or email. Identity fields can be a string or a single-element string array; do not return multiple candidate identities. Group attributes required for other authorization should continue to use the existing structure.
When integrating multiple independent identity sources, the customer function determines the identity source and verifies the identity. You must ensure that exterId is globally stable and unique within the integration scope. The same name or email does not mean the same person, and existing LDAP/OIDC accounts are not automatically merged. Before migrating existing users, verify the identity source and existing bindings to avoid duplicate accounts or identity conflicts.
Attribute Processing¶
First create a synchronous HTTP API for ldap_client_demo__main.transform, configure authMode: builtin_transform in Studio, and point external.url to this API. It shares the original LDAP connection, search, TLS, and mapping. If request body encryption is enabled, continue to use the same secret as Func.
In the downloaded example, patch = {} does not modify any attributes by default. Customers write the actual processing rules in the transform_only branch of main.py and republish main after modification. For example:
# Example: the actual value should be computed by a trusted business rule or attribute service; do not accept arbitrary values from the client.
patch = {"displayName": ["Business display name"]}
core.merge_transform(original, patch, config["mapping"])
return {"status": "success", "userinfo": patch}
At the same time, configure name: displayName in the mapping of both Studio and Func, keeping the rest of the original mapping unchanged. An empty patch {} means the original attributes are retained. Patches are merged at the top-level fields, not recursively. Fields that are not returned are retained, while explicitly returning empty values or empty arrays may clear fields and change role mapping results.
Attribute processing must not change the identity value corresponding to mapping.exterId. Before modifying other attributes, also verify workspace and role mappings. When updating nested objects, return the complete content required for that field. Do not return credential fields such as passwords, tokens, private keys, or shared secrets.
Security Verification and Rollback¶
The old encryption protocol is for test environments only. When switching to v2, first pause the relevant test authentication and wait for in-flight requests and the Func queue to drain. Publish the new version of core first, then update Studio, and resume after verification. The protocol versions on both sides must match; old ciphertext is not automatically accepted, and there is no fallback to plaintext. JSON pass-through without secret configured is unaffected.
Before going live, verify with synthetic data and dedicated test accounts:
- When
secretis empty on both sides, Func receives the JSON string; when both sides configure the samesecret, Func receives ciphertext and authentication succeeds; when the two sides are inconsistent, authentication fails. - A correct password succeeds and a wrong password is rejected; when Func is stopped, times out, or returns a malformed response, authentication is rejected and does not switch back to the old authentication.
- In
builtin_transform, wrong-password requests do not reach Func; an empty patch retains identity and group attributes, while a patch that modifies the identity value is rejected. - Existing users still log in to their original accounts, and account profile, workspace, and roles match expectations; subsequent login and MFA still follow existing policies.
- Check the collection policies of Studio, Func, gateway, task records, and APM to ensure that authentication request parameters, passwords, and secrets do not enter logs or monitoring. The example entry points do not print these items, but they do not automatically disable platform parameter and task collection; encryption does not replace this check. Normal user attributes should also be returned within the minimum necessary scope.
When troubleshooting failures, first verify DNS, network, and CA from Studio to the gateway/Func, then verify the synchronous mode, access authentication, the secret on both sides, mapping, and return structure of the Func API. Inspect error categories and trace information. Do not troubleshoot by printing the full request body or configuration.
To roll back, restore authMode: builtin from before the upgrade, the original single-address server.host, and the original LDAP configuration; clear or delete server.hosts; and restart the relevant services. There is no need to delete the Func scripts. Rollback does not automatically restore processed attributes or merge accounts that have already been created. You must separately verify existing accounts and their authorization status.