16.1.1.3. Validate endpoints

The validate REST API verifies one-time passwords, drives challenge- response flows, and supports out-of-band token polling. It is the endpoint group that consumers (RADIUS plugins, SAML adapters, PAM modules, web applications) call to actually authenticate a user.

This is distinct from Authentication endpoints, which issues the JWT-based admin/user session tokens for the management API.

The endpoints fall in five groups:

  • POST /validate/check — verify a user/serial + password and, for challenge-response tokens, trigger or complete the challenge. Two URL aliases exist for protocol adapters: /validate/radiuscheck shapes the response into RADIUS-friendly status codes (204 / 400), and /validate/samlcheck returns user attributes alongside the auth result.

  • POST /validate/triggerchallenge — admin-only, requires the triggerchallenge policy. Forces a challenge for every matching challenge-response token of a user.

  • GET /validate/polltransaction — anonymous. Out-of-band tokens (push, container) poll this to see whether a challenge has been answered.

  • POST /validate/initialize — anonymous. Bootstraps a FIDO2/passkey challenge before login.

  • POST /validate/offlinerefill — refills the OTP buffer of a token attached to a machine for offline use.

Authentication workflow

In case of authenticating a user:

In case of authenticating a serial number:

See Authentication Modes and Client Modes for the per-token-type interaction model that clients consume from challenge responses.

POST /validate/offlinerefill

Replenish the OTP buffer of a token that is attached to a machine for offline use. Each successful refill rotates the refilltoken so that the previous one cannot be reused.

For HOTP tokens, the response carries enough fresh OTP values to bring the offline buffer back up to the configured count. The caller must supply the last password (PIN+OTP) the end user entered so the server can advance the counter to the right position. For WebAuthn / Passkey tokens, the response carries only the new refilltoken and the WebAuthn/Passkey machine name is read from the user agent string; pass should be an empty string in that case.

The response carries the new refilltoken, the token serial, and the OTP material under response.auth_items.offline. Failures may be masked into a generic error by the hide_specific_error_message_for_offline_refill user-scope policy.

JSON Parameters:
  • serial – token serial number (required).

  • refilltoken – the refill authorization token issued on the previous refill or at offline attachment time (required).

  • pass – the last PIN+OTP the user entered (required; empty string for WebAuthn / Passkey).

Status Codes:
  • 200 OK – refill payload in the response body, with the new refilltoken and OTP material under auth_items.offline.

  • 400 Bad Request – the token does not exist, is not marked for offline use, the refilltoken is wrong, or the calling machine cannot be identified from the user agent (WebAuthn/Passkey).

POST /validate/samlcheck
POST /validate/radiuscheck
POST /validate/check

Verify an authentication attempt.

The endpoint is bound to three URL paths that share the same request shape but produce different response shapes for protocol adapters:

  • /validate/check — standard JSON response, result.value is true / false.

  • /validate/radiuscheck — RADIUS adapter shape: a successful authentication returns an empty 204, a failed authentication an empty 400. Error responses (server-side faults) are the same as for /validate/check.

  • /validate/samlcheck — SAML adapter shape: result.value is a dictionary with auth (bool) and attributes (the user’s resolver attributes, optionally extended via the resolver’s attribute map and MULTIVALUEATTRIBUTES).

Either user (with optional realm) or serial is required. The PIN+OTP is sent in pass. Subsequent legs of a challenge-response flow carry transaction_id (and any additional fields the token type needs). The authorization decision can be vetoed by the AUTHZ-scope authorized=deny_access policy (see Authorization policies).

JSON Parameters:
  • serial – token serial. Either serial or user is required.

  • user – login name of the user. Either serial or user is required.

  • realm – realm of the user; defaults to the default realm if omitted.

  • pass – PIN concatenated with OTP. For WebAuthn/Passkey endpoints it may be empty.

  • type – restrict the authentication to tokens of this type. Requires the AUTHZ policy application_tokentype. Ignored when a serial is supplied.

  • otponly1 to skip the PIN check and only verify the OTP value. Used by the management UI; only meaningful with serial.

  • transaction_id – transaction id for the second leg of a challenge-response flow.

  • state – alias of transaction_id for legacy callers.

  • exception1 to surface delivery failures (SMS, email, push) as HTTP 500 instead of returning a generic challenge-creation error.

  • credential_id – FIDO2 credential id for passkey / WebAuthn authentication.

  • cancel_enrollment1 together with transaction_id cancels an in-progress enroll_via_multichallenge flow without authenticating.

Example Validation Request:

POST /validate/check HTTP/1.1
Host: example.com
Accept: application/json

user=user
realm=realm1
pass=s3cret123456

Example response for a successful authentication:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "detail": {
    "message": "matching 1 tokens",
    "serial": "PISP0000AB00",
    "type": "spass"
  },
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "status": true,
    "value": true
  },
  "version": "privacyIDEA unknown"
}

