QueryProxy
Documentation menu

Single sign-on with OpenID Connect

Sign users in through your identity provider with OpenID Connect, and turn provider groups into team roles. Settings for Keycloak, Entra ID, Okta and Google.

Updated:

QueryProxy can sign users in through any OpenID Connect (OIDC) provider that publishes a discovery document, and it can set their team roles from the groups the provider reports. This page covers the settings, one example per provider, the role map, and the behaviour you need to know before you switch password login off.

Multi-factor authentication belongs to the provider. A user who signs in through SSO is outside QUERYPROXY_REQUIRE_2FA: QueryProxy does not ask them for a second factor and cannot check which one the provider used. Enforce MFA in the identity provider, on the application you register for QueryProxy.

How it works

QueryProxy uses the authorization code flow with PKCE. It checks state and nonce, and it verifies the signature of the ID token against the provider’s published keys. SAML is not supported.

  1. The login page shows a Log in with SSO button once both QUERYPROXY_OIDC_ISSUER and QUERYPROXY_OIDC_CLIENT_ID are set.
  2. The user signs in at the provider and comes back to {APP_URL}/auth/oidc/callback.
  3. QueryProxy finds or creates the user, applies the role map, and opens a session.

A user is identified by the issuer and the subject (sub) together, not by e-mail address.

Register QueryProxy at the provider

Create a confidential OIDC client (a “web application”) and register one redirect URI:

https://queryproxy.example.com/auth/oidc/callback

It is APP_URL plus /auth/oidc/callback, and it must match exactly. Set APP_URL to the address users type, normally the HTTPS URL behind your reverse proxy.

Also register a post-logout redirect URI, APP_URL plus /login, for example https://queryproxy.example.com/login. QueryProxy sends it when it ends the provider’s session at sign-out (see Signing out). If it is not registered, the provider may show an error page instead of sending the user back.

Then set the client on the QueryProxy side:

QUERYPROXY_OIDC_ISSUER=https://idp.example.com/realms/queryproxy
QUERYPROXY_OIDC_CLIENT_ID=queryproxy
QUERYPROXY_OIDC_CLIENT_SECRET=...        # or QUERYPROXY_OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc

QUERYPROXY_OIDC_ISSUER is the URL whose /.well-known/openid-configuration describes the provider. Restart the containers after changing it.

Variable Default Meaning
QUERYPROXY_OIDC_ISSUER none Issuer URL. SSO is on when this and the client ID are both set
QUERYPROXY_OIDC_CLIENT_ID none Client id registered at the provider
QUERYPROXY_OIDC_CLIENT_SECRET none Client secret. QUERYPROXY_OIDC_CLIENT_SECRET_FILE reads it from a file instead
QUERYPROXY_OIDC_SCOPES openid email profile Scopes requested, separated by spaces. Some providers need groups added
QUERYPROXY_OIDC_GROUP_CLAIM groups Claim that lists the user’s groups. A dotted path such as realm_access.roles is followed when no claim has that exact name
QUERYPROXY_OIDC_ROLE_MAP none Comma-separated group:team:role entries; see The role map
QUERYPROXY_LOCAL_LOGIN all Who may still sign in with a password: all, admins_only or none

What the ID token must carry

  • sub, always.
  • email, a valid address, the first time a provider identity signs in. Without it QueryProxy can neither link nor create an account, and the sign-in is refused. An identity it already knows (same issuer and sub) signs in without it.
  • A verified e-mail for any account QueryProxy has not seen before (see below): email_verified set to true. Microsoft’s ID token claims reference does not list that claim for Microsoft Entra ID; for it, xms_edov set to true counts as verified when email_verified is absent. An email_verified that is explicitly false is refused. If neither claim says the address is verified, it counts as not verified.
  • name or preferred_username for the display name. If neither is present the part of the address before @ is used.

First sign-in, linking and verified e-mail

  • A new user is created on the first sign-in (just-in-time provisioning), but only when the provider says the address is verified (email_verified is true, or xms_edov is true for Entra ID). A new user has no password and no team until the role map, or an administrator, gives them one. Nobody becomes a system administrator this way.
  • An existing local account with the same e-mail address is linked to the provider, again only when the address is verified in the same way. The link is audited as user.sso_linked. After linking, the account keeps its password unless you restrict password login with QUERYPROXY_LOCAL_LOGIN.
  • If the address is not verified, the sign-in is refused and the refusal is audited as auth.sso_refused.
  • A local account that is already tied to a different provider subject is refused too, because a matching e-mail alone proves nothing. That refusal is written to the application log, not to the audit log.
  • Suspended and deleted users cannot sign in through SSO either.

