--- title: "Set up service user multi-factor authentication" description: "Configure mTLS client authentication certificates for secure access, certificate management, signing, and audit logs.\n" source_url: https://dev.digicert.com/account-manager-api/tutorials/set-up-service-user-multi-factor-authentication.html --- A client authentication certificate enables multi-factor authentication (mTLS) for DigiCert® ONE APIs. This certificate works alongside or replaces your API token to provide enhanced security for sensitive operations, such as code signing, certificate management, and audit log access. In this tutorial, you will: - Create a CSR and request a signed client authentication certificate. - Generate a PKCS#12 file with your certificate and private key. - Verify mTLS authentication works with DigiCert® ONE APIs. - Manage certificate access and learn about rotation and expiration. > **Info** > > DigiCert® provides different authentication methods depending on the sensitivity of the operation. Make sure you understand when to use each method when building integrations. #### When to use mTLS vs API token authentication **Use mTLS (client certificate) for:** - All DigiCert® Software Trust Manager signing operations (required) - Production certificate issuance in DigiCert® Device Trust Manager (required) - Document signing operations in DigiCert® Content Trust Manager (required) - High-security operations in DigiCert® Account Manager (optional but recommended) **Use API token authentication for:** - Read-only operations (listing resources, viewing details) - Development and testing workflows - Non-signing certificate management tasks **Why mTLS for sensitive operations:** - Provides cryptographic proof of identity (something you have) - More difficult to compromise than API tokens - Certificates can be quickly disabled without regenerating API tokens - Meets compliance requirements for code signing ## Before you begin - A DigiCert® ONE account. - Service user API token. - OpenSSL installed. Use `openssl version` to verify. - If using `curl`, jq installed. Use `jq --version` to verify. ## Endpoint overview | Path | Method | Description | | --- | --- | --- | | `/account/api/v1/client-auth-certificate` | POST | Request a client authentication certificate for the authenticated user | | `/account/api/v1/user/me` | GET | Get details about the authenticated user | | `/account/api/v1/client-auth-certificate/CERT_ID/enable` | PUT | Enable or disable a client authentication certificate | ## Step 1: Generate a certificate signing request (CSR) First you need to generate a private key and CSR using OpenSSL. ```bash # Create directory for secure storage mkdir -p ~/.digicert/certs cd ~/.digicert/certs # Generate private key and CSR openssl req -new -newkey rsa:2048 -nodes \ -keyout client_auth_private.key \ -out client_auth.csr \ -subj "/CN=Demo API Service User/O=Your Organization/C=US" ``` Immediately secure your `client_auth_private.key` file. ```bash # Restrict access to owner read-only chmod 400 client_auth_private.key ``` Two certificate files are created: - `client_auth_private.key`: Your private key. **Keep this secure!** You will safely store this in a PKCS#12 file in step 4. - `client_auth.csr`: Certificate signing request to send to DigiCert®. Subject fields explained: - `CN` (Common Name): Use a descriptive name identifying the service (for example, "Production API Service", "CI/CD Signing Service", "IoT Provisioning Service") - `O` (Organization): Your organization’s legal name - `C` (Country): Two-letter country code (US, CA, GB, DE) - Additional fields (OU, L, ST) are optional for client authentication certificates > **Tip** > > Verify your CSR using `openssl req -text -noout -in client_auth.csr`. You should see certificate request details, including subject information and a 2048-bit RSA public key. ## Step 2: Request a client authentication certificate Next you will submit your generated CSR to Account Manager `/account/api/v1/client-auth-certificate` endpoint and save the response for the next step. Include these fields in your request body: - `csr` Certificate signing request content - `name` Friendly name for the client authentication certificate - `exp` *(optional)* Service user expiration date in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) > **Info** > > These examples save the API response to `response.json` for instructional purposes. Typically you would extract the `x509_cert` field directly from the response object and use it programmatically without saving to a file. **curl** ```bash curl -X POST "https://demo.one.digicert.com/account/api/v1/client-auth-certificate" \ -H "x-api-key: SERVICE_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "csr": "CSR", "name": "Certificate Name", "expiration_date": "2024-04-30T00:00:00Z" }' | jq '.' ``` **python** ```python import requests # Configuration BASE_URL = "https://demo.one.digicert.com" SERVICE_API_TOKEN = "SERVICE_API_TOKEN" # Request details CSR = "CSR" NAME = "Certificate Name" EXPIRATION_DATE = "2024-04-30T00:00:00Z" payload = { "csr": CSR, "name": NAME, "expiration_date": EXPIRATION_DATE, } response = requests.post( f"{BASE_URL}/account/api/v1/client-auth-certificate", headers={ "x-api-key": SERVICE_API_TOKEN, "Content-Type": "application/json" }, json=payload ) print(f"Status Code: {response.status_code}") print(response.json()) ``` **Java** ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; public class ApiExample { public static void main(String[] args) throws Exception { // Configuration String baseUrl = "https://demo.one.digicert.com"; String serviceApiToken = "SERVICE_API_TOKEN"; // Request details String csr = "CSR"; String name = "Certificate Name"; String expirationDate = "2024-04-30T00:00:00Z"; // Build payload ObjectMapper mapper = new ObjectMapper(); ObjectNode payload = mapper.createObjectNode(); payload.put("csr", csr); payload.put("name", name); payload.put("expiration_date", expirationDate); // Send request HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/account/api/v1/client-auth-certificate")) .header("x-api-key", serviceApiToken) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(payload))) .build(); HttpResponse response = client.send( request, HttpResponse.BodyHandlers.ofString() ); System.out.println("Status Code: " + response.statusCode()); System.out.println(response.body()); } } ``` **C#** ```csharp using System; using System.Net.Http; using System.Text; using System.Text.Json.Nodes; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { // Configuration string baseUrl = "https://demo.one.digicert.com"; string serviceApiToken = "SERVICE_API_TOKEN"; // Request details string csr = "CSR"; string name = "Certificate Name"; string expirationDate = "2024-04-30T00:00:00Z"; // Build payload var payload = new JsonObject { ["csr"] = csr, ["name"] = name, ["expiration_date"] = expirationDate }; // Send request using var client = new HttpClient(); client.DefaultRequestHeaders.Add("x-api-key", serviceApiToken); var content = new StringContent( payload.ToJsonString(), Encoding.UTF8, "application/json" ); var response = await client.PostAsync($"{baseUrl}/account/api/v1/client-auth-certificate", content); string responseBody = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Status Code: {(int)response.StatusCode}"); Console.WriteLine(responseBody); } } ``` **Successful response** (HTTP 201): ```json { "id": "e281fe3b-fb97-4183-a4fa-205913a7aee4", "user_id": "8b210e38-b8a9-41df-a96e-1ffe4512f6d1", "name": "API Client Auth Certificate", "common_name": "Service User Client Auth", "x509_cert": "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIQZA...\n-----END CERTIFICATE-----\n", "ca_cert": "-----BEGIN CERTIFICATE-----\nMIIEFjCCAv6gAwIBAgIQDx...\n-----END CERTIFICATE-----\n", "end_date": "2026-10-20T23:59:59Z", "start_date": "2025-10-20T00:00:00Z", "enabled": true, "serial_number": "6403F2A8B1234567890ABCDEF", "public_key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." } ``` ## Step 3: Extract the certificate from response.json With the response saved to `response.json`, you can extract your signed certificate in the `x509_cert` field. ```bash jq -r '.x509_cert' response.json > client_auth.crt ``` Make sure the certificate was extracted: ```bash openssl x509 -in client_auth.crt -text -noout | head -n 10 ``` You should see certificate details including the subject, issuer, and validity dates. > **Info** > > You now have the certificate (`client_auth.crt)` and private key (`client_auth_private.key`) as separate files. These can be used directly for authentication, or you can package them into a PKCS#12 (`.p12`) file in the next steps for easier distribution and use. ## Step 4: Create PKCS#12 file Combine your private key and the signed certificate into a password-protected PKCS#12 file. ```bash openssl pkcs12 -export \ -out client_auth.p12 \ -inkey client_auth_private.key \ -in client_auth.crt \ -name "DigiCert Client Auth Certificate" ``` At the password prompt, **enter a strong password and remember it.** You will need it for all mTLS API calls. > **Tip** > > Verify the PKCS#12 file using `openssl pkcs12 -in client_auth.p12 -noout -info`. Enter your password when prompted and confirm certificate details. ## Delete sensitive files The plaintext private key should not remain on your filesystem. The PKCS#12 file contains both the certificate and private key in a password-protected format. ```bash # Delete plaintext private key rm client_auth_private.key ``` ## Step 5: Test mTLS authentication Verify the certificate works by making an API call using mTLS. To do this add `clientauth` to the base URL and use `--cert` flag to provide the certificate file (`client_auth.p12`) and password (`YOUR_PASSWORD`). > **Important** > > **Important differences for mTLS:** > > - Base URL changes from `demo.one.digicert.com` to `clientauth.demo.one.digicert.com` > - Use `--cert` flag with certificate file and password > - **Do not include** the `x-api-key` header **curl** ```bash curl -X GET \ --cert client_auth.p12:YOUR_PASSWORD \ --cert-type P12 \ "https://clientauth.demo.one.digicert.com/account/api/v1/user/me" ``` **python** ```python import requests from cryptography.hazmat.primitives.serialization import pkcs12, Encoding, PrivateFormat, NoEncryption # Configuration BASE_URL = "https://clientauth.demo.one.digicert.com" CERT_FILE = "client_auth.p12" CERT_PASSWORD = "YOUR_PASSWORD" # Load PKCS12 certificate with open(CERT_FILE, "rb") as f: p12_data = f.read() # Extract certificate and private key private_key, certificate, additional_certs = pkcs12.load_key_and_certificates( p12_data, CERT_PASSWORD.encode() ) # Convert to PEM format for requests cert_pem = certificate.public_bytes(Encoding.PEM) key_pem = private_key.private_bytes( Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption() ) # Write temporary PEM files with open("temp_cert.pem", "wb") as f: f.write(cert_pem) with open("temp_key.pem", "wb") as f: f.write(key_pem) # Get user details using client certificate authentication response = requests.get( f"{BASE_URL}/account/api/v1/user/me", cert=("temp_cert.pem", "temp_key.pem") ) print(f"Status Code: {response.status_code}") print(response.json()) ``` **Java** ```java import java.io.FileInputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.security.KeyStore; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; public class Example { public static void main(String[] args) throws Exception { // Configuration String baseUrl = "https://clientauth.demo.one.digicert.com"; String certFile = "client_auth.p12"; String certPassword = "YOUR_PASSWORD"; // Load PKCS12 certificate KeyStore keyStore = KeyStore.getInstance("PKCS12"); try (FileInputStream fis = new FileInputStream(certFile)) { keyStore.load(fis, certPassword.toCharArray()); } // Configure SSL context with client certificate KeyManagerFactory kmf = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm() ); kmf.init(keyStore, certPassword.toCharArray()); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), null, null); // Get user details using client certificate authentication HttpClient client = HttpClient.newBuilder() .sslContext(sslContext) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/account/api/v1/user/me")) .GET() .build(); HttpResponse response = client.send( request, HttpResponse.BodyHandlers.ofString() ); System.out.println("Status Code: " + response.statusCode()); System.out.println(response.body()); } } ``` **C#** ```csharp using System; using System.Net.Http; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { // Configuration string baseUrl = "https://clientauth.demo.one.digicert.com"; string certFile = "client_auth.p12"; string certPassword = "YOUR_PASSWORD"; // Load PKCS12 certificate var certificate = new X509Certificate2(certFile, certPassword); // Create HttpClientHandler with client certificate var handler = new HttpClientHandler(); handler.ClientCertificates.Add(certificate); // Get user details using client certificate authentication using var client = new HttpClient(handler); var response = await client.GetAsync( $"{baseUrl}/account/api/v1/user/me" ); string responseBody = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Status Code: {(int)response.StatusCode}"); Console.WriteLine(responseBody); } } ``` **Expected response** (HTTP 200): ```json { "id": "783a6a45-b74b-4635-bb4b-69855910ccd3", "email": "jane@example.com", "status": "ACTIVE", "access_scope": "account", "primary_account_id": "130deba5-3ebb-43b2-8183-1a9940d460f5", "created_at": "2021-06-23T08:55:19Z", "created_by": "8f7f8b1f-63c1-4580-af7d-ea4cd890e8f7", "user_type": "service", "friendly_name": "Example service user", "description": "", "locale": "en_US", "applications": [ { "id": "2d3a72fd-ad44-4a9b-952a-2bcadb14a741", "name": "Trust Lifecycle", "permissions": [ "VIEW_EM_SEAT", "VIEW_EM_AUDIT_LOG", "VIEW_EM_CERTIFICATE", "VIEW_EM_PROFILE" ] }, { "id": "1a05282a-ec70-4da9-b921-933c070fcf80", "name": "IoT Trust", "permissions": [ "VIEW_IOT_CERTIFICATE", "VIEW_IOT_ENROLLMENT_PROFILE" ] }, { "id": "97b97f1b-8d1d-4203-a62c-0a209a1bea0a", "name": "Document Trust", "permissions": [ "MANAGE_DSM_VIEW_CERTIFICATE_PROFILES", "MANAGE_DSM_ADD_VALIDATIONS", "MANAGE_DSM_VIEW_CERTIFICATE_TEMPLATES", "MANAGE_DSM_VIEW_VALIDATIONS" ] }, { "id": "78c0355a-e1ca-4978-b60c-e9d66b9e1f30", "name": "Account Manager", "permissions": [ "MANAGE_AM_ACCOUNT", "VIEW_AM_AUDIT_LOG" ] }, { "id": "fd2b688d-43bd-4b4a-9fd6-f883ad9e813d", "name": "CA Manager", "permissions": [ "VIEW_CM_LICENSE" ] }, { "id": "7660bdb3-66e7-46e6-928f-dcae4d64ee91", "name": "Software Trust", "permissions": [ "MANAGE_SM_CERTIFICATE_PROFILE", "VIEW_SM_CERTIFICATE", "SIGN_SM_HASH" ] } ], "accounts": [ { "id": "4b13f12d-c8b2-4c49-a21f-9b399364f2ce", "name": "Example account", "active": true, "service_period": { "from": "2021-05-06", "to": "2030-05-06" }, "friendly_identifier": "1234567", "locale": "en_US" } ] } ``` ## Step 6: Manage certificate access (enable/disable) Client authentication certificates can be temporarily disabled to revoke API access without deleting the certificate. To do this you need the certificate ID, which you can get from the `id` field of the `response.json` file from step 2. ```bash CERT_ID=$(jq -r '.id' response.json) echo "Certificate ID: $CERT_ID" ``` ## Disable the certificate Add the certificate ID to the endpoint path `/account/api/v1/client-auth-certificate/CERT_ID/enable` and provide the required body content. Make sure to use API token authentication (`x-api-key` header) and remove the `clientauth.` from the base URL. ```bash curl -X PUT "https://demo.one.digicert.com/account/api/v1/client-auth-certificate/${CERT_ID}/enable" \ -H "x-api-key: SERVICE_USER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"enabled": false}' ``` **Response** (HTTP 200): ```json { "id": "CERTIFICATE_ID", "user_id": "USER_ID", "name": "Certificate name", "common_name": "COMMON_NAME", "end_date": "2023-04-30T23:59:59Z", "start_date": "2021-06-10T00:00:00Z", "enabled": false, "serial_number": "AUTH_CERT_SERIAL", "public_key": "PUBLIC_KEY" } ``` ## Verify access is revoked Try making an mTLS API call with the disabled certificate. ```bash curl -X GET \ --cert client_auth.p12:YOUR_PASSWORD \ --cert-type P12 \ "https://clientauth.one.digicert.com/account/api/v1/user/me" ``` **Response** (HTTP 403): ```json { "errors": [ { "code": "AUTHORIZATION_ERROR", "message": "No authentication data provided" } ] } ``` ## Re-enable the certificate ```bash curl -X PUT "https://demo.one.digicert.com/account/api/v1/client-auth-certificate/${CERT_ID}/enable" \ -H "x-api-key: SERVICE_USER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"enabled": true}' ``` The returned `enable` field shows `true`. ## Client certificate expiration and rotation Client authentication certificates expire on the date specified during creation (maximum 397 days from creation date). ## Check certificate expiration **Using OpenSSL**: ```bash openssl pkcs12 -in ~/.digicert/certs/client_auth.p12 -nokeys -passin pass:YOUR_PASSWORD | \ openssl x509 -noout -enddate ``` Expected output: ``` notAfter=Oct 20 23:59:59 2026 GMT ``` **Using the API**: ```bash curl -X GET \ "https://demo.one.digicert.com/account/api/v1/client-auth-certificate" \ -H "x-api-key: SERVICE_API_KEY" | jq '.[] | {name, end_date, enabled}' ``` ## What happens at expiration When a certificate expires: - mTLS authentication immediately fails with HTTP 403 - All integrations using the certificate stop working - The certificate cannot be renewed or extended - The certificate must be regenerated ## Best practices for certificate rotation **Set up monitoring** (90 days before expiration): ```bash # Check expiration and alert if < 90 days remain EXPIRY=$(openssl pkcs12 -in client_auth.p12 -nokeys -passin pass:YOUR_PASSWORD | \ openssl x509 -noout -enddate | cut -d= -f2) EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s) NOW_EPOCH=$(date +%s) DAYS_REMAINING=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 )) if [ $DAYS_REMAINING -lt 90 ]; then echo "WARNING: Certificate expires in $DAYS_REMAINING days. Rotate soon." fi ``` **Rotation workflow** (zero-downtime): 1. Generate new certificate 30-60 days before expiration (follow Steps 1-4) 2. Test new certificate in non-production environment 3. Update application configuration to use new certificate 4. Deploy updated configuration 5. Verify new certificate working 6. Disable old certificate (Step 6) 7. Monitor for any failed authentications 8. Delete old certificate files after 7-day grace period ## Common errors and solutions **duplicate_error** ```json { "errors": [ { "code": "duplicate_error", "message": "Cannot have duplicate names for client authentication certificate" } ] } Choose a different `common_name` for the client certificate. ``` **no_access** ```json { "errors": [ { "code": "no_access", "message": "Not enough permissions" } ] } ``` **invalid_input_field** ```json { "errors": [ { "code": "invalid_input_field", "message": "Wrong input or input format" } ] } ```