Skip to content

Trust Diagnostics#

Trust misconfiguration is one of the harder FIDO2 problems to diagnose, because it reaches the end user as a generic registration failure. An authenticator that was working yesterday starts being rejected, and nothing in the response says why.

The FIDO2 server exposes read-only endpoints that make the trust state visible. They are diagnostics only — they never change attestation behaviour.

Attestation configuration#

GET /jans-fido2/restv1/trust/attestation/config
GET /fido2/trust/attestation/config

Served under the Config API base path. Requires one of the https://jans.io/oauth/config/fido2.readonly, fido2.write, fido2.admin or read-all scopes.

{
    "attestationMode": "monitor",
    "attestationModeRecognized": true,
    "unattestedAuthenticatorsAllowed": true,
    "enterpriseAttestation": false,
    "metadataServiceDisabled": false,
    "appleRootCaPresent": true,
    "enabledFidoAlgorithms": ["RS256", "ES256"],
    "hints": []
}

Attestation modes#

attestationMode is set in the FIDO2 configuration and defaults to monitor.

Mode Behaviour
disabled Attestation is not validated.
monitor Attestation is validated and logged, but a failure does not reject the registration.
enforced A failing attestation check rejects the registration.

Important

Only enforced rejects a registration on the basis of attestation. Both disabled and monitor are lenient about attestation, so under the default monitor mode an authenticator whose attestation cannot be validated — an unknown AAGUID, an expired metadata blob, an untrusted root — is still registered successfully. If you are relying on attestation to keep unapproved authenticators out, monitor will not do it.

This concerns attestation only. The other registration checks — challenge, origin, RP ID, signature — reject an invalid registration in every mode.

unattestedAuthenticatorsAllowed reports this directly: it is true for every mode except enforced.

attestationModeRecognized is false when the configured value is not one of the three modes above — for example a typo such as Enforce. An unrecognised value leaves the server in lenient behaviour, so this flag is worth checking when enforcement appears not to be taking effect.

metadataServiceDisabled reflects the disableMetadataService setting. When it is true, attestation cannot be validated against FIDO metadata at all.

appleRootCaPresent reports whether the Apple WebAuthn root CA certificate was loaded at startup. When it is false, Apple anonymous attestation cannot be validated.

MDS health#

A stale or failed metadata load is a common cause of "an authenticator that worked yesterday is suddenly rejected". Until now those failures were written to the server log and then discarded, leaving log-grepping as the only way to find them.

GET /jans-fido2/restv1/trust/mds/health
GET /fido2/trust/mds/health

Served under the Config API base path. Requires one of the https://jans.io/oauth/config/fido2.readonly, fido2.write, fido2.admin or read-all scopes.

{
    "status": "UP",
    "metadataServiceDisabled": false,
    "tocEntryCount": 1284,
    "nextUpdate": "2026-08-15",
    "blobExpired": false,
    "lastSuccessfulRefresh": "2026-08-01T04:15:22Z",
    "metadataServers": [
        {
            "url": "https://mds.fidoalliance.org/",
            "rootCertConfigured": false
        }
    ],
    "timestamp": "2026-08-07T09:31:04.118Z"
}

The endpoint is read-only in the strict sense: it reports in-memory state only, and never triggers a metadata download or reads the document store. It is safe to poll.

Status and HTTP codes#

status HTTP Meaning
UP 200 Metadata is loaded and its nextUpdate is still in the future.
DOWN 503 No metadata is loaded, or the loaded blob has reached its nextUpdate — today or earlier — and a refresh is overdue.
DISABLED 200 disableMetadataService is set.

DISABLED deliberately returns 200. A metadata service switched off by configuration is a choice, not an outage, and should not page anyone when this endpoint is wired to a monitor.

The DOWN body is returned with the 503, so a monitor that captures the response gets lastRefreshError and tocEntryCount along with the status code.

Note

A failed refresh on its own is not DOWN. While the cached blob is still inside its validity window, attestation validation keeps working normally off that cached metadata, so returning 503 would page an operator for a service that is functioning. The failure is still reported — as lastRefreshError on a 200 response. Alert on that field if you want early warning that the metadata is heading towards expiry, and on status for the outage itself.

Fields#

Field Meaning
tocEntryCount Authenticator metadata entries currently loaded in memory. Zero means attestation has nothing to validate against.
nextUpdate The nextUpdate declared by the loaded TOC blob. Absent when no blob has been parsed since startup.
blobExpired true when no blob is loaded, or its nextUpdate is today or earlier — a re-download is due. This is the same rule the server applies when deciding whether to download. It does not mean the blob has been dropped: that only happens once nextUpdate has passed.
lastSuccessfulRefresh When metadata was last downloaded and parsed successfully, as an ISO-8601 date-time with a UTC offset. Absent when no refresh has succeeded since startup.
lastRefreshError Why the most recent refresh failed. Absent when the last refresh succeeded.
metadataServers[].rootCertConfigured Whether a per-endpoint trust anchor is configured for that endpoint.

lastRefreshError reports the first failure of the attempt, because that is the root cause: a failed download is followed by a fallback to the cached blob, which tends to fail as well. Reporting the later failure would describe an unreachable endpoint as an unparseable document.

metadataServers[].rootCertConfigured is a presence flag only. The configured certificate is never included in the response.

Attestation rejection diagnostics#