Connect only providers that verify the address a user claims. A provider that marks unverified addresses as verified can take over a local account that has the same e-mail address.

Changing QUERYPROXY_OIDC_ISSUER later does not carry existing SSO accounts over: they are tied to the old issuer and are refused instead of being matched by e-mail.

Audit events written by SSO: auth.sso_login, auth.sso_refused, user.sso_provisioned, user.sso_linked and user.role_synced.

The role map

QUERYPROXY_OIDC_ROLE_MAP turns provider groups into team roles at every sign-in. Each entry is group:team:role; separate entries with commas:

QUERYPROXY_OIDC_ROLE_MAP=platform-dba:payments:dba,platform-dev:payments:developer,security:payments:auditor
  • group is compared with the values in the group claim as an exact, case-sensitive string. The entry is split from the right, so a group name may contain colons.
  • team is the slug or the name of an existing team. An entry that names a team that does not exist is skipped and logged.
  • role is dba, developer or auditor. The system administrator flag cannot be set from a group.
  • When several groups reach the same team, the highest role wins (dba, then developer, then auditor).

At every sign-in QueryProxy makes the user’s memberships in the teams the map names match the groups: it adds, changes and removes them, and writes each change to the audit log as user.role_synced. Memberships in teams the map does not name are not touched. That makes the provider the source of truth for those teams: a role you change by hand there is overwritten at the user’s next sign-in.

Things to know:

  • Changes apply at the next sign-in. QueryProxy does not poll the provider. To cut off someone at once, suspend them on the Users screen: that ends their open sessions.
  • A missing group claim means the sign-in is refused. While a role map is set, an ID token without the group claim is refused, logged, and nothing is changed. An empty group list is a valid answer and is applied as such: the user loses the roles in the mapped teams.
  • Microsoft Entra ID group overage is not supported. When a user belongs to too many groups, Entra ID leaves the groups claim out and points to Microsoft Graph instead; QueryProxy does not follow that pointer, so with a role map set those users are refused. See the Entra ID example for ways to stay under the limit.
  • If the ID token lacks the group claim, or says nothing about whether the e-mail is verified (neither email_verified nor xms_edov), QueryProxy also reads the provider’s userinfo endpoint for the claims that are missing.
  • The role map is optional. Without it, users who sign in through SSO get no team until an administrator adds them.

Provider examples

Only Keycloak is tested end to end by the project (the CI workflow signs in to a Keycloak 26.8.1 container and checks the group sync). The other three follow each provider’s documentation; try them with a test user before you roll out.

Keycloak

  1. In your realm, create an OpenID Connect client that uses the standard (authorization code) flow and has a client secret. Set the valid redirect URI as above.
  2. Add a Group Membership mapper to the client’s dedicated scope, with the token claim name groups, and keep Add to ID token on.
  3. Decide on Full group path. When it is on, group names are paths such as /engineering/backend, and the role map must use that exact text. Switching it off gives plain names.
  4. Settings:
QUERYPROXY_OIDC_ISSUER=https://keycloak.example.com/realms/queryproxy
QUERYPROXY_OIDC_CLIENT_ID=queryproxy
QUERYPROXY_OIDC_CLIENT_SECRET=...
QUERYPROXY_OIDC_ROLE_MAP=dba-team:payments:dba,developers:payments:developer

Only users whose address Keycloak reports as verified (email_verified) can be created or linked. Reference: Keycloak server administration guide.

Microsoft Entra ID

  1. In App registrations, register a web application with the redirect URI above and create a client secret.
  2. Use the v2.0 issuer of your tenant: https://login.microsoftonline.com/<tenant-id>/v2.0.
  3. Under Token configuration, add a groups claim. By default Entra ID puts group object IDs in the claim, so the role map entries use those IDs (0760b6cf-170e-4a14-91b3-4b78e0739963:payments:dba).
  4. Choose Groups assigned to the application instead of all groups to stay below the limit of 200 groups in a token; users beyond it get no groups claim and are refused while a role map is set.
  5. Add the claims QueryProxy uses to verify the e-mail address. Under Token configuration, select Add optional claim, select the ID token type, select xms_edov and, if the token does not already carry an e-mail address, email, then select Add. (xms_edov is returned only when the email claim is present. QueryProxy requests the email scope by default; on the v2.0 endpoint that scope also gives the email claim.) If the portal does not list xms_edov, add it to optionalClaims.idToken in the app’s Manifest instead; Microsoft documents the manifest as the other way to configure optional claims.
