Single sign-on with OpenID Connect Module
How it works
Vaks PM acts as an OpenID Connect relying party. It uses the authorization-code flow with PKCE, and it discovers your provider's endpoints automatically — you never enter an authorization or token URL by hand, only the discovery address.
Three behaviours are worth understanding before you configure anything, because they determine what you must make your provider emit:
- Accounts are matched on one claim only: the object identifier. By default that is
oid, falling back tosub. Email address is never used to link an existing account — if a local account already holds the incoming email but carries no matching object identifier, the sign-in is refused rather than silently taking over the account. - Roles come from a groups claim, which most providers do not emit unless you ask them to. Without it every user lands on the default role and your mappings appear to do nothing.
- Unknown users are either created or refused, your choice. Creating consumes a licence seat; refusing does not.
The claims read are, in order of fallback: object identifier from oid then sub; email from email, then preferred_username, then upn; display name from name, then the email; groups from groups, then roles.
groups on the provider side. Okta and Keycloak both let you choose the claim name; do so rather than expecting Vaks PM to adapt.
Prerequisites
| Side | What you need |
|---|---|
| Identity provider | The right to register an application and, if you want role mapping, to add a groups claim to the token. On Entra this means an app registration plus token configuration; on Google Workspace, super-administrator access. |
| Vaks PM | The org:manage permission — an organization administrator. |
| Licence | A licence covering the single sign-on feature. You can configure a provider without one; federated sign-in stays blocked until the licence is imported under Admin → Organization → License. |
| Infrastructure | Redis must be reachable — the sign-in handshake is held there for 10 minutes. The provider's discovery URL must be HTTPS and publicly resolvable; an identity provider on a private address range is rejected. |
Step 1 — Start in Vaks PM to get the redirect URI
You need the redirect URI before you can register anything with your provider, and Vaks PM generates it from a slug you choose.
Open Admin → Integrations → Single sign-on, add a provider, and fill in the first three fields:
- Protocol — OIDC. This can only be set at creation.
- Slug — lowercase letters, digits and hyphens, 2 to 40 characters (for example
entra). It is frozen once saved, because it is baked into the redirect URI. Changing your mind later means deleting the provider and recreating it. - Display name — the label users see on the login button, such as "Sign in with Microsoft".
The form then shows Redirect URI (generated) with a copy button. It looks like this:
https://<your-host>/api/v1/auth/oidc/<slug>/callback
Copy it. This exact string, character for character, is what your provider needs.
Step 2 — In your identity provider
Follow the section for your provider, then return to step 3. All of them ask for the same three things: the redirect URI you just copied, a client ID and a client secret.
Microsoft Entra ID
- Go to Microsoft Entra admin center → Identity → Applications → App registrations → New registration. Name it, choose single-tenant, and under Redirect URI select platform Web and paste the URI from step 1.
- From Overview, copy the Application (client) ID and the Directory (tenant) ID.
- Under Certificates & secrets → Client secrets → New client secret, create one and copy its Value immediately — it is shown once.
- For role mapping, go to Token configuration → Add groups claim, choose which groups to include (security groups, or groups assigned to the application), and make sure the claim is emitted in the ID token. Entra emits group object IDs, so your mappings will use GUIDs rather than group names.
Vaks PM builds the discovery address for you from the tenant ID — you will paste the tenant ID rather than a URL.
roles claim, which Vaks PM reads as a fallback for groups.
Entra alternative — certificate authentication (no secret)
Instead of a shared client secret, Entra accepts an application authenticating with a certificate (private_key_jwt, RFC 7523): the application itself signs a short-lived proof of identity with its private key, and Entra only ever holds the matching public certificate. Nothing secret travels over the network or is shared with Entra. This is a full alternative to the client secret from the previous step, not an addition — you pick one method or the other.
- Generate a private key / self-signed certificate pair (for example with OpenSSL):
openssl req -x509 -newkey rsa:2048 -keyout vaks.key -out vaks.cer -days 730 -nodes -subj "/CN=vaks-pm". Keepvaks.key(the private key, PEM PKCS#8) somewhere safe — that is what you paste into Vaks PM. - In Entra, on the same application registered for SSO, go to Certificates & secrets → Certificates → Upload certificate and upload the public
vaks.cerfile. - Once the upload is accepted, Entra displays the certificate thumbprint — copy it.
- In Vaks PM, on the provider form, switch Authentication to Certificate, paste the thumbprint and the contents of
vaks.key(PEM private key). Leave the client secret field blank.
private_key_jwt assertion with a kid header, but Entra only matches the certificate on the x5t header (the certificate thumbprint). An assertion without x5t is silently rejected by Entra. Vaks PM therefore signs this assertion itself with the correct header — this detail only matters if you are trying to reproduce this flow elsewhere.
The same certificate authentication method, with the same steps on the Entra side, is also available for Microsoft Teams provisioning (which the SharePoint context inherits from) and for an Entra Agent ID directory — four integrations, one certificate to manage if you reuse the same Entra application across several of them, or a separate certificate per application if you prefer to keep them isolated.
Google Workspace
- In the Google Cloud console, select or create a project, then go to APIs & Services → OAuth consent screen and configure it as Internal so only your Workspace users can use it.
- Go to APIs & Services → Credentials → Create credentials → OAuth client ID, application type Web application. Under Authorized redirect URIs, paste the URI from step 1.
- Copy the Client ID and Client secret shown after creation.
- The discovery URL is the same for every Google tenant:
https://accounts.google.com.
Okta
- In the Okta admin console, go to Applications → Create App Integration, choose OIDC — OpenID Connect and Web Application.
- Under Sign-in redirect URIs, paste the URI from step 1. Set the assignments to the users or groups who should have access.
- Copy the Client ID and Client secret from the application's General tab.
- Your discovery URL is
https://<your-org>.okta.com/oauth2/default(or the custom authorization server you intend to use). - For role mapping, open Security → API → Authorization Servers, select the server, and under Claims add a claim named exactly
groups, included in the ID token, with a filter matching the groups you want to send.
Keycloak
- In the realm you use, go to Clients → Create client, client type OpenID Connect, and give it a client ID.
- Enable Client authentication (this makes it a confidential client, which is what Vaks PM expects), and leave the standard flow enabled.
- Under Valid redirect URIs, paste the URI from step 1.
- Take the client secret from the Credentials tab.
- Your discovery URL is
https://<keycloak-host>/realms/<realm>. - For role mapping, go to the client's Client scopes, open the dedicated scope, and add a Group Membership mapper with token claim name
groups. Turn off Full group path unless you intend to map paths like/engineering/backend.
Any other provider
Nothing here is Microsoft-specific. Any provider that publishes a standard discovery document works: register a confidential web client with the redirect URI from step 1, collect the client ID and secret, and supply the discovery address. The only two requirements are that the ID token carries a stable subject identifier and an email address, and — if you want role mapping — a claim named groups.
Step 3 — Finish in Vaks PM
Back in the provider form, complete the remaining fields:
| Field | Value |
|---|---|
| Microsoft Entra — Directory (tenant) ID | For Entra: the tenant GUID, a verified domain, or organizations. The discovery URL is built from it and shown below the field. |
| OIDC discovery URL | For every other provider: follow the Use a custom OIDC discovery URL link and paste the address from step 2 (https://accounts.google.com, your Okta authorization server, your Keycloak realm). Must be HTTPS. |
| Client ID (application ID) | From step 2. |
| Authentication | Client secret (default) or Certificate — see the certificate alternative for Entra. Both methods are encrypted at rest and never displayed again after saving; leave fields blank when editing to keep what is already stored. |
| Scopes | Space- or comma-separated extras. openid is always included; leaving this empty requests openid profile email, which is what you want in almost every case. |
| Default role (JIT) | The role given to a newly created user who matches no group mapping. |
| Unknown user (no matching objectId) | Create the account automatically or Refuse the sign-in. See below. |
The choice on unknown users is the one with real consequences. Create automatically provisions the account on first sign-in and consumes a licence seat. Refuse the sign-in means only accounts that already exist can authenticate — the safer setting when seats are limited, or when you provision through SCIM and want the directory to remain the only source of accounts.
Step 4 — Map groups to roles
Under Group → role mappings, add one row per group: the group's identifier on the left, the Vaks PM role on the right. On Entra the identifier is the group's object ID; on Okta and Keycloak it is the group name as emitted in the claim.
Two rules govern evaluation:
- Order is priority. A user has exactly one organization role, so the first matching row from the top wins. Use the arrows to put your most privileged groups first. Matching is exact string equality — no wildcards, no case-insensitivity.
- No match means the default role for a new account, and no change at all for an existing one.
The checkbox Identity provider is authoritative for roles extends this to demotion: on every sign-in, a user matching no mapping is reset to the default role. Leave it off if you grant elevated roles by hand inside Vaks PM, since it will undo them.
Step 5 — Test, then enable
Save the provider, then press Test on its row. For OIDC this performs a live discovery fetch and reports the issuer it found, the client ID and whether a secret is stored. It confirms that the discovery URL is reachable and coherent — it does not attempt a sign-in.
Then tick Enabled (shown on the login screen) and save. The button appears on the login page.
Verify with a real sign-in, ideally in a private browser window and with a pilot user rather than your own administrator account:
- The user reaches the provider, authenticates, and lands back in Vaks PM signed in.
- In Admin → Users, the account shows the expected role — confirming the groups claim arrived and matched.
- The account holds a licence seat. A newly created user does not necessarily get one, and without a seat they sign in successfully but see an empty application — which looks like a failed setup and is not. Grant the seat here, or use a team with automatic licensing.
- The audit log holds a
auth.sso.loginentry.
Troubleshooting
Sign-in errors are shown on the login page, in the language your browser requests — the user is not signed in yet, so no interface preference exists. They are reproduced below in English.
| Symptom | Cause & fix |
|---|---|
| An account with this email already exists but is not linked to this IdP identity (objectId) … | A local account already holds that email but was never linked to this identity. Deliberate — accounts are never taken over by email. Set the account's external identifier to the provider's object ID, or delete the account permanently and let it be recreated. |
| No account matches this identity (objectId) and auto-provisioning is disabled. | Automatic creation is off and no account matches. Either create the account first (by hand or via SCIM), or switch the unknown-user setting to automatic creation. |
| L'id_token ne contient pas d'identifiant ou d'email exploitable. | The token carries no usable email. Add the email scope or claim on the provider side; on Entra, confirm the account actually has a mail address. |
| Invalid or expired SSO session state. | More than 10 minutes elapsed, the URL was replayed, or Redis is unavailable. Retry from a clean login page; if it persists, check Redis. |
| IdP response validation failed. | The token failed validation. The real reason is deliberately not shown in the browser — read the api container logs, which record it. |
| Incomplete OIDC provider (discovery/clientId missing). | The provider was enabled with a field missing. Reopen and complete it. |
| OIDC single sign-on requires an active license. | Configuration is allowed without a licence, sign-in is not. Import the licence under Admin → Organization → License. |
| Everyone lands on the default role | The groups claim is not arriving. On Entra, check Token configuration includes it in the ID token; on Okta and Keycloak, check the claim is named exactly groups. On Google Workspace this is expected — groups are not emitted at all. |
| Sign-in works, but the user sees nothing after landing | The account has no licence seat. Grant one in Admin → Users, or use a team with automatic licensing. |
What is logged
Under Admin → Security & Compliance → Audit log: auth.sso.login on every federated sign-in, recording the provider and whether the account was created; auth.sso.failure with the refusal reason; user.provisioned.sso when an account is created, with the role applied; and idp.created / idp.updated / idp.deleted for configuration changes, all at critical severity. Client secrets never appear in the audit log.
Signature and token-validation failures are deliberately vague in the browser to avoid giving an attacker a probing oracle. Their real cause is in the api container logs.
Related: SAML single sign-on if your provider does not speak OIDC · SCIM provisioning to create and retire accounts automatically · all integrations.