Set up service user multi-factor authentication
11 minute read
A client authentication certificate enables multi-factor authentication (mTLS) for DigiCert® ONE APIs. This certificate works alongside or replaces your API key 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.
Note
DigiCert® provides different authentication methods depending on the sensitivity of the operation. Make sure you understand when to use each method when building integrations.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® Document Trust Manager (required)
- High-security operations in DigiCert® Account Manager (optional but recommended)
Use API key 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 keys
- Certificates can be quickly disabled without regenerating API keys
- Meets compliance requirements for code signing
Before you begin
openssl version to verify.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.
# 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.
# 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 nameC(Country): Two-letter country code (US, CA, GB, DE)- Additional fields (OU, L, ST) are optional for client authentication certificates
Tip
Verify your CSR usingopenssl 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:
csrCertificate signing request contentnameFriendly name for the client authentication certificateexp(optional) Service user expiration date in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)
Note
These examples save the API response toresponse.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.jq -n \
--arg csr "$(cat client_auth.csr)" \
--arg name "API Client Auth Certificate" \
--arg exp "2026-10-20T23:59:59Z" \
'{csr: $csr, name: $name, expiration_date: $exp}' | \
curl -X POST https://demo.one.digicert.com/account/api/v1/client-auth-certificate \
-H "x-api-key: SERVICE_USER_TOKEN" \
-H "Content-Type: application/json" \
-d @- | tee response.json | jq '.'
**What this does**:
- `jq` reads the CSR file and formats it as JSON
- `|` Pipes the JSON to curl for the API request
- `tee response.json` saves the response to a file
- `jq '.'` displays the formatted responseimport requests
# Configuration
BASE_URL = "https://demo.one.digicert.com"
USER_API_TOKEN = "SERVICE_USER_TOKEN"
CSR_FILE = "client_auth.csr"
CERT_NAME = "API Client Auth Certificate"
EXPIRATION_DATE = "2026-10-20T23:59:59Z"
# Read CSR from file
with open(CSR_FILE, "r") as f:
csr_content = f.read()
# Create client authentication certificate
payload = {
"csr": csr_content,
"name": CERT_NAME,
"expiration_date": EXPIRATION_DATE
}
response = requests.post(
f"{BASE_URL}/account/api/v1/client-auth-certificate",
headers={
"x-api-key": SERVICE_USER_TOKEN,
"Content-Type": "application/json"
},
json=payload
)
print(f"Status Code: {response.status_code}")
print(response.json())import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class Example {
public static void main(String[] args) throws Exception {
// Configuration
String baseUrl = "https://demo.one.digicert.com";
String userApiToken = "SERVICE_USER_TOKEN";
String csrFile = "client_auth.csr";
String certName = "API Client Auth Certificate";
String expirationDate = "2026-10-20T23:59:59Z";
// Read CSR from file
String csrContent = Files.readString(Paths.get(csrFile));
// Build JSON payload
ObjectMapper mapper = new ObjectMapper();
ObjectNode payload = mapper.createObjectNode();
payload.put("csr", csrContent);
payload.put("name", certName);
payload.put("expiration_date", expirationDate);
// Create client authentication certificate
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/account/api/v1/client-auth-certificate"))
.header("x-api-key", userApiToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(payload)))
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("Status Code: " + response.statusCode());
System.out.println(response.body());
}
}using System;
using System.IO;
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 userApiToken = "SERVICE_USER_TOKEN";
string csrFile = "client_auth.csr";
string certName = "API Client Auth Certificate";
string expirationDate = "2026-10-20T23:59:59Z";
// Read CSR from file
string csrContent = await File.ReadAllTextAsync(csrFile);
// Build JSON payload
var payload = new JsonObject
{
["csr"] = csrContent,
["name"] = certName,
["expiration_date"] = expirationDate
};
// Create client authentication certificate
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("x-api-key", userApiToken);
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):
{
"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.
jq -r '.x509_cert' response.json > client_auth.crt
Make sure the certificate was extracted:
openssl x509 -in client_auth.crt -text -noout | head -n 10
You should see certificate details including the subject, issuer, and validity dates.
Note
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.
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 usingopenssl 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.
# 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.comtoclientauth.demo.one.digicert.com - Use
--certflag with certificate file and password - Do not include the
x-api-keyheader
curl -X GET \
--cert client_auth.p12:YOUR_PASSWORD \
--cert-type P12 \
"https://clientauth.demo.one.digicert.com/account/api/v1/user/me"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())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<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("Status Code: " + response.statusCode());
System.out.println(response.body());
}
}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):
{
"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.
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.
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):
{
"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.
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):
{
"errors": [
{
"code": "AUTHORIZATION_ERROR",
"message": "No authentication data provided"
}
]
}
Re-enable the certificate
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:
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:
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):
# 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):
- Generate new certificate 30-60 days before expiration (follow Steps 1-4)
- Test new certificate in non-production environment
- Update application configuration to use new certificate
- Deploy updated configuration
- Verify new certificate working
- Disable old certificate (Step 6)
- Monitor for any failed authentications
- Delete old certificate files after 7-day grace period
Common errors and solutions
{
"errors": [
{
"code": "duplicate_error",
"message": "Cannot have duplicate names for client authentication certificate"
}
]
}
Choose a different `common_name` for the client certificate.{
"errors": [
{
"code": "no_access",
"message": "Not enough permissions"
}
]
}{
"errors": [
{
"code": "invalid_input_field",
"message": "Wrong input or input format"
}
]
}