QUERYPROXY_OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
QUERYPROXY_OIDC_CLIENT_ID=<application-client-id>
QUERYPROXY_OIDC_CLIENT_SECRET=...
QUERYPROXY_OIDC_ROLE_MAP=<group-object-id>:payments:dba

Without xms_edov, the first sign-in is refused. Microsoft’s ID token claims reference does not list an email_verified claim for Entra ID. If you do not add xms_edov to the ID token, QueryProxy cannot tell that the address is verified, and it neither creates nor links the account: the sign-in is refused and the refusal is audited as auth.sso_refused. xms_edov is true only when the tenant has verified the domain of the address; Microsoft states that Facebook and SAML/WS-Fed accounts do not have verified domains, so such users are refused as well.

References: Configure optional claims, Optional claims reference (xms_edov, email), Group claims in tokens.

Okta

  1. Create an OIDC web application and register the redirect URI.
  2. Add a groups claim to the ID token: on the app’s Sign On tab, edit OpenID Connect ID Token and set a Group claim type with the claim name groups and a filter that returns the groups you map. On a custom authorization server, add a claim of value type Groups to the ID token instead.
  3. With the org authorization server, request the groups scope:
QUERYPROXY_OIDC_ISSUER=https://example.okta.com
QUERYPROXY_OIDC_CLIENT_ID=<client-id>
QUERYPROXY_OIDC_CLIENT_SECRET=...
QUERYPROXY_OIDC_SCOPES="openid email profile groups"
QUERYPROXY_OIDC_ROLE_MAP=Platform DBA:payments:dba

With a custom authorization server the issuer is its URL, for example https://example.okta.com/oauth2/default; there the groups come from the claim you added, and groups is needed in QUERYPROXY_OIDC_SCOPES only if you tied that claim to a groups scope. Reference: Customize tokens with a groups claim.

Google

Create an OAuth client of type Web application in the Google Cloud console and add the redirect URI. Google’s issuer is https://accounts.google.com, and its ID token carries email_verified when the email scope is requested. Google’s OpenID Connect documentation does not describe a groups claim, so use Google for sign-in only and leave QUERYPROXY_OIDC_ROLE_MAP empty; give teams to users by hand.

QUERYPROXY_OIDC_ISSUER=https://accounts.google.com
QUERYPROXY_OIDC_CLIENT_ID=<client-id>.apps.googleusercontent.com
QUERYPROXY_OIDC_CLIENT_SECRET=...

Reference: OpenID Connect | Google Identity.

Signing out

When a user who signed in through SSO signs out of QueryProxy, QueryProxy also ends the session at the identity provider (RP-initiated logout), so the user is signed out there too. It redirects the browser to the provider’s end_session_endpoint with the user’s ID token as id_token_hint, the client_id and the post-logout redirect URI {APP_URL}/login.

  • The provider must publish an end_session_endpoint in its discovery document. Without one, or when the discovery document cannot be read, sign-out ends only the QueryProxy session and returns to the login page.
  • A session opened with a password, and a local user, never go to the provider.
  • The ID token is kept in the server-side session for this purpose only. It is not written to the log or the audit log.
  • Sign-out is a POST with a CSRF token, as before.

Password login next to SSO

QUERYPROXY_LOCAL_LOGIN limits who may still use a password:

Value Meaning
all Default. Everyone with a password can use it
admins_only Only system administrators sign in with a password (break-glass access); everyone else uses SSO
none Password login and password reset are off

An unknown value is treated as admins_only. Make sure SSO works, with an administrator account that can use it, before you set none: to get back in you would have to change the variable and restart.

Accounts that exist only through SSO have no local password. A user linked to the provider gets no password through the reset link, and the administrator’s Reset password action refuses them, whatever QUERYPROXY_LOCAL_LOGIN says. The password form on the profile page is hidden for users without a local password. Accounts created by SCIM count as SSO accounts. QUERYPROXY_REQUIRE_2FA still applies to a local account that was linked to the provider and signs in with its password.

A user who is linked to the provider and already had a local password can still sign in with it while QUERYPROXY_LOCAL_LOGIN is all, and can change it on the profile page. To make the provider the only way in for everyone, set QUERYPROXY_LOCAL_LOGIN=admins_only or none.

Limits

  • Sign-out at the provider needs an end_session_endpoint. A provider that does not publish one keeps the user signed in there; see Signing out.
  • One provider per installation. SAML is not supported. Removing access between sign-ins is what SCIM provisioning is for.
  • Creating a personal access token requires an SSO user without a password to have signed in within the last 15 minutes; see REST API and tokens.

Next steps

Arrow keys to move, Enter to open.