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.
- The login page shows a Log in with SSO button once both
QUERYPROXY_OIDC_ISSUERandQUERYPROXY_OIDC_CLIENT_IDare set. - The user signs in at the provider and comes back to
{APP_URL}/auth/oidc/callback. - 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 andsub) signs in without it.- A verified e-mail for any account QueryProxy has not seen before (see
below):
email_verifiedset totrue. Microsoft’s ID token claims reference does not list that claim for Microsoft Entra ID; for it,xms_edovset totruecounts as verified whenemail_verifiedis absent. Anemail_verifiedthat is explicitlyfalseis refused. If neither claim says the address is verified, it counts as not verified. nameorpreferred_usernamefor 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_verifiedistrue, orxms_edovistruefor 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 withQUERYPROXY_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
groupis 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.teamis the slug or the name of an existing team. An entry that names a team that does not exist is skipped and logged.roleisdba,developerorauditor. The system administrator flag cannot be set from a group.- When several groups reach the same team, the highest role wins
(
dba, thendeveloper, thenauditor).
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
groupsclaim 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_verifiednorxms_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
- 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.
- Add a Group Membership mapper to the client’s dedicated scope, with the
token claim name
groups, and keep Add to ID token on. - 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. - 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
- In App registrations, register a web application with the redirect URI above and create a client secret.
- Use the v2.0 issuer of your tenant:
https://login.microsoftonline.com/<tenant-id>/v2.0. - 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). - 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
groupsclaim and are refused while a role map is set. - 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_edovis returned only when theemailclaim is present. QueryProxy requests theemailscope by default; on the v2.0 endpoint that scope also gives theemailclaim.) If the portal does not listxms_edov, add it tooptionalClaims.idTokenin 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 anemail_verifiedclaim for Entra ID. If you do not addxms_edovto 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 asauth.sso_refused.xms_edovistrueonly 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
- Create an OIDC web application and register the redirect URI.
- 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
groupsand 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. - With the org authorization server, request the
groupsscope:
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.
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_endpointin 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
- SCIM provisioning to create and suspend users from the provider.
- Configuration for the variables in context.
- Production hardening for the rest of the checklist.
- Approval workflow for what each team role may do.