Example response for this first part of a challenge response authentication:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "detail": {
    "serial": "PIEM0000AB00",
    "type": "email",
    "transaction_id": "12345678901234567890",
    "multi_challenge": [ {"serial": "PIEM0000AB00",
                          "transaction_id":  "12345678901234567890",
                          "message": "Please enter otp from your email",
                          "client_mode": "interactive"},
                         {"serial": "PISM12345678",
                          "transaction_id": "12345678901234567890",
                          "message": "Please enter otp from your SMS",
                          "client_mode": "interactive"}
    ]
  },
  "id": 2,
  "jsonrpc": "2.0",
  "result": {
    "status": true,
    "value": false
  },
  "version": "privacyIDEA unknown"
}

In this example two challenges are triggered, one with an email and one with an SMS. The application and thus the user has to decide, which one to use. They can use either.

The challenges also contain the information of the “client_mode”. This tells the plugin, whether it should display an input field to ask for the OTP value or e.g. to poll for an answered authentication. Read more at Authentication Modes and Client Modes.

Note

All challenge response tokens have the same transaction_id in this case.

Example response for a successful authentication with /samlcheck:

HTTP/1.1 200 OK
Content-Type: application/json

 {
   "detail": {
     "message": "matching 1 tokens",
     "serial": "PISP0000AB00",
     "type": "spass"
   },
   "id": 1,
   "jsonrpc": "2.0",
   "result": {
     "status": true,
     "value": {"attributes": {
                 "username": "koelbel",
                 "realm": "themis",
                 "mobile": null,
                 "phone": null,
                 "myOwn": "/data/file/home/koelbel",
                 "resolver": "themis",
                 "surname": "Kölbel",
                 "givenname": "Cornelius",
                 "email": null},
               "auth": true}
   },
   "version": "privacyIDEA unknown"
 }

The response in value->attributes can contain additional attributes (like “myOwn”) which you can define in the LDAP resolver in the attribute mapping.

GET /validate/samlcheck
GET /validate/radiuscheck
GET /validate/check

Verify an authentication attempt.

The endpoint is bound to three URL paths that share the same request shape but produce different response shapes for protocol adapters:

  • /validate/check — standard JSON response, result.value is true / false.

  • /validate/radiuscheck — RADIUS adapter shape: a successful authentication returns an empty 204, a failed authentication an empty 400. Error responses (server-side faults) are the same as for /validate/check.

  • /validate/samlcheck — SAML adapter shape: result.value is a dictionary with auth (bool) and attributes (the user’s resolver attributes, optionally extended via the resolver’s attribute map and MULTIVALUEATTRIBUTES).

Either user (with optional realm) or serial is required. The PIN+OTP is sent in pass. Subsequent legs of a challenge-response flow carry transaction_id (and any additional fields the token type needs). The authorization decision can be vetoed by the AUTHZ-scope authorized=deny_access policy (see Authorization policies).

JSON Parameters:
  • serial – token serial. Either serial or user is required.

  • user – login name of the user. Either serial or user is required.

  • realm – realm of the user; defaults to the default realm if omitted.

  • pass – PIN concatenated with OTP. For WebAuthn/Passkey endpoints it may be empty.

  • type – restrict the authentication to tokens of this type. Requires the AUTHZ policy application_tokentype. Ignored when a serial is supplied.

  • otponly1 to skip the PIN check and only verify the OTP value. Used by the management UI; only meaningful with serial.

  • transaction_id – transaction id for the second leg of a challenge-response flow.

  • state – alias of transaction_id for legacy callers.

  • exception1 to surface delivery failures (SMS, email, push) as HTTP 500 instead of returning a generic challenge-creation error.

  • credential_id – FIDO2 credential id for passkey / WebAuthn authentication.

  • cancel_enrollment1 together with transaction_id cancels an in-progress enroll_via_multichallenge flow without authenticating.

Example Validation Request:

POST /validate/check HTTP/1.1
Host: example.com
Accept: application/json

user=user
realm=realm1
pass=s3cret123456

Example response for a successful authentication:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "detail": {
    "message": "matching 1 tokens",
    "serial": "PISP0000AB00",
    "type": "spass"
  },
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "status": true,
    "value": true
  },
  "version": "privacyIDEA unknown"
}

Example response for this first part of a challenge response authentication:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "detail": {
    "serial": "PIEM0000AB00",
    "type": "email",
    "transaction_id": "12345678901234567890",
    "multi_challenge": [ {"serial": "PIEM0000AB00",
                          "transaction_id":  "12345678901234567890",
                          "message": "Please enter otp from your email",
                          "client_mode": "interactive"},
                         {"serial": "PISM12345678",
                          "transaction_id": "12345678901234567890",
                          "message": "Please enter otp from your SMS",
                          "client_mode": "interactive"}
    ]
  },
  "id": 2,
  "jsonrpc": "2.0",
  "result": {
    "status": true,
    "value": false
  },
  "version": "privacyIDEA unknown"
}

