VisualEyes as an OpenID Connect provider
If your app speaks OpenID Connect — Nextcloud, OpenProject, Grafana, GitLab, Moodle, and most modern platforms do — you can offer “Sign in with VisualEyes” without writing any code. Point the app at our discovery URL, paste your client credentials, register your callback URL, and your users sign in by recognising their own photos instead of typing a password.
- Register your site at /client/register (email + one-time
code). You get a
client_idand asecret— the same credentials used by the client API; one registration covers both. - Enable OpenID Connect in your dashboard: open the
OpenID Connect card, switch it on, and add your app’s redirect URL.
Your app shows (or documents) this callback URL in its OIDC provider settings — copy it
exactly. URLs are matched character-for-character and must be
https(plainhttp://localhostis allowed for development). - Configure your app with:
Discovery / issuer URL: https://aqa.com/.well-known/openid-configuration Client ID: <your client_id> Client secret: <your secret> Scopes: openid (add email only if you need it — see Claims)
- Test it. Click your app’s new “Sign in with VisualEyes” button: it sends the browser to our hosted sign-in gate, the user types their VisualEyes name and solves their photo challenge, and lands back in your app signed in.
Standard authorization-code flow — your app never sees photos or secrets, only standard OIDC artifacts:
- Your app redirects the browser to
/oidc/authorizewith itsclient_id, registeredredirect_uri,stateandnonce(PKCES256supported and encouraged). - We show the VisualEyes gate, run the user’s photo-recognition challenge — including
all of our risk step-up, lockout and duress machinery — then redirect back to your
redirect_uriwith a single-use, 60-second authorizationcode. - Your app’s server exchanges the code at
/oidc/token(HTTP Basic or POST-body client authentication) for an ID token (RS256-signed JWT, 5-minute lifetime) and an opaque access token (1 hour) usable only at/oidc/userinfo.
Replaying a consumed code revokes every token minted from it, redirect URLs are exact-match
only, and alg is pinned to RS256 — the sharp edges of OAuth are handled for you.
| Endpoint | URL |
|---|---|
| Discovery | https://aqa.com/.well-known/openid-configuration |
| Authorization | https://aqa.com/oidc/authorize |
| Token | https://aqa.com/oidc/token |
| UserInfo | https://aqa.com/oidc/userinfo |
| JWKS | https://aqa.com/oidc/jwks.json |
Supported: response_type=code only ·
grant_type=authorization_code · PKCE S256 ·
client_secret_basic / client_secret_post · scopes
openid and email · signing RS256 ·
pairwise subject identifiers.
| Claim | Meaning |
|---|---|
sub | Pairwise and stable: the same user always gets the same
sub at your app, but a different one at every other VisualEyes-connected
site — apps cannot correlate users with each other. Key your accounts on it. |
auth_time | When the photo challenge was passed (epoch seconds). |
nonce | Echoed verbatim from your authorization request. |
email, email_verified | Returned in the
UserInfo response (per the spec, not in the ID token) — only when you requested
scope email and the email scope is enabled for your client in the
dashboard. Prefer sub; request email only if your app genuinely needs it. |
ve_alias | Additive: the per-site alias the user signed in with, when one is in play — sign in the matching local account. |
ve_duress | Boolean. See the warning below. |
⚠ 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. VisualEyes also runs its own out-of-band
escalation independently of your app.
Nextcloud (user_oidc app)
Install the OpenID Connect user backend (user_oidc) app, then
Administration → OpenID Connect → register a provider:
Identifier: VisualEyes Client ID: <your client_id> Client secret: <your secret> Discovery endpoint: https://aqa.com/.well-known/openid-configuration Scope: openid
Nextcloud displays the provider’s redirect URL in the same form — add exactly that URL
to the OpenID Connect card in your VisualEyes dashboard. Map the account
field to the token’s sub (the pairwise ID) unless you granted the email scope.
OpenProject
Administration → Authentication → OpenID providers → add a custom provider:
Display name: VisualEyes
Client ID: <your client_id>
Client secret: <your secret>
Issuer / metadata URL:
https://aqa.com/.well-known/openid-configurationOpenProject shows its callback URL when you save the provider (typically
https://<your-openproject>/auth/<provider>/callback) — register it in your
dashboard’s OpenID Connect card, matching it exactly.
Everything else
Grafana, GitLab, Moodle, Discourse, Keycloak-brokered stacks and most SaaS admin panels ask for the same four values: discovery/issuer URL, client ID, client secret, scopes — plus registering their callback URL with us. If your platform documents “generic OAuth2/OIDC”, that’s the one to use.
- Each successful OIDC sign-in costs the same as any VisualEyes login — 15 credits (~1.5¢), debited from your existing balance. Failed or abandoned sign-ins cost nothing. Your dashboard ledger shows OIDC entries by the user’s pairwise ID, never by email.
- If your balance can’t cover a login, users are returned to your app with
error=temporarily_unavailablebefore they ever see the gate — top up at /dash. - Authorization codes are single-use and live 60 seconds; access tokens 1 hour; ID tokens 5 minutes. Sign-in requests at the gate expire after 10 minutes.
- Not in v1 (tell us if you need them): refresh tokens, dynamic client registration, RP-initiated logout, implicit/hybrid flows (never), SAML.
- Always send and verify
state; send anonceand check it in the ID token. Use PKCE (S256) if your library supports it — ours does. - Register the exact redirect URL your app uses — including path and any trailing slash. We never wildcard-match.
- Validate the ID token against our JWKS (your OIDC library does this from discovery);
expect
RS256and issuerhttps://aqa.com. - Key accounts on
sub. Ifve_aliasis present, use it to select the local account; treatemailas informational. - Have a
ve_duressplaybook before you go live — and keep it entirely server-side (see the warning above). - Keep your client secret server-side only; rotate it from the dashboard if it’s ever exposed.
Our OpenID Connect endpoints are tested against the OpenID Foundation conformance suite (Basic OP profile) — authorization-code flow, PKCE, UserInfo, code single-use and reuse-revocation, redirect-URI validation, scopes, and request-object handling all behave per the OpenID Connect Core specification.
Two deliberate design choices differ from a session-based provider, on purpose:
- No silent re-authentication. We hold no login session and issue no refresh
tokens, so
prompt=none,id_token_hintand largemax_agere-use returnlogin_requiredrather than logging a user in without a photo challenge. Every sign-in is a fresh recognition — that is the security property, not a limitation. - No profile data. We don’t hold names, addresses or phone numbers, so
those scopes and claims are simply not offered. You get a stable pairwise
sub(andemailif you request and are granted it).
We are conformance-tested, not formally certified — by choice: certification would require adding a login session, which would undermine the “always prove it with photos” guarantee that is the whole point of VisualEyes.
Questions, or a platform that won’t cooperate? hello@aqa.com — we’ll help you wire it up.