The attestation configuration tells you what the policy is, and MDS health tells you whether the metadata behind it loaded. Neither tells you why individual registrations are failing — an unknown AAGUID, an authenticator blocked by an MDS status report and an untrusted root certificate all reach the end user as the same generic registration failure.

When attestation is rejected for a trust or metadata reason, the server records a diagnostic code against the registration in the metrics store, in the existing errorReason field, under the ATTESTATION_TRUST error category.

Note

These codes are internal. The response returned to the client is unchanged — still the FIDO {"status": "failed", "errorMessage": "…"} envelope — and a diagnostic code is never included in it. Recording a code changes nothing about whether a registration is rejected.

Code reference#

Code Cause What to do
JFS_AAGUID_NOT_IN_MDS The authenticator's AAGUID is not in the loaded metadata. Check tocEntryCount on the MDS health endpoint. If metadata loaded normally, the model genuinely is not in the FIDO Alliance blob — see Vendor Metadata.
JFS_AUTHENTICATOR_STATUS_UNACCEPTABLE The authenticator is blocked by an MDS status report — revoked, compromised, or not FIDO certified. Working as intended. The model is one the FIDO Alliance has flagged.
JFS_ROOT_CERT_NOT_TRUSTED The attestation certificate chain did not verify to a trusted root. Check that the authenticator's root CA is present in the configured authenticatorCertsFolder.
JFS_APPLE_ROOT_CA_MISSING Apple attestation was attempted with no Apple WebAuthn root CA loaded. Load the Apple WebAuthn root CA; appleRootCaPresent on the attestation config endpoint reports the same condition. Until then every Apple registration fails.
JFS_ATTESTATION_FORMAT_NOT_PERMITTED The attestation statement format is not one the server accepts. Check the attestation formats the deployment supports against what the authenticator sent.
JFS_MDS_UNAVAILABLE Metadata was required to validate the authenticator, but none could be obtained. Check MDS healthlastRefreshError says why the most recent refresh failed.
JFS_MDS_METADATA_EXPIRED The loaded metadata blob was past its nextUpdate. Reserved. An expired blob is discarded at load time rather than used, so an expired blob currently surfaces as JFS_AAGUID_NOT_IN_MDSblobExpired on the MDS health endpoint is what confirms expiry is the cause.

Rejection analytics#

GET /jans-fido2/restv1/metrics/analytics/attestation-rejections?startTime=&endTime=
GET /fido2/metrics/analytics/attestation-rejections?start_date=&end_date=
{
    "totalRejections": 37,
    "registrationAttempts": 412,
    "rejectionRate": 0.0898,
    "reasonCodes": {
        "JFS_AAGUID_NOT_IN_MDS": 21,
        "JFS_AUTHENTICATOR_STATUS_UNACCEPTABLE": 9,
        "JFS_ROOT_CERT_NOT_TRUSTED": 5,
        "JFS_ATTESTATION_FORMAT_NOT_PERMITTED": 2
    },
    "topRejectedAaguids": {
        "d8522d9f-575b-4866-88a9-ba99fa02f35b": 14,
        "cb69481e-8ff7-4039-93ec-0a2729a154a8": 9,
        "2fc0579f-8113-47ea-b116-bb5a8db9202a": 7
    }
}

This reads the same metrics store as analytics/errors, so it requires metrics to be enabled — with metrics off, nothing is recorded and the endpoint reports zero.

rejectionRate is rejections over the registration attempts in the same range. A rejection and the attempt it belongs to are separate records, so a range can contain one without the other: when no attempts fall in the range the rate is null rather than 0.0, with a rejectionRateNote explaining why, and when rejections outnumber attempts it is capped at 1.0. Widen the range for an exact figure.

topRejectedAaguids counts rejections per authenticator model. Rejections not tied to a model — an attestation format the mode does not permit, for instance — appear in reasonCodes only.

Important

Under the default monitor mode an authenticator that fails attestation is still registered. Rejections counted here are the ones that actually failed the registration, which in monitor mode means the failure came from something other than the lenient attestation path. If you expect rejections and see none, check attestationMode first.

Troubleshooting#

Enforcement does not seem to apply. Check attestationMode and attestationModeRecognized. An unrecognised mode value silently leaves the server lenient.

Unapproved authenticators are being registered. The default monitor mode does not reject them. Check unattestedAuthenticatorsAllowed; only enforced will keep them out.

Certain authenticators are rejected while others succeed. With attestationMode set to enforced, an authenticator whose AAGUID is absent from the loaded metadata is rejected. Check tocEntryCount on the MDS health endpoint — a low or zero count means the metadata did not load. See Vendor Metadata for supplying metadata locally.

Authenticators that worked yesterday are now rejected. Check MDS health. blobExpired: true with status: DOWN means a refresh is overdue. Note the two are not the same thing: a blob is only discarded once its nextUpdate has actually passed — on the day itself it is still used. Once it has passed, the cached blob is dropped rather than used, so under enforced mode nothing validates and tocEntryCount falls to 0. lastRefreshError says why the refresh that should have replaced it failed.

MDS health reports DISABLED. disableMetadataService is set in the FIDO2 configuration. No metadata is downloaded and attestation cannot be validated against FIDO metadata.

Apple devices fail to register. Check appleRootCaPresent. Without the Apple WebAuthn root CA, Apple anonymous attestation cannot be validated.