In this example two challenges are triggered, one with an email and one with an SMS. The application and thus the user has to decide, which one to use. They can use either.

The challenges also contain the information of the “client_mode”. This tells the plugin, whether it should display an input field to ask for the OTP value or e.g. to poll for an answered authentication. Read more at Authentication Modes and Client Modes.

Note

All challenge response tokens have the same transaction_id in this case.

Example response for a successful authentication with /samlcheck:

HTTP/1.1 200 OK
Content-Type: application/json

 {
   "detail": {
     "message": "matching 1 tokens",
     "serial": "PISP0000AB00",
     "type": "spass"
   },
   "id": 1,
   "jsonrpc": "2.0",
   "result": {
     "status": true,
     "value": {"attributes": {
                 "username": "koelbel",
                 "realm": "themis",
                 "mobile": null,
                 "phone": null,
                 "myOwn": "/data/file/home/koelbel",
                 "resolver": "themis",
                 "surname": "Kölbel",
                 "givenname": "Cornelius",
                 "email": null},
               "auth": true}
   },
   "version": "privacyIDEA unknown"
 }

The response in value->attributes can contain additional attributes (like “myOwn”) which you can define in the LDAP resolver in the attribute mapping.

POST /validate/triggerchallenge

Trigger a fresh challenge for every challenge-response token matching the given user and/or serial. Used by the WebUI and by automation that must initiate challenge-response flows on behalf of a user (for example pre-positioning a push prompt).

Requires admin authentication and the policy action triggerchallenge. The request must carry a valid PI-Authorization header.

If the AUTHZ-scope increase_failcounter_on_challenge policy is active, the fail counter is incremented on every matching token before the challenges are created.

JSON Parameters:
  • user – user the challenges should be created for.

  • realm – realm of the user; defaults to the default realm.

  • serial – restrict to a specific token.

  • type – restrict to tokens of this type. Requires the AUTHZ policy application_tokentype. Ignored when serial is supplied.

Request Headers:
  • PI-Authorization – admin auth token.

Status Codes:
  • 200 OKresult.value is the number of created challenges; detail.multi_challenge lists them.

Example response for a successful triggering of challenge:

HTTP/1.1 200 OK
Content-Type: application/json

{
   "detail": {
        "client_mode": "interactive",
        "message": "please enter otp: , please enter otp: ",
        "messages":     [
            "please enter otp: ",
            "please enter otp: "
        ],
        "multi_challenge": [
            {
                "client_mode": "interactive",
                "message": "please enter otp: ",
                "serial": "TOTP000026CB",
                "transaction_id": "11451135673179897001",
                "type": "totp"
            },
            {
                "client_mode": "interactive",
                "message": "please enter otp: ",
                "serial": "OATH0062752C",
                "transaction_id": "11451135673179897001",
                "type": "hotp"
            }
        ],
        "serial": "OATH0062752C",
        "threadid": 140329819764480,
        "transaction_id": "11451135673179897001",
        "transaction_ids": [
            "11451135673179897001",
            "11451135673179897001"
        ],
        "type": "hotp"
   },
   "id": 2,
   "jsonrpc": "2.0",
   "result": {
       "status": true,
       "value": 2
   }

Example response for response, if the user has no challenge token:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "detail": {"messages": [],
             "threadid": 140031212377856,
             "transaction_ids": []},
  "id": 1,
  "jsonrpc": "2.0",
  "result": {"status": true,
             "value": 0},
  "signature": "205530282...54508",
  "time": 1484303812.346576,
  "version": "privacyIDEA 2.17",
  "versionnumber": "2.17"
}

Example response for a failed triggering of a challenge. In this case the status will be false.

HTTP/1.1 200 OK
Content-Type: application/json

