← VisualEyes for your site

VisualEyes as a SAML 2.0 identity provider

If your app speaks SAML 2.0 — Nextcloud, Shibboleth-era SaaS, enterprise portals and most older intranet platforms do — you can offer “Sign in with VisualEyes” without writing any code. Point the app at our metadata URL, register your Assertion Consumer Service URL, map one attribute, and your users sign in by recognising their own photos instead of typing a password.

VisualEyes is a SAML 2.0 Identity Provider supporting the Web Browser SSO profile, SP-initiated only. It runs alongside our OpenID Connect provider — same photo-login core, same pairwise identity per user — so the legacy tier integrates by configuration too. If your app supports both protocols, use OIDC: it is simpler, has no XML, and needs no certificate handling.

Quick start
  1. Register your site at /client/register (email + one-time code). You get a client_id and a secret — the same credentials used by the client API and the OIDC facade; one registration covers all of them.
  2. Find your app’s SAML details. Every SP publishes (or documents) two values: its EntityID and its Assertion Consumer Service (ACS) URL. They are usually in the SP’s own metadata document.
  3. Enable SAML in your dashboard: switch it on, paste your SP’s EntityID, and add your ACS URL. ACS URLs are matched character-for-character and must be https (http://localhost and http://127.0.0.1 are allowed for development).
  4. Configure your app with:
    IdP metadata URL:  https://aqa.com/saml/metadata
    IdP EntityID:      https://aqa.com
    SSO URL:           https://aqa.com/saml/sso     (HTTP-Redirect or HTTP-POST)
    NameID format:     urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
    Signing cert:      the X.509 certificate published in our metadata
    Account mapping:   the ve_uid attribute        (see Identity below)
  5. Test it. Start a login from your app: the browser goes to our hosted sign-in gate, the user types their VisualEyes name and solves their photo challenge, and a signed assertion is POSTed back to your ACS.
What the flow looks like

SP-initiated Web Browser SSO — your app never sees photos or secrets, only standard SAML artifacts:

  1. Your app sends the browser to /saml/sso with an AuthnRequest (HTTP-Redirect or HTTP-POST binding) and, optionally, RelayState.
  2. We check the request: your EntityID must be registered and enabled, and the AssertionConsumerServiceURL must be on your exact-match allow-list. Until both are proven, nothing is ever sent to an address the request supplied — a bad request renders a local error page here, never a redirect.
  3. We show the VisualEyes gate, run the user’s photo-recognition challenge — including all of our risk step-up, lockout and duress machinery.
  4. We build a Response containing one Assertion, sign both, and auto-POST it to your registered ACS URL over the HTTP-POST binding, with your RelayState echoed back byte-for-byte.

If the user declines at the gate, or the challenge fails, you get a signed error Response at the same ACS rather than a dangling browser — so your app can end the attempt cleanly.

Endpoints & wire format
ItemValue
Metadatahttps://aqa.com/saml/metadata
served as application/samlmetadata+xml
Single Sign-Onhttps://aqa.com/saml/sso
HTTP-Redirect and HTTP-POST bindings
EntityID / Issuerhttps://aqa.com
NameID formatpersistent
Response bindingHTTP-POST to your registered ACS
Signingrsa-sha256, exclusive C14N, sha256 digests — both the Response and the Assertion are signed
AuthnContextClassRefurn:vercet:visualeyes:photo
Assertion validity5 minutes, with 30 seconds of clock skew

Fetch the signing certificate from our metadata rather than hard-coding it. When we rotate a key, both the old and the new certificate are listed as use="signing" throughout the overlap window, so an SP that refreshes its metadata cache rotates with no downtime. Metadata is served over HTTPS and is deliberately unsigned — the transport is the trust anchor, exactly as for OIDC discovery.

Identity — map your accounts to ve_uid

The NameID we issue is a persistent, pairwise subject: deterministic for a given person at your service provider, different at every other VisualEyes-connected SP, and never the email address. Services cannot correlate users with each other. It is also the same subject the OIDC facade issues for the same VisualEyes customer account — so a customer running both protocols, or migrating from one to the other, keeps one stable identity per user.

We also emit that identical value as an attribute named ve_uid, and that is the mapping we recommend you use. Many SAML products resolve the local account strictly from an attribute and never read the NameID at all — Nextcloud’s user_saml is exactly this, with no NameID fallback. Without ve_uid such an SP could only key on email, which needs the email grant and throws away the uncorrelatable identity the product exists to provide. ve_uid and the NameID always carry the same value, so mapping either one gives you the same stable account key.

AttributeMeaning
ve_uidAlways present. The pairwise, persistent subject — identical to the NameID. Key your accounts on it.
ve_duressAlways present, "true" or "false". See the warning below.
ve_aliasPresent when the site uses per-site aliases: the alias the user signed in with — sign in the matching local account.
emailOnly when your SP has been granted the email attribute (a per-SP setting in your dashboard). Prefer ve_uid; request email only if your app genuinely needs it.

Attribute values are emitted untyped (no xsi:type) with NameFormat basic, which every mainstream SP accepts.

⚠ ve_duress is server-side only — never show it in any UI

If ve_duress is true, the user completed their login with a panic photo — a silent alarm that they may be acting under coercion. Act on it server-side only: show a completely normal session while limiting sensitive actions, alerting your security team, or whatever your duress playbook says. Anything visible in the browser — a banner, a different page, even a subtly different response time you can avoid — can endanger the person standing next to your user. Never surface the attribute in an admin screen, a profile field or a log your users can read. VisualEyes also runs its own out-of-band escalation independently of your app.

Deliberately absent, permanently

VisualEyes holds no login session. That single decision — the same one behind the OIDC facade — removes three things a session-based IdP would offer. They are security properties, not gaps, and they are not on a roadmap:

Also not implemented: assertion encryption, the HTTP-Artifact binding, ECP, metadata-federation ingest and signed metadata. Tell us if one of these genuinely blocks you.

Registering your service provider
Worked example — Nextcloud

Verified end-to-end against Nextcloud 34.0.2 with the SSO & SAML authentication app (user_saml) 8.2.0: Nextcloud’s own AuthnRequest → our gate → photo challenge → signed assertion → its ACS accepts it and establishes a session, with the Nextcloud account keyed on the pairwise subject rather than the user’s email.

1. Register Nextcloud with us

Unless you have changed them, Nextcloud’s SP values are:

SP EntityID:  https://<your-nextcloud>/apps/user_saml/saml/metadata
SP ACS URL:   https://<your-nextcloud>/apps/user_saml/saml/acs

Put both into the SAML card in your dashboard — the ACS exactly as written, with no trailing slash.

2. Configure Nextcloud

Use the occ command line: it is the reliable path, and it is the only one that can set every field in one go. Copy the certificate body out of https://aqa.com/saml/metadata (the text inside <ds:X509Certificate>, without the PEM header and footer lines).

CERT="MIICvjCCAaagAwIBAgIU...<the X509Certificate from our metadata>...=="

# create a provider and note the numeric id it prints
occ saml:config:create

occ saml:config:set 1 \
  --general-idp0_display_name="VisualEyes" \
  --general-uid_mapping="ve_uid" \
  --idp-entityId="https://aqa.com" \
  --idp-singleSignOnService.url="https://aqa.com/saml/sso" \
  --idp-x509cert="$CERT" \
  --security-authnRequestsSigned=0 \
  --security-wantAssertionsSigned=1 \
  --security-wantXMLValidation=1

occ saml:config:validate

On a containerised install, prefix these with your runtime’s exec command and run them as the web user — for example podman exec -u www-data <container> php occ ....

3. The two settings that matter

occ config:app:set user_saml … will silently do nothing

user_saml 8.x stores each provider’s configuration in its own database table, not in the app-config store. Writing keys like idp-entityId with occ config:app:set reports success and has no effect whatsoever — the login then fails with no obvious cause. The occ saml:config:create / saml:config:set / saml:config:validate commands are the correct path.

Other service providers

Shibboleth SP, simpleSAMLphp, and most enterprise SaaS admin panels ask for the same handful of values: our metadata URL (or EntityID + SSO URL + signing certificate), the persistent NameID format, and an attribute to key accounts on — plus registering their ACS URL with us. Map accounts to ve_uid wherever an attribute mapping is offered.

Billing & limits
Security checklist
Standards & conformance

The facade implements SAML 2.0 Web Browser SSO, SP-initiated, and is verified by tools we did not write:

On the inbound side we take a deliberately narrow stance: we only ever produce XML signatures, over XML we generate ourselves, and we never verify XML-DSig on an untrusted document — which removes signature-wrapping as a class of attack. Inbound AuthnRequests are parsed with DTDs, entity declarations, processing instructions and external references hard-rejected (XXE), under strict size caps before and after decompression (compression bombs).

Two design choices differ from a conventional session-based IdP — no session, and therefore no SLO, no IdP-initiated SSO and no silent re-authentication. Both are covered above; both are permanent, and both exist so that every sign-in is a fresh recognition.

Speaks OIDC instead? Use the OpenID Connect guide — it is the simpler integration wherever your platform supports both. Questions, or a platform that won’t cooperate? hello@aqa.com — we’ll help you wire it up.