> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/administration-guide/security/certificate-authentication.md).

# Certificate authentication

Configure client-certificate sign-in end to end, with the single-capture-group regex contract, a CA validation script that actually validates, and the negative tests that prove it.

## What this is, and when you need it

Certificate authentication uses an X.509 client certificate as identity evidence: OpenIAM extracts a principal from the certificate's subject DN and looks it up as a login. It suits managed devices, smart cards and regulated populations that already have an operated certificate lifecycle.

`CERT_AUTH` can stand alone: the shipped `DEF_CERT_AUTH_RULE` is `CERT_AUTH` → `AUTHORIZATION_ACCEPT`, so a rule carrying it and nothing else is a **passwordless** sign-in. It can equally be chained after a password as a second factor. See [Passwordless sign-in](/administration-guide/security/authentication-model.md#passwordless-sign-in).

There are **two** distinct flows in this product and they behave differently. Get the right one before you configure anything:

| Flow                            | How the certificate arrives                                                                             | Entry point                                                                                           |
| ------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **IdP form flow** — `CERT_AUTH` | The user types a login **and** uploads a PKCS#12 file **and** its password                              | The `/idp/auth-cert` screen                                                                           |
| **Access Gateway flow**         | The proxy terminates the TLS handshake and hands the validated certificate to OpenIAM behind the scenes | Nothing the user or you visits directly — the gateway does it on every request for the protected path |

Neither is a TLS client-certificate handshake terminated by OpenIAM. If you need mutual TLS (**mTLS**) at the edge, the proxy terminates it and the Access Gateway flow forwards the validated certificate — see [Configure the Access Gateway](/administration-guide/gateway/access-gateway.md). This page configures the IdP form flow.

## At a glance

|                          |                                                                                                                                                                                                                                                     |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | A test identity signing in with a PKCS#12 certificate on one URI pattern, a `LOGIN_PKS12` audit event, and seven negative cases that all fail                                                                                                       |
| **Where**                | **Access Control → Authentication Providers** (`/webconsole/authenticationProviders`), then the URI pattern at `/webconsole/editProviderPattern`                                                                                                    |
| **You need first**       | An `OPENIAM_POLICY` provider you can edit, your issuing CA certificate as a file, a client certificate for `<TEST_USER>` exported as PKCS#12 with a known password, and a tested break-glass administrator on a route that does not use `CERT_AUTH` |
| **Takes**                | Half a day. Most of it is the CA validation script and the negative tests, not the form                                                                                                                                                             |
| **Reversible**           | Yes — detach the rule, then clear **Supports Certificate Authentication**, which hides the other five fields without erasing them                                                                                                                   |

{% hint style="warning" %}
**Nothing ships with certificate authentication turned on, and nothing ships that validates a CA.** `AUTH_PROVIDER.SUPPORTS_CERT_AUTH` arrived in 4.0.0.0 as `CHAR(1) NOT NULL DEFAULT 'N'`, and no release since has set it to `'Y'` on a provider row. The *type* flag is set on exactly one type, `OPENIAM_POLICY`, so the checkbox is only ever rendered on an authentication-policy provider, and the shipped `OPENIAM_DEFAULT_AUTH` row has it off. What *does* ship is the plumbing: the `CERT_AUTH` level, the `CERT_AUTH` grouping, `DEF_CERT_AUTH_RULE`, and `CERT_AUTH` steps on `DEF_ANY_AUTH_RULE`, `STEPUP_AUTH_RULE` and `UNLOCK_ACCOUNT_RULE`. Chain validation is something **you** implement — see [What actually validates the certificate](#what-actually-validates-the-certificate).
{% endhint %}

Before you start, do [Keep a working way in](/administration-guide/security/authentication-providers-and-mfa.md#keep-a-working-way-in), and decide which subject DN component identifies the person. The principal you extract must exist on the **default managed system**, which is not the provider's managed system — step 4 confirms it.

## Configure certificate authentication

{% stepper %}
{% step %}

### Create a new `OPENIAM_POLICY` provider

Only that type exposes the fields; the read-only confirmation is `SELECT PROVIDER_TYPE, SUPPORTS_CERT_AUTH FROM AUTH_PROVIDER_TYPE WHERE SUPPORTS_CERT_AUTH = 'Y'`, which returns one row. Create a **new** provider rather than repointing `OPENIAM_DEFAULT_AUTH`, and leave `Active` off while you build it.
{% endstep %}

{% step %}

### Tick Supports Certificate Authentication, then set the regex

The five remaining fields only render once the checkbox is ticked, and the Webconsole refuses to save a script, a regex or a CRL path while it is clear — so tick it, save, then fill the rest in.

Set **Subject Principal Regular Expression** to `CN=([^,]+)` and leave **Certificate Groovy Script** empty. The regex takes priority over the script whenever both are set, so an empty script field is not a gap. Read [The regex contract](#the-regex-contract-one-capture-group-default-managed-system) before you write a different expression: anything other than **exactly one capture group** is refused at sign-in.
{% endstep %}

{% step %}

### Upload the CA certificate and select a real CA validation script

Upload your issuing CA to **CA Certificate**, then reopen the provider and use **View CA Certificate** to confirm subject, issuer, fingerprint and validity dates.

Select the `StrictCACertCheck` script from [What actually validates the certificate](#what-actually-validates-the-certificate) — **not** the shipped example, which contains an inverted condition and swallows a genuine revocation failure. Leave **CRL List Path** empty so the certificate's own distribution points are used.
{% endstep %}

{% step %}

### Confirm the principal exists on the default managed system

{% code overflow="wrap" %}

```sql
SELECT LOGIN, MANAGED_SYS_ID FROM LOGIN WHERE LOGIN = '<TEST_USER>';
```

{% endcode %}

`MANAGED_SYS_ID` must be the default managed system — the shipped default is `0`, the OpenIAM internal repository. If it is anything else, the built-in converter will not find the principal, and no field on the provider changes that; you need a converter script instead.
{% endstep %}

{% step %}

### Attach `DEF_CERT_AUTH_RULE` to one narrow URI pattern

The rule ships: `DEF_CERT_AUTH_RULE`, *"Def. Certificate Auth Rule"*, with step `DEF_CERT_AUTH_RULE_1` (`CERT_AUTH`) edging to `DEF_CERT_AUTH_RULE_2` (`AUTHORIZATION_ACCEPT`). Open it on **Policy → Authentication Rule** to confirm both steps and the edge, then attach it to one test pattern at `/webconsole/editProviderPattern`.

That pattern now ignores the content provider's password rule entirely. That is the point for a certificate-only path, and a lockout if you do it to `DEFAULT_CP`. If you want *either* a certificate *or* a password, `DEF_ANY_AUTH_RULE` already contains a `CERT_AUTH` root step alongside `PASSWORD_AUTH`.
{% endstep %}

{% step %}

### Activate, wait a sweep, then run the positive test

`org.openiam.auth.provider.sweep.time` is `60000` ms. Then: `<TEST_USER>`, their PKCS#12 file, the correct **file** password. Expect a session, a `LOGIN_PKS12` audit event, and `CERT_AUTH` recorded as the approved authentication level.

The password field on that form is the PKCS#12 file's password, not the user's OpenIAM password. The user's OpenIAM password is not involved in this flow at all, so an expired or must-change OpenIAM password does not block it.
{% endstep %}

{% step %}

### Run the seven negative tests

Every one must fail:

| Case                                                                      | Expected                                                                                     |
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Wrong PKCS#12 password                                                    | `CERT_CLIENT_INVALID`; *"Cannot read file as pkcs12"* in the ESB log                         |
| A valid certificate but a **different** login typed                       | `INVALID_LOGIN_CERTIFICATE`, `401` — the login/certificate equality check                    |
| A certificate whose subject has no `CN=`                                  | `INVALID_LOGIN`, *"Subject '%s' did not match regex"*                                        |
| A certificate whose `CN` is not a principal on the default managed system | `INVALID_LOGIN`, *"did not resolve to any login for principal"*                              |
| An expired client certificate                                             | `CERT_CLIENT_INVALID` — **only if your CA validation script checks validity**                |
| A certificate from an untrusted CA                                        | `CERT_INVALID_VERIFY_WITH_CA` — same caveat                                                  |
| A revoked certificate                                                     | `CERT_WAS_REVOKED` — only if the certificate carries a reachable `cRLDistributionPoints` URI |

The last three are the ones worth doing properly. **If any of them succeeds, your CA validation script is inheriting the default `resolve()` and its unconditional true**, and you have a screen full of security settings that do nothing.

Certificate failures increment the same counter a wrong password does, against `FAILED_AUTH_COUNT` on authentication policy `4001`, so run these on a disposable identity and watch `LOGIN.AUTH_FAIL_COUNT`.
{% endstep %}

{% step %}

### Decide what happens when the CRL is unreachable

Every revocation check is a live download with no cache, so the distribution point's availability is now part of your sign-in path. Decide before you go live which way you want that to fail: your script can treat a download failure as "reject" or as "accept", and the shipped example treats it as accept. Record the decision, and make sure the distribution point is monitored as a sign-in dependency rather than as a PKI detail.

Testing it means denying the ESB egress to the distribution point, which is a network change on the server rather than a Webconsole one — see the [Installation Guide](/installation/installation.md). What you will see from this side is a certificate sign-in that either fails with `CERT_CRL_DOWNLOAD_EXCEPTION` and *"Can not download CRL from certificate distribution point: "* in the ESB log, or succeeds silently. Whichever you get is your answer.
{% endstep %}
{% endstepper %}

## Verification

| What to check                                | How                                                                                                                                                                                                                                                                                    |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The type exposes the fields                  | `SELECT SUPPORTS_CERT_AUTH FROM AUTH_PROVIDER_TYPE WHERE PROVIDER_TYPE='OPENIAM_POLICY'` returns `Y`                                                                                                                                                                                   |
| The provider flag is on and the fields saved | `SELECT SUPPORTS_CERT_AUTH, CERT_AUTH_REGEX, CERT_AUTH_REGEX_SCRIPT_ID, CERT_AUTH_CA_VALIDATE_SCRIPT_ID, CERT_DER_PATH, LENGTH(CA_CERT) FROM AUTH_PROVIDER WHERE PROVIDER_ID='<PROVIDER_ID>'` — read-only                                                                              |
| The regex has exactly one group              | Count unescaped `(` that are not `(?:`. Anything but one is refused at sign-in with *"Regular expression must contain a single group"*                                                                                                                                                 |
| The CA certificate parses                    | **View CA Certificate** renders it. A parse failure is `CERT_CA_INVALID` at save or at first use                                                                                                                                                                                       |
| CA validation is real                        | Present a certificate from an untrusted CA. Success means your script inherits the default `resolve()`                                                                                                                                                                                 |
| Revocation is real                           | Present a revoked certificate whose `cRLDistributionPoints` URI the ESB can reach. Expect `CERT_WAS_REVOKED`                                                                                                                                                                           |
| The rule is attached                         | `SELECT * FROM URI_RULE_XREF WHERE RULE_ID='DEF_CERT_AUTH_RULE'`                                                                                                                                                                                                                       |
| Audit records exist                          | `LOGIN_PKS12` for a certificate sign-in, plus `LOGIN` / `DO_LOGIN`, and `INCREMENT_FAIL_AUTH_COUNT` and `USER_ACCOUNT_LOCKED` for the failures. `LOGIN_PKS12` is the only place the session's origin is recorded — the cookie the browser holds looks the same as a password session's |
| The lock counter behaves                     | `SELECT AUTH_FAIL_COUNT, LOCK_MARKER FROM LOGIN WHERE LOGIN='<TEST_USER>'` after a failed attempt                                                                                                                                                                                      |

## When it goes wrong

| Symptom                                                                         | Cause                                                                                                                                                                                     | Fix                                                                                                                                        |
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| No **Supports Certificate Authentication** checkbox                             | The provider's *type* has `SUPPORTS_CERT_AUTH='N'` — only `OPENIAM_POLICY` ships `Y`                                                                                                      | Put certificate auth on an `OPENIAM_POLICY` provider                                                                                       |
| The other five fields are absent                                                | The checkbox is not ticked, and the rest of the section is hidden until it is                                                                                                             | Tick it and save                                                                                                                           |
| Save refuses with the regex or CRL path filled in                               | OpenIAM rejects those values while **Supports Certificate Authentication** is clear                                                                                                       | Tick the checkbox first, save, then fill the rest in                                                                                       |
| `CERT_CONFIG_INVALID` — *"Not define regex and regexScript"*                    | Both `CERT_AUTH_REGEX` and `CERT_AUTH_REGEX_SCRIPT_ID` are blank                                                                                                                          | Set one. The regex wins when both are set                                                                                                  |
| `CERT_CONFIG_INVALID` — *"Auth provider not support Certificate authorization"* | `AUTH_PROVIDER.SUPPORTS_CERT_AUTH='N'` on the provider the URL resolved to                                                                                                                | You may have ticked it on a different provider than the content provider points at                                                         |
| The selected Groovy script is ignored                                           | `CERT_AUTH_REGEX` is non-blank, so the built-in converter is used instead                                                                                                                 | Clear the regex to use the script                                                                                                          |
| *"Regular expression must contain a single group"*                              | Zero or two-plus capture groups                                                                                                                                                           | Exactly one `(...)`; make the rest `(?:...)`                                                                                               |
| *"did not resolve to any login for principal"*                                  | The extracted value is not a principal on the **default** managed system                                                                                                                  | Correct the regex, or write a converter script that looks up the right managed system                                                      |
| The regex matches something you did not intend                                  | The search is unanchored and case-insensitivity is forced                                                                                                                                 | Anchor with `(?:^\|,)` and bound with `[^,]+`                                                                                              |
| `INVALID_LOGIN_CERTIFICATE`, `401`, certificate looks fine                      | The extracted principal does not equal the typed login                                                                                                                                    | Type the principal the certificate resolves to                                                                                             |
| `CERT_CLIENT_INVALID`, *"Cannot read file as pkcs12"*                           | Wrong file password, or not a PKCS#12                                                                                                                                                     | Re-export the `.p12` and confirm the password                                                                                              |
| Revoked certificates still authenticate                                         | Either no CA validation script is set, so validation is skipped entirely, or the script inherits the default `resolve()`, or the certificate carries no `cRLDistributionPoints` extension | Override `resolve()`; add the extension to your certificate profile                                                                        |
| Setting **CRL List Path** changes nothing                                       | The value reaches your script as `crlPath` and nothing shipped reads it                                                                                                                   | Read `crlPath` in your own `resolve()` override                                                                                            |
| Sign-in stalls under load                                                       | Every revocation check downloads the CRL; there is no cache                                                                                                                               | Cache in your script, or host the CRL closer to the ESB — a placement question for the [Installation Guide](/installation/installation.md) |
| Account locks after a few certificate attempts                                  | Certificate failures increment `AUTH_FAIL_COUNT` against policy `4001` `FAILED_AUTH_COUNT`                                                                                                | Expected. Test on a disposable identity                                                                                                    |
| The certificate page 401s before anything is submitted                          | `/idp/auth-cert` is guarded by an unlock-validity check, not open to a typed URL                                                                                                          | Reach it through the rule, not by typing the URL                                                                                           |
| *"Mandatory extra data don't present"* alongside a working certificate sign-in  | The provider lookup on this path passes no adaptive extra data                                                                                                                            | Noise from the metadata lookup on this path, not an authorization decision                                                                 |

## Roll it back

Remove the `URI_RULE_XREF` row for `DEF_CERT_AUTH_RULE` first and prove password sign-in on that path, then clear `SUPPORTS_CERT_AUTH` on the provider — which hides the other five fields without erasing them. Terminate sessions established at `CERT_AUTH`. Clear `CA_CERT`, the two script IDs and `CERT_DER_PATH` before deleting a provider, and never keep a private key in a ticket or a document. Redacted fingerprints and audit events are the right evidence to retain.

## The fields behind the form

Six columns on `AUTH_PROVIDER` carry the whole configuration. These are the columns to query when the screen and the behaviour disagree:

| Column                            | Type                              | Webconsole label                         | Since                                   |
| --------------------------------- | --------------------------------- | ---------------------------------------- | --------------------------------------- |
| `SUPPORTS_CERT_AUTH`              | `CHAR(1)`, `Y`/`N`, default `'N'` | **Supports Certificate Authentication**  | 4.0.0.0                                 |
| `CERT_AUTH_REGEX`                 | `VARCHAR(100)`                    | **Subject Principal Regular Expression** | 4.0.0.0                                 |
| `CERT_AUTH_REGEX_SCRIPT_ID`       | `VARCHAR(300)`                    | **Certificate Groovy Script**            | 4.0.0.0, widened and renamed in 4.2.2.0 |
| `CERT_AUTH_CA_VALIDATE_SCRIPT_ID` | `VARCHAR(300)`                    | **CA Validation Groovy Script**          | 4.1.5.0, widened and renamed in 4.2.2.0 |
| `CERT_DER_PATH`                   | `VARCHAR(250)`                    | **CRL List Path**                        | 4.1.5.0                                 |
| `CA_CERT`                         | Binary large object               | **CA Certificate**                       | 4.1.5.0                                 |

Two labels mislead. The regex field is **Subject Principal Regular Expression**, not "Certificate Subject Regex" as older documentation calls it; its on-screen help reads *"This regular expression will be used to parse the certificate and obtain the user's identity."* And **CRL List Path**, whose help reads *"Path to the CRL list"*, is stored in a column the schema calls a DER path — one column, two disagreeing names — and nothing shipped reads the value.

## What actually validates the certificate

{% hint style="danger" %}
**A CA Validation Groovy Script that does not override `resolve()` performs no validation at all.** The inherited `resolve()` returns true unconditionally — no chain check, no validity-period check, no revocation check, and no use of **CRL List Path**. OpenIAM reads that true as "the certificate is good" and proceeds. Filling in **CRL List Path** and selecting a script that inherits the default `resolve()` produces a screen that looks like revocation checking and is not.

Certificate authentication with **no** CA validation script at all is *also* no chain validation: OpenIAM skips validation entirely when the field is empty. The only cryptographic check in the shipped default path is whatever your reverse proxy or TLS terminator performed before the certificate reached OpenIAM. Treat CA validation as something you must implement. The untrusted-CA negative test is what proves it either way.
{% endhint %}

**CRLs are not obtained from CRL List Path.** OpenIAM reads the CRL distribution points extension of the certificate itself, through a helper available inside a CA validation script as `certManager` without your declaring anything:

| Call                                               | What it does                                                                                                                                                              |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getCrlDistributionPoints(cert)`                   | Reads the X.509 `cRLDistributionPoints` extension, walks each full-name distribution point and collects every URI. Returns an **empty list** when the extension is absent |
| `downloadCRL(crlURL)`                              | Fetches by prefix: `http://`, `https://` and `ftp://` are downloaded, `file://` is read from disk. Anything else fails with `CERT_CRL_DOWNLOAD_EXCEPTION`                 |
| `getAllCRLs(cert)`                                 | Every distribution point on the certificate, downloaded                                                                                                                   |
| `isCertificateRevoked(crlHolder, ca, certificate)` | Verifies the CRL's signature against the CA public key, then looks the serial number up in the revoked list                                                               |
| `verifyCertificateNotRevoked(ca, certificate)`     | Collects CRLs from the **CA** certificate *and* the **client** certificate, and fails with `CERT_WAS_REVOKED` on a hit                                                    |

So the operational requirement is that your certificate profile carries a reachable `cRLDistributionPoints` URI and that the ESB can fetch it. A certificate without the extension yields an empty list and therefore **no revocation check and no error**. There is no CRL cache and no refresh interval — every check downloads. **CRL List Path** exists so that a script can override the distribution point with a fixed location; what you type there reaches your script as a variable named `crlPath`, and only when a CA validation script is selected.

### The shipped example script, and why not to copy it

`AM/ExampleDefaultCACertCheck_1.0.groovy` does the four right things — CA validity, client validity, verifying the client certificate against the CA's public key, then `verifyCertificateNotRevoked` — and then contains two problems in five lines. Its CRL fallback runs when `crlPath` is **blank**, which is inverted and cannot succeed, and its `catch` swallows a genuine `CERT_WAS_REVOKED` from the first call when `crlPath` is non-blank. A correct version keeps the class declaration and signature exactly and fixes both:

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```groovy
import org.apache.commons.lang3.StringUtils
import org.bouncycastle.cert.X509CRLHolder
import org.openiam.base.ws.ResponseCode
import org.openiam.esb.ws.groovy.DefaultCACertCheck
import org.openiam.exception.BasicDataServiceException

class StrictCACertCheck extends DefaultCACertCheck {

    StrictCACertCheck() { super() }

    @Override
    Boolean resolve() throws BasicDataServiceException {
        if (caCert == null || clientCert == null) {
            throw new BasicDataServiceException(ResponseCode.CERT_CONFIG_INVALID, 'CA or client certificate missing')
        }

        try {
            caCert.checkValidity()
        } catch (Exception ex) {
            throw new BasicDataServiceException(ResponseCode.CERT_CA_INVALID, ex.getMessage())
        }

        try {
            clientCert.checkValidity()
        } catch (Exception ex) {
            throw new BasicDataServiceException(ResponseCode.CERT_CLIENT_INVALID, ex.getMessage())
        }

        try {
            clientCert.verify(caCert.getPublicKey())
        } catch (Exception ex) {
            throw new BasicDataServiceException(ResponseCode.CERT_INVALID_VERIFY_WITH_CA, ex.getMessage())
        }

        // Prefer the certificate's own distribution points. Fall back to CRL List Path
        // only when the certificate carries no extension.
        if (StringUtils.isNotBlank(crlPath)) {
            List<X509CRLHolder> crlList = new ArrayList<X509CRLHolder>()
            crlList.add(certManager.downloadCRL(crlPath))
            certManager.verifyCertificateNotRevoked(crlList, caCert, clientCert)
        } else {
            // throws CERT_WAS_REVOKED on a hit; a certificate with no
            // cRLDistributionPoints extension yields an empty list and passes silently
            certManager.verifyCertificateNotRevoked(caCert, clientCert)
        }

        return true
    }
}
```

{% endcode %}

Four things are already in scope when your `resolve()` runs, and you declare none of them: `caCert` (the certificate you uploaded), `clientCert` (the certificate presented), `crlPath` (whatever you typed into **CRL List Path**), and `certManager`. Override `resolve()` and nothing else; returning true means "accept", and throwing is how you reject with a specific code the user and the ESB log will both show. Register the script per [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md).

## The regex contract: one capture group, default managed system

If **Subject Principal Regular Expression** is non-blank, OpenIAM uses its built-in converter and that regex; **only if the regex is blank** does it load the script named in **Certificate Groovy Script**. Both blank fails with `CERT_CONFIG_INVALID` and *"Not define regex and regexScript"*.

Six things about the built-in converter decide whether your regex works:

1. The input is the **whole subject DN as one string**. Not the SAN, not a nominated RDN. There is no SAN support in this path.
2. The match is case-insensitive whether you want it or not.
3. The search is **unanchored** — the pattern may match anywhere in the DN. A no-match fails with `INVALID_LOGIN` and *"Subject '%s' did not match regex '%s'"*.
4. **Anything other than exactly one capture group fails** with `INVALID_LOGIN` and *"Regular expression must contain a single group"*. Zero fails. Two fails. Use non-capturing `(?:...)` for everything else.
5. The principal is the contents of that one group, and a blank result fails with *"Regular expression '%s' did not resolve to any matches"*.
6. The principal is looked up on the **default managed system**, not on `AUTH_PROVIDER.MANAGED_SYS_ID`.

All six error messages appear in the ESB log with the subject and the regex interpolated, which is the fastest way to see what the certificate actually presented.

For a subject DN like `CN=jsmith, OU=Employees, O=Example Corp, C=US`, extracting the `CN` value:

{% code overflow="wrap" %}

```
CN=([^,]+)
```

{% endcode %}

For a subject whose `CN` is a display name and whose `UID` is the principal:

{% code overflow="wrap" %}

```
(?:^|,)\s*UID=([^,]+)
```

{% endcode %}

The `(?:...)` group is non-capturing, so the count of capture groups is still one — and this is exactly why you need it. `\s*` absorbs the space some issuers put after the comma. Anchor and bound your expression, and test a hostile subject: because the search is unanchored and case-insensitive, a naive `.*=(.*)` would happily capture an attacker-controlled RDN value. `CERT_AUTH_REGEX` is `VARCHAR(100)`, so the expression must be short.

{% hint style="warning" %}
**If your certificate subjects carry identifiers that only exist on a different managed system**, the built-in converter cannot find them and no field on the provider changes that. Clear the regex and register a **Certificate Groovy Script** that extends `DefaultCertToIdentityConverter` and overrides `resolve()` to look the principal up where it actually lives. The query in step 4 is how you confirm where the principal lives before you decide.
{% endhint %}

## What the IdP flow does, in order

Worth knowing when a failure has to be placed. OpenIAM takes the certificate path when the request carries **both** a certificate and a login, and then:

| Order | What happens                                                                                                                                                                                                                             | The failure it produces                                                                                                       |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 1     | The user reaches `/idp/auth-cert`. The path comes from `org.openiam.ui.auth.cert.url`, and it is reachable without a session but not open — OpenIAM checks the request is a valid unlock request for `CERT_AUTH` and for the login named | A typed URL returns `401`                                                                                                     |
| 2     | The uploaded file is opened as a PKCS#12 keystore with the password submitted alongside it, stripping any base64 data-URI prefix so a browser-encoded upload works                                                                       | `CERT_CLIENT_INVALID`, *"Cannot read file as pkcs12"* — the line that distinguishes a bad password from a bad certificate     |
| 3     | **Lockout is evaluated before the certificate is**, from the account's secondary status                                                                                                                                                  | `RESULT_LOGIN_LOCKED`, before certificate validation is reached at all                                                        |
| 4     | The URL resolves to a content provider, and the authentication provider is taken from it                                                                                                                                                 | `CERT_CONFIG_INVALID`, *"Can not find Auth provider for id : %s"*, or *"Auth provider not support Certificate authorization"* |
| 5     | The uploaded **CA Certificate** is parsed, then the CA validation script runs — if one is set                                                                                                                                            | `CERT_CA_INVALID` *"Can not parse CA Cert"*; a `resolve()` returning false gives `CERT_INVALID_VERIFY_WITH_CA`                |
| 6     | The converter returns a login, and the IdP compares it case-insensitively with what the user typed. This is what binds the certificate to a claimed identity rather than simply asserting one                                            | `INVALID_LOGIN_CERTIFICATE` and a `401`                                                                                       |
| 7     | A session is created with `CERT_AUTH` recorded as the satisfied grouping, which is what completes a `CERT_AUTH` step and what a later `HAS_APPROVED_AUTH_TYPE` adaptive risk can see                                                     | —                                                                                                                             |

In the browser this is an ordinary OpenIAM session cookie: nothing about it says "certificate", so the `LOGIN_PKS12` audit record is where you confirm how the session was obtained.

## Related

* [Authentication providers and MFA](/administration-guide/security/authentication-providers-and-mfa.md) — capability flags, the other factors, and the break-glass recovery
* [Configure the Access Gateway](/administration-guide/gateway/access-gateway.md) — the proxy-terminated certificate flow
* [Password and authentication policy parameters](/administration-guide/reference/policy-parameters.md) — `FAILED_AUTH_COUNT` on policy `4001`

*Applies to OpenIAM 2026.8.1. Last reviewed 7 August 2026.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/administration-guide/security/certificate-authentication.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