{
  "detail": null,
  "id": 1,
  "jsonrpc": "2.0",
  "result": {"error": {"code": 905,
                       "message": "ERR905: The user can not be
                       found in any resolver in this realm!"},
             "status": false},
  "signature": "14468...081555",
  "time": 1484303933.72481,
  "version": "privacyIDEA 2.17"
}
GET /validate/triggerchallenge

Trigger a fresh challenge for every challenge-response token matching the given user and/or serial. Used by the WebUI and by automation that must initiate challenge-response flows on behalf of a user (for example pre-positioning a push prompt).

Requires admin authentication and the policy action triggerchallenge. The request must carry a valid PI-Authorization header.

If the AUTHZ-scope increase_failcounter_on_challenge policy is active, the fail counter is incremented on every matching token before the challenges are created.

JSON Parameters:
  • user – user the challenges should be created for.

  • realm – realm of the user; defaults to the default realm.

  • serial – restrict to a specific token.

  • type – restrict to tokens of this type. Requires the AUTHZ policy application_tokentype. Ignored when serial is supplied.

Request Headers:
  • PI-Authorization – admin auth token.

Status Codes:
  • 200 OKresult.value is the number of created challenges; detail.multi_challenge lists them.

Example response for a successful triggering of challenge:

HTTP/1.1 200 OK
Content-Type: application/json

{
   "detail": {
        "client_mode": "interactive",
        "message": "please enter otp: , please enter otp: ",
        "messages":     [
            "please enter otp: ",
            "please enter otp: "
        ],
        "multi_challenge": [
            {
                "client_mode": "interactive",
                "message": "please enter otp: ",
                "serial": "TOTP000026CB",
                "transaction_id": "11451135673179897001",
                "type": "totp"
            },
            {
                "client_mode": "interactive",
                "message": "please enter otp: ",
                "serial": "OATH0062752C",
                "transaction_id": "11451135673179897001",
                "type": "hotp"
            }
        ],
        "serial": "OATH0062752C",
        "threadid": 140329819764480,
        "transaction_id": "11451135673179897001",
        "transaction_ids": [
            "11451135673179897001",
            "11451135673179897001"
        ],
        "type": "hotp"
   },
   "id": 2,
   "jsonrpc": "2.0",
   "result": {
       "status": true,
       "value": 2
   }

Example response for response, if the user has no challenge token:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "detail": {"messages": [],
             "threadid": 140031212377856,
             "transaction_ids": []},
  "id": 1,
  "jsonrpc": "2.0",
  "result": {"status": true,
             "value": 0},
  "signature": "205530282...54508",
  "time": 1484303812.346576,
  "version": "privacyIDEA 2.17",
  "versionnumber": "2.17"
}

Example response for a failed triggering of a challenge. In this case the status will be false.

HTTP/1.1 200 OK
Content-Type: application/json

{
  "detail": null,
  "id": 1,
  "jsonrpc": "2.0",
  "result": {"error": {"code": 905,
                       "message": "ERR905: The user can not be
                       found in any resolver in this realm!"},
             "status": false},
  "signature": "14468...081555",
  "time": 1484303933.72481,
  "version": "privacyIDEA 2.17"
}
GET /validate/polltransaction/(transaction_id)
GET /validate/polltransaction

Report whether a challenge has been answered. Out-of-band tokens (push, container) poll this endpoint to learn when the user has interacted with the challenge so that the calling client can follow up with POST /validate/check.

This endpoint is anonymous — no authentication header is required.

Parameters:
  • transaction_id – optional path component, the transaction id to check. May also be supplied as a query parameter.

Query Parameters:
  • transaction_id – alternative to the path component.

Status Codes:
  • 200 OKresult.value is true if at least one non-expired challenge with this transaction id has been answered, false otherwise. detail.challenge_status is one of accept (an answered challenge exists), declined (the user declined a challenge), or pending (the challenges are still open or no matching challenge exists at all).

POST /validate/initialize

Initialize an authentication by requesting a fresh challenge for a token type. Currently only the passkey type is supported; the WebUI calls this to obtain the FIDO2 challenge it then forwards to the browser’s navigator.credentials.get call.

For passkey, the webauthn_relying_party_id policy must be set (the FIDO2 prepolicy reads it from the AUTH scope); the user-verification requirement is taken from the user_verification_requirement policy and defaults to preferred.

This endpoint is anonymous — no authentication header is required.

JSON Parameters:
  • type – the token type to initialize a challenge for (passkey; required).

Status Codes:
  • 200 OKresult.value is false (no authentication decision yet); detail.transaction_id carries the transaction id and detail.passkey carries the FIDO2 challenge payload that the client must pass to the authenticator.

  • 400 Bad Request – the requested type is unsupported, the token type is disabled by policy, or the relying-party id policy is missing.

GET /validate/initialize

Initialize an authentication by requesting a fresh challenge for a token type. Currently only the passkey type is supported; the WebUI calls this to obtain the FIDO2 challenge it then forwards to the browser’s navigator.credentials.get call.

For passkey, the webauthn_relying_party_id policy must be set (the FIDO2 prepolicy reads it from the AUTH scope); the user-verification requirement is taken from the user_verification_requirement policy and defaults to preferred.

This endpoint is anonymous — no authentication header is required.

JSON Parameters:
  • type – the token type to initialize a challenge for (passkey; required).

Status Codes:
  • 200 OKresult.value is false (no authentication decision yet); detail.transaction_id carries the transaction id and detail.passkey carries the FIDO2 challenge payload that the client must pass to the authenticator.

  • 400 Bad Request – the requested type is unsupported, the token type is disabled by policy, or the relying-party id policy is missing.