Skip to content
Merged
Changes from all commits
Commits
Show all changes
155 commits
Select commit Hold shift + click to select a range
70833fb
OIDC resource server: configuration and a protected resource registry…
KrzysztofPajak Oct 8, 2026
0f66cdd
OIDC resource server: canonical resource URIs, audience, authorized p…
KrzysztofPajak Oct 8, 2026
cfd1888
OIDC resource server: RFC 6750 challenges and RFC 9728 protected reso…
KrzysztofPajak Oct 8, 2026
e4fe29d
OIDC resource server: Oidc JwtBearer scheme, startup validation and m…
KrzysztofPajak Oct 8, 2026
ef88aee
OIDC resource server: pipeline tests with an in-memory signing key
KrzysztofPajak Oct 8, 2026
7b1cc26
OIDC resource server: store URL as audience base, fail-closed account…
KrzysztofPajak Oct 8, 2026
ce114a8
OIDC resource server: map the token to a customer by subject link, ve…
KrzysztofPajak Oct 8, 2026
12e5be9
OIDC resource server: admin panel, vendor and store manager condition…
KrzysztofPajak Oct 8, 2026
6768e2b
OIDC resource server: always write the ProblemDetails body of challen…
KrzysztofPajak Oct 9, 2026
f1ed29a
OIDC resource server: the token's customer becomes the customer of th…
KrzysztofPajak Oct 11, 2026
0257877
External authentication: unique index on provider and external identi…
KrzysztofPajak Oct 8, 2026
11eb95f
External authentication: identity link index unique only on MongoDB
KrzysztofPajak Oct 8, 2026
424a487
OIDC resource server: refuse system, non-registered, other-store and …
KrzysztofPajak Oct 8, 2026
98ed651
External authentication: installer creates the identity link index as…
KrzysztofPajak Oct 8, 2026
6752457
OIDC resource server: link name of the login plugin, automatic regist…
KrzysztofPajak Oct 10, 2026
677e160
Web API: accept access tokens of the OpenID Connect provider next to …
KrzysztofPajak Oct 8, 2026
e0ecb69
Web API: never fail on a malformed bearer token while routing to the …
KrzysztofPajak Oct 8, 2026
90ad302
API users with SecretKey tokens marked obsolete in favour of the OIDC…
KrzysztofPajak Oct 8, 2026
e05657f
Web API: BackendAPI switch applies to provider tokens; route asymmetr…
KrzysztofPajak Oct 8, 2026
e89211b
Docs: OIDC resource server guide with Keycloak and Zitadel, test envi…
KrzysztofPajak Oct 8, 2026
6d8dc2e
Web API: treat a bearer value that is not a JWT as no token
KrzysztofPajak Oct 9, 2026
afe6fef
OIDC: browser sign-in mode of the customer resolver - never registers…
KrzysztofPajak Oct 10, 2026
e663216
External registration: pending provider identity in the external auth…
KrzysztofPajak Oct 10, 2026
2f9f086
Docs: OIDC guide - Zitadel scope only for entitled users, compose har…
KrzysztofPajak Oct 8, 2026
db5cbbb
Login: keep a local return URL across the two-factor step
KrzysztofPajak Oct 10, 2026
937e440
Docs: OIDC guide - fixes from the end-to-end run against Keycloak 26.…
KrzysztofPajak Oct 8, 2026
ffeadc7
Registration: complete a provider sign-in on the regular form, e-mail…
KrzysztofPajak Oct 10, 2026
fbacc24
Docs: OIDC guide - a bearer value that is not a JWT gets the OIDC cha…
KrzysztofPajak Oct 9, 2026
3103099
Browser sign-in: the first link of an account with two-factor authent…
KrzysztofPajak Oct 10, 2026
67f73cb
Docs: OIDC guide - automatic registration is off by default
KrzysztofPajak Oct 10, 2026
40bd857
OpenID Connect login plugin: provider, settings, sign-in button and a…
KrzysztofPajak Oct 10, 2026
fc3a1b2
OpenID Connect login plugin: code flow with PKCE, provider identity k…
KrzysztofPajak Oct 10, 2026
241ce52
OpenID Connect login plugin: ship only the handler package, not assem…
KrzysztofPajak Oct 10, 2026
4a289a4
OpenID Connect login plugin: absolute lifetime of the pending identit…
KrzysztofPajak Oct 10, 2026
7b1858d
OpenID Connect login plugin: sign-in callback, connecting an existing…
KrzysztofPajak Oct 10, 2026
5419b64
Docs: OIDC guide - browser sign-in with the OpenID Connect login plugin
KrzysztofPajak Oct 10, 2026
4f53226
OpenID Connect login plugin: connecting an account always needs a fre…
KrzysztofPajak Oct 10, 2026
223eab0
Docs: OIDC guide - browser sign-in accuracy fixes
KrzysztofPajak Oct 10, 2026
c65592a
My account: removing an external sign-in connection works again
KrzysztofPajak Oct 10, 2026
a207a0e
OpenID Connect login plugin: warning when not configured, connecting …
KrzysztofPajak Oct 10, 2026
b8a6beb
Docs: OIDC guide - admin approval, two-factor customers, Zitadel e-ma…
KrzysztofPajak Oct 10, 2026
db6434f
OIDC resource server: resolve the audience store from the request hos…
KrzysztofPajak Oct 11, 2026
f51fb29
OIDC resource server: RFC 9728 metadata URL with the well-known segme…
KrzysztofPajak Oct 11, 2026
c611acd
OIDC resource server: 404 and a warning instead of a metadata documen…
KrzysztofPajak Oct 11, 2026
7f1a4a8
Skip store and work context setup on the protected resource metadata …
KrzysztofPajak Oct 11, 2026
30741b7
OIDC resource server: answer 503 instead of invalid_token when the au…
KrzysztofPajak Oct 11, 2026
1101972
OIDC resource server: treat a negative ClockSkewSeconds as zero inste…
KrzysztofPajak Oct 11, 2026
c0d3952
OIDC resource server: build the registry's read-only resource list once
KrzysztofPajak Oct 11, 2026
ea2c20e
OIDC resource server: leave enablement of the metadata endpoints to t…
KrzysztofPajak Oct 11, 2026
5af62c3
OIDC resource server tests: dispose request messages and RSA instances
KrzysztofPajak Oct 11, 2026
e084407
OIDC resource server: filter explicitly in the header escaping and re…
KrzysztofPajak Oct 11, 2026
fcd4cba
Merge feature/oidc-resource-server into feature/oidc-customer-mapping…
KrzysztofPajak Oct 11, 2026
0605fc6
Merge feature/oidc-module-api into feature/oidc-guides-e2e: review fi…
KrzysztofPajak Oct 11, 2026
c607833
Merge feature/oidc-customer-mapping into feature/oidc-module-api: rev…
KrzysztofPajak Oct 11, 2026
6c10d52
OIDC guide: audience store from the host, RFC 9728 metadata URLs, 503…
KrzysztofPajak Oct 11, 2026
3c6291a
Merge feature/oidc-guides-e2e into feature/oidc-login-core: review fi…
KrzysztofPajak Oct 11, 2026
0634eff
Merge feature/oidc-login-core into feature/oidc-login-plugin: review …
KrzysztofPajak Oct 11, 2026
35ab7be
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: re…
KrzysztofPajak Oct 11, 2026
be60cf3
OIDC resource server: build the work context in the store the access …
KrzysztofPajak Oct 11, 2026
964f800
Merge develop into feature/oidc-customer-mapping: #891 squash-merged
KrzysztofPajak Oct 11, 2026
ebb689f
Merge feature/oidc-customer-mapping into feature/oidc-module-api: wor…
KrzysztofPajak Oct 11, 2026
2e93760
Merge feature/oidc-module-api into feature/oidc-guides-e2e: work-cont…
KrzysztofPajak Oct 11, 2026
db68499
OIDC Web API tests: a token forwarded from the Bearer scheme pins the…
KrzysztofPajak Oct 11, 2026
1ee791b
OIDC guide: a request with an accepted token runs in the token's stor…
KrzysztofPajak Oct 11, 2026
3d30852
Merge feature/oidc-guides-e2e into feature/oidc-login-core: work-cont…
KrzysztofPajak Oct 11, 2026
4063795
Merge feature/oidc-login-core into feature/oidc-login-plugin: work-co…
KrzysztofPajak Oct 11, 2026
dc1f598
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: wo…
KrzysztofPajak Oct 11, 2026
3495de8
OIDC: match the request store by host and port before host alone
KrzysztofPajak Oct 11, 2026
50822d7
OIDC: no root protected resource metadata for a store under a path
KrzysztofPajak Oct 11, 2026
f204ad8
OIDC: log an unknown metadata host at debug, a store without URL as w…
KrzysztofPajak Oct 11, 2026
282522d
Context middleware: skip the protected resource metadata route exactly
KrzysztofPajak Oct 11, 2026
2eb1441
OIDC: Retry-After on the 503 for an unreachable identity provider
KrzysztofPajak Oct 11, 2026
f42b5f2
OIDC tests: recovery after an identity provider outage
KrzysztofPajak Oct 11, 2026
f9c0804
OIDC tests: dispose responses and readers
KrzysztofPajak Oct 11, 2026
9bfdb88
Merge feature/oidc-module-api into feature/oidc-guides-e2e: review fo…
KrzysztofPajak Oct 11, 2026
6333561
Merge feature/oidc-customer-mapping into feature/oidc-module-api: rev…
KrzysztofPajak Oct 11, 2026
6161803
OIDC guide: forwarded headers behind TLS termination, host:port store…
KrzysztofPajak Oct 11, 2026
7ab2f19
Merge feature/oidc-guides-e2e into feature/oidc-login-core: review fo…
KrzysztofPajak Oct 11, 2026
6f54eb6
Merge feature/oidc-login-core into feature/oidc-login-plugin: review …
KrzysztofPajak Oct 11, 2026
ac721a5
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: re…
KrzysztofPajak Oct 11, 2026
ef527ce
OIDC tests: dispose responses in the forwarding pipeline tests
KrzysztofPajak Oct 11, 2026
2355fd3
Merge feature/oidc-module-api into feature/oidc-guides-e2e: review fo…
KrzysztofPajak Oct 11, 2026
35630d9
Merge feature/oidc-guides-e2e into feature/oidc-login-core: review fo…
KrzysztofPajak Oct 11, 2026
e607445
Merge feature/oidc-login-core into feature/oidc-login-plugin: review …
KrzysztofPajak Oct 11, 2026
88a358f
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: re…
KrzysztofPajak Oct 11, 2026
537a986
External authentication: store of the linked customer
KrzysztofPajak Oct 11, 2026
9b22008
OIDC: one identity link per store when customers are registered per s…
KrzysztofPajak Oct 11, 2026
6a381c3
External authentication index: provider, identifier and store
KrzysztofPajak Oct 11, 2026
9b08fed
Merge feature/oidc-customer-mapping into feature/oidc-module-api: per…
KrzysztofPajak Oct 11, 2026
c1dae96
Merge feature/oidc-module-api into feature/oidc-guides-e2e: per-store…
KrzysztofPajak Oct 11, 2026
459e983
OIDC guide: identity links per store
KrzysztofPajak Oct 11, 2026
885a9cf
Merge feature/oidc-guides-e2e into feature/oidc-login-core: per-store…
KrzysztofPajak Oct 11, 2026
72f4ebd
OIDC sign-in: link the identity in the current store
KrzysztofPajak Oct 11, 2026
eeb9f7c
Merge feature/oidc-login-core into feature/oidc-login-plugin: per-sto…
KrzysztofPajak Oct 11, 2026
ba1b9ec
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: pe…
KrzysztofPajak Oct 11, 2026
ff8129d
OIDC sign-in guide: identity links per store
KrzysztofPajak Oct 11, 2026
28609f5
OIDC: a link without a store to another store's customer does not blo…
KrzysztofPajak Oct 11, 2026
f76de76
Merge feature/oidc-customer-mapping into feature/oidc-module-api: per…
KrzysztofPajak Oct 11, 2026
e189dfb
Merge feature/oidc-module-api into feature/oidc-guides-e2e: per-store…
KrzysztofPajak Oct 11, 2026
449b585
Merge feature/oidc-guides-e2e into feature/oidc-login-core: per-store…
KrzysztofPajak Oct 11, 2026
4e58b16
OIDC guide: links without a store under per-store identity
KrzysztofPajak Oct 11, 2026
37d6ffd
OIDC sign-in tests: links without a store under per-store identity
KrzysztofPajak Oct 11, 2026
5264db1
Merge feature/oidc-login-core into feature/oidc-login-plugin: per-sto…
KrzysztofPajak Oct 11, 2026
973989c
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: pe…
KrzysztofPajak Oct 11, 2026
a511da8
External authentication: find a linked identity by its exact identifier
KrzysztofPajak Oct 11, 2026
ac3cb12
Merge feature/oidc-customer-mapping into feature/oidc-module-api: exa…
KrzysztofPajak Oct 11, 2026
492b20e
Merge feature/oidc-module-api into feature/oidc-guides-e2e: exact ide…
KrzysztofPajak Oct 11, 2026
13119ec
Merge feature/oidc-guides-e2e into feature/oidc-login-core: exact ide…
KrzysztofPajak Oct 11, 2026
2935cc9
Merge feature/oidc-login-core into feature/oidc-login-plugin: exact i…
KrzysztofPajak Oct 11, 2026
8231086
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: ex…
KrzysztofPajak Oct 11, 2026
41bb378
Enforce one OIDC identity per customer with a customer-derived link id
KrzysztofPajak Oct 11, 2026
10f54e2
Merge feature/oidc-customer-mapping into feature/oidc-module-api: one…
KrzysztofPajak Oct 11, 2026
065a26e
Merge feature/oidc-module-api into feature/oidc-guides-e2e: one OIDC …
KrzysztofPajak Oct 11, 2026
575712e
Document that the link id enforces one OIDC identity per customer
KrzysztofPajak Oct 11, 2026
cdffe55
Merge feature/oidc-guides-e2e into feature/oidc-login-core: one OIDC …
KrzysztofPajak Oct 11, 2026
e7b7f8b
Test that browser sign-in linking keeps one OIDC identity per custome…
KrzysztofPajak Oct 11, 2026
fae445a
Merge feature/oidc-login-core into feature/oidc-login-plugin: one OID…
KrzysztofPajak Oct 11, 2026
3709a1d
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: on…
KrzysztofPajak Oct 11, 2026
dc0bcb2
Merge develop into feature/oidc-module-api
KrzysztofPajak Oct 11, 2026
d84f81b
API users: trust only tickets the Bearer scheme issued itself, not re…
KrzysztofPajak Oct 11, 2026
3d544c3
OIDC: forward only the Web API's Bearer scheme to Oidc, not every Jwt…
KrzysztofPajak Oct 11, 2026
6d1b565
OIDC scheme selector: drop the Enabled check it is never registered w…
KrzysztofPajak Oct 11, 2026
f768c12
OIDC scheme selector: route on the signing algorithm alone, without t…
KrzysztofPajak Oct 11, 2026
6ea8c1e
OIDC scheme selector: route by the Oidc scheme's own algorithm list i…
KrzysztofPajak Oct 11, 2026
09824fe
OIDC scheme selector: parse the bearer token once and keep the routin…
KrzysztofPajak Oct 11, 2026
789718c
OIDC request customer resolver: follow the scheme selector's decision…
KrzysztofPajak Oct 11, 2026
4c70e34
OIDC: withdraw the Web API's protected resource while BackendAPI is d…
KrzysztofPajak Oct 11, 2026
abfa8ae
Web API: apply the BackendAPI kill switch before the ignore flag of A…
KrzysztofPajak Oct 11, 2026
13fb897
appsettings: deny vendors and store managers on the Web API's protect…
KrzysztofPajak Oct 11, 2026
535e85c
Web API tests: join the temp settings path instead of combining it
KrzysztofPajak Oct 11, 2026
ac081ce
Merge feature/oidc-module-api into feature/oidc-guides-e2e: review fi…
KrzysztofPajak Oct 11, 2026
0eefaf5
OIDC guide: routing by algorithm only, Web API kill switch before cus…
KrzysztofPajak Oct 11, 2026
b386fd5
Merge feature/oidc-guides-e2e into feature/oidc-login-core: review fi…
KrzysztofPajak Oct 11, 2026
db4802c
Merge feature/oidc-login-core into feature/oidc-login-plugin: review …
KrzysztofPajak Oct 11, 2026
701f95c
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: re…
KrzysztofPajak Oct 11, 2026
03a26bb
Merge remote-tracking branch 'origin/develop' into feature/oidc-guide…
KrzysztofPajak Oct 11, 2026
8ca7b93
Merge feature/oidc-guides-e2e into feature/oidc-login-core: develop a…
KrzysztofPajak Oct 11, 2026
a46e3fe
Merge feature/oidc-login-core into feature/oidc-login-plugin: develop…
KrzysztofPajak Oct 11, 2026
e012d08
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: de…
KrzysztofPajak Oct 11, 2026
8dbf4c9
OIDC guide: wrap long lines, Client scopes menu name; resource URI co…
KrzysztofPajak Oct 11, 2026
99ce77e
Merge branch 'feature/oidc-guides-e2e' into feature/oidc-login-core
KrzysztofPajak Oct 11, 2026
6acee48
Merge branch 'feature/oidc-login-core' into feature/oidc-login-plugin
KrzysztofPajak Oct 11, 2026
3df3575
Merge branch 'feature/oidc-login-plugin' into feature/oidc-login-guid…
KrzysztofPajak Oct 11, 2026
067a7db
OIDC login: check the method's availability at every step, ask for th…
KrzysztofPajak Oct 11, 2026
748e30a
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: re…
KrzysztofPajak Oct 11, 2026
0972513
OIDC guide: the provider sign-in replaces the password only, GrandNod…
KrzysztofPajak Oct 11, 2026
bbd1ca7
External registration: connect only a verified e-mail, warn when conn…
KrzysztofPajak Oct 11, 2026
836c489
Merge remote-tracking branch 'origin/feature/oidc-login-core' into fe…
KrzysztofPajak Oct 11, 2026
9d5ab4a
Merge remote-tracking branch 'origin/feature/oidc-login-plugin' into …
KrzysztofPajak Oct 11, 2026
49f3c21
Merge develop into feature/oidc-login-plugin: #894 and #895 merged
KrzysztofPajak Oct 11, 2026
562b8d0
OIDC login: the provider sign-in is the sign-in (no store two-factor …
KrzysztofPajak Oct 11, 2026
8905be6
Merge feature/oidc-login-plugin into feature/oidc-login-guide-e2e: re…
KrzysztofPajak Oct 11, 2026
fb2eaa6
OIDC guide: the provider sign-in is the whole sign-in (no GrandNode t…
KrzysztofPajak Oct 11, 2026
344c615
Merge remote-tracking branch 'origin/develop' into feature/oidc-login…
KrzysztofPajak Oct 11, 2026
03df14d
OIDC guide: registration with an unverified e-mail asks for a passwor…
KrzysztofPajak Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 177 additions & 5 deletions docs/oidc-resource-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ identity provider and never issues tokens, registers clients or shows a consent
* GrandNode publishes RFC 9728 protected resource metadata, so clients discover which provider to use. A 401 carries
a `WWW-Authenticate` header with `resource_metadata`.
* Off by default (`OidcResourceServer:Enabled = false`): no scheme, no metadata endpoints, no behavior change.
* One identity provider at a time. Not covered: browser login of storefront or admin panel users, opaque tokens
(see [Introspection](#8-introspection)).
* One identity provider at a time. Not covered here: opaque tokens (see [Introspection](#8-introspection)) and
login to the admin panels. Browser sign-in of storefront users is a separate plugin
([section 11](#11-browser-sign-in-openid-connect-login-plugin)).

Accepted signing algorithms: RS256/384/512, PS256/384/512, ES256/384/512. Tokens signed with HS\*, or with `alg: none`,
are refused by the OpenID Connect scheme.
Expand Down Expand Up @@ -432,9 +433,9 @@ Steps:
exists or is registered, is in a group with *Access admin panel* and the needed permissions. Step 4 is only as strong
as who may grant the project role or edit Zitadel Actions. If you do not run the target, leave `RequiredScopes`
**empty** for Zitadel (a required scope would reject every token) and rely on (1)-(3) and (5); then any user of the
instance who can log in to an allowed client reaches the GrandNode permission check, and a user that is
auto-registered holds the Registered group only, which has no admin panel access. `AutoRegister` is off by default;
turn it on only when every user of the provider may get a store account.
instance who can log in to an allowed client reaches the GrandNode permission check. With `AutoRegister` enabled, a
user registered automatically holds the Registered group only, which has no admin panel access. `AutoRegister` is off
by default; turn it on only when every user of the provider may get a store account.
5. **Dynamic client registration** (RFC 7591) is **off by default** in Zitadel. Two modes: with a token of a user
holding the `project.app.register_dynamic` permission, or open (`dynamicClientRegistration.allowUnauthenticated =
true`), which MCP-class clients need. See <https://zitadel.com/docs/guides/integrate/dynamic-client-registration>.
Expand Down Expand Up @@ -497,6 +498,177 @@ Ports are published on `127.0.0.1` only.
Use the URLs above as `Authority` (`http://localhost:8080/realms/<realm>` for Keycloak, `http://localhost:8081` for
Zitadel) with `RequireHttpsMetadata: false` in Development.

## 11. Browser sign-in (OpenID Connect login plugin)

The plugin `Authentication.OpenIdConnect` (system name `ExternalAuth.OpenIdConnect`) lets customers and staff sign in
to the storefront with the same provider that issues the API tokens. It is separate from the resource server: the
resource server validates access tokens, the plugin runs the browser flow (authorization code with PKCE) and gives the
visitor a normal GrandNode session. The provider proves who the user is; the customer's **groups and permissions in
GrandNode** still decide what the user may do.

### 11.1 What the visitor sees

* A **sign-in button** next to the other external authentication methods.
* The provider and its claim names are the ones of `OidcResourceServer` (`Authority`, `RequireHttpsMetadata`,
`Claims:*`). The plugin adds one client of its own (section 11.3). `RequireEmailVerified` applies to API tokens
only; browser sign-in links an account automatically only when the provider reports the e-mail as verified,
whatever that setting says.
* The link between the provider's `sub` and the customer is stored in the same place as for API tokens (provider name
`ExternalAuth.OpenIdConnect`). An account linked by browser sign-in is the account API tokens map to, and the other
way round.
* "My account" lists the connection and lets the customer remove it. After removing it, the button leads to linking
again. The account always has a password (random until the customer sets one with "Forgot password"), so removing the
connection never locks the customer out.
* Routes used: `/oidc/login`, `/signin-oidc` (callback of the OpenID Connect handler), `/oidc/callback`, `/oidc/link`,
`/oidc/link/complete`, `/oidc/failed`.

### 11.2 Account rules

After the provider signed the user in, the result depends on the GrandNode side:

| Situation | Result |
|---|---|
| A link for the provider's `sub` exists | The linked customer is signed in. Staff included; the admin panel access follows their existing permissions. The customer must be active, not deleted and registered. GrandNode two-factor authentication is not asked for; the provider's multi-factor policy applies. |
| No link; an account with the same e-mail exists, the provider **verified** the e-mail, the account is **not staff** and does not use GrandNode two-factor authentication | The account is linked and signed in at once. |
| No link; the account is **staff** (admin panel access, vendor, store staff), the account uses **GrandNode two-factor authentication** (enabled for the customer and in the store), or the provider did **not** verify the e-mail | "Connect your account": the page ends any current store session, and the visitor signs in **once** with the store password (and the two-factor code, when enabled). The connection is then completed (`/oidc/link/complete`); the signed-in account must have the same e-mail address. |
| No account with that e-mail | The regular registration form opens with the e-mail taken from the provider (read-only). When the provider **verified** the e-mail, the form has **no password fields** and the new account is connected to the provider identity. When it did **not**, the customer chooses a password as in any registration and the account is **not** connected; the customer connects it later with "Connect your account" (row above). Store registration type "Standard" becomes e-mail validation when the provider did not verify the e-mail; "E-mail validation" becomes standard when it did; "Admin approval" stays admin approval either way. New accounts get the **Registered** group only. |
| Registration disabled in the store, account inactive, e-mail already linked to another identity, provider identity already linked to another account, the sign-in method switched off (or limited to other stores or groups) while the sign-in was in progress, another account already signed in to the store | The failure page (fixed text; the reason is written to the log). |

Notes:

* The provider's identity is kept for **15 minutes** (absolute) in the `ExternalAuthentication` cookie, which is not
persistent. It only decides who is registering or connecting; an e-mail value posted with the registration form is
ignored.
* A provider that sends `email_verified` as `"True"`, `true` or not at all is handled: the comparison ignores case and a
missing value means unverified.
* **"Admin approval" and an unverified e-mail.** The account waits for approval and GrandNode sends no activation
e-mail (its activation link would approve the account), so GrandNode does not validate an e-mail address the
provider reported as unverified. Administrators should check the address before approving such an account.
* A customer who uses GrandNode two-factor authentication connects the provider identity **once** with the store
password and the two-factor code, like staff; after that the provider sign-in alone signs the customer in (the
provider's multi-factor policy applies, not GrandNode two-factor authentication).
* With `RegisterCustomersPerStore` enabled all lookups are limited to the store of the request, and a link belongs to
the store of the linked account (section 3.1): the same provider identity can be connected to a separate account in
each store, while an account of no store (such as staff) keeps one link that works in every store.
* Provider tokens are **not stored**.
* The Web API setting `OidcResourceServer:AutoRegister` is `false` by default (section 3.2); browser sign-in registers
new users through the registration form instead.

### 11.3 Configuration

1. Build the plugin and install it in the admin panel (*Configuration > Plugins*). The plugin output contains only its
own assembly and `Microsoft.AspNetCore.Authentication.OpenIdConnect.dll`; it uses the `Microsoft.IdentityModel.*`
assemblies of the application.
2. Enable it in *Configuration > External authentication*. The display order of the button is set on the plugin's
configuration page.
3. Configure the provider (section 11.4 or 11.5) and add the `OidcLogin` section. The secret goes into an environment
variable, never into the database or the admin panel.

Section `OidcLogin` of `appsettings.json`:

| Key | Default | Meaning |
|---|---|---|
| `ClientId` | empty | Client id of the **confidential** client created for browser sign-in. |
| `ClientSecret` | empty | Its secret. Prefer the environment variable `OidcLogin__ClientSecret`. |
| `Scopes` | `openid profile email` | Space separated scopes requested at the provider. `openid` and `email` are needed. |

```json
"OidcResourceServer": {
"Authority": "https://idp.example.com/realms/grandnode",
"RequireHttpsMetadata": true
},
"OidcLogin": {
"ClientId": "grandnode-login",
"ClientSecret": "",
"Scopes": "openid profile email"
}
```

`Authority`, `RequireHttpsMetadata` and the claim names come from `OidcResourceServer`. `OidcResourceServer:Enabled`
does not have to be on for browser sign-in; the plugin only reads `Authority` and the other values above. It does not
read `RequireEmailVerified`: automatic linking always needs an e-mail the provider reports as verified.

**Not configured.** When `Authority` or `ClientId` is empty the plugin registers no sign-in scheme and the button leads
to the failure page; it never causes an error page.

Handler settings (fixed): authorization code flow with PKCE (S256), response mode `query`, `sub`, e-mail,
`email_verified`, `given_name` and `family_name` are also read from the **user info endpoint**, provider tokens are not
saved, claims are not renamed. State and nonce are checked by the handler.

The redirect URI of the client is `https://<store-url>/signin-oidc` for **every** store URL (each store of a
multi-store installation, each host name).

### 11.4 Keycloak 26.8

Use the realm of section 6. Create a **separate** client; do not reuse the public client of the API.

1. *Clients > Create client*: client type OpenID Connect, client id `grandnode-login`.
2. Capability config: *Client authentication* **on** (confidential), *Standard flow* **on**; the other flows can stay
off.
3. Login settings: *Valid redirect URIs* `https://<store-url>/signin-oidc` (one entry per store URL), *Web origins*
the store origin `https://<store-url>`.
4. *Advanced > Advanced settings*: set *Proof Key for Code Exchange Code Challenge Method* to `S256`.
5. *Credentials* tab: copy the client secret into `OidcLogin__ClientSecret`.
6. The default `profile` and `email` client scopes are assigned to a new client; keep them. `email_verified` is part of
the ID token and of the user info answer by default. Users need a verified e-mail address (*Users > Email
verified*, or enable *Verify email* in the realm login settings) to be linked automatically.
7. Enforce MFA for staff in the realm (section 11.6).

### 11.5 Zitadel v4.19

Use the project of section 7. Create a **separate** application for the browser flow.

1. In the project add an application of type **Web** with authentication method **Code** (authorization code with a
client secret). PKCE is sent by the plugin. Copy the client id and the generated client secret
(`OidcLogin__ClientSecret`; the secret is shown once).
2. Redirect URIs: `https://<store-url>/signin-oidc` (an `http` redirect needs *Development Mode*). Post logout URIs
are not used (logout is local).
3. The plugin reads the claims from the ID token or, when they are absent there, from the user info endpoint (the
handler merges both); Zitadel answers the user info endpoint for the scope `email`. To have the ID token carry the
user information as well, enable the application's option to put user information into the ID token (token
settings of the application). The default scopes `openid profile email` are sufficient.
4. **The Actions v2 target of section 7 is not required for browser sign-in.** It runs on `preaccesstoken`, that is for
access tokens only, and the plugin does not use the access token. Verify this on your instance: after a test
sign-in with a verified e-mail the application log must not contain "OpenID Connect sign-in without a subject or an
e-mail". If it does, enable the ID token option of step 3 and check that the user's e-mail is marked verified in
Zitadel (an unverified e-mail is sent with `email_verified = false`).
5. Users need a verified e-mail address in Zitadel to be linked automatically. Zitadel's hosted login asks a user
whose e-mail is not verified to confirm it with a code before it completes the sign-in, so `email_verified` is
normally `true` for sign-ins through Zitadel. Enforce MFA for staff in the login policy of the organization
(section 11.6).

### 11.6 Security notes

* **After linking, the provider sign-in is the whole sign-in.** A linked account, staff included, signs in without the
GrandNode password and **without GrandNode two-factor authentication**: the security of the sign-in is the
provider's. Enforce MFA at the provider for every user who may be linked to a staff account. The first link of a
staff account always needs the store password (and its 2FA).
* The button never replaces another signed-in account: a visitor signed in to the store as someone else gets the
failure page and stays signed in.
* A staff account is never linked by e-mail alone, but the one who completes the password sign-in and holds the
provider identity owns the link; keep the provider's e-mail verification and e-mail change policy strict (same advice
as section 3.1).
* **Logout is local.** Signing out of GrandNode does not end the provider session; the next click on the button signs
in again without a prompt while the provider session lasts.
* Provider tokens are not stored. The only data kept from the provider is the `sub`/e-mail link, plus the pending
identity for at most 15 minutes.
* After sign-in only a **local** `returnUrl` is followed; anything else leads to the store home page.
* New accounts created through this path get the Registered group only.
* Keep `ClientSecret` out of source control: environment variable or secret store.

### 11.7 Troubleshooting

| Symptom | Check |
|---|---|
| The button is not shown | `OidcResourceServer:Authority` and `OidcLogin:ClientId` set? The plugin installed and enabled in *External authentication*? The application restarted after the install or a settings change? Without both settings, or before the restart, no sign-in scheme exists and the button is hidden. |
| "Sign-in failed" after the provider page | The reason is in the application log (`OpenID Connect sign-in failed`, with the provider's error), never on the page. Common: wrong client secret, the user cancelled at the provider, the state/nonce cookie lost (a proxy, or a host name different from the one that started the flow). |
| Provider error about the redirect URI | The redirect URI at the provider must equal `https://<store-url>/signin-oidc` exactly: scheme, host, port, no trailing slash. Behind a TLS-terminating proxy GrandNode must see `https` (forwarded headers) or it builds an `http` URI. |
| Failure caused by token lifetime ("not yet valid", "expired") | Clock skew between the application server and the provider; synchronize the clocks (NTP). |
| Discovery document cannot be loaded | `Authority` must equal the `issuer` of `{Authority}/.well-known/openid-configuration`; `http` needs `RequireHttpsMetadata: false` (Development only); in container setups use a host name both the application and the browser resolve. |
| Visitor is sent to the registration form although the account exists | The e-mail differs, or `RegisterCustomersPerStore` limits lookups to another store. |
| Visitor is asked to connect accounts though the e-mail is verified | The account is staff or uses GrandNode two-factor authentication, or the provider sends `email_verified` as false or not at all (for Zitadel check the user info answer). |
| After sign-in the visitor lands on the home page, not on the requested page | The `returnUrl` was not local. |

Sources used for provider specifics (checked 2026-10-08): Keycloak 26.8 MCP guide and release notes, Zitadel v4.19.4
claims, scopes, Actions v2 and dynamic client registration documentation; MCP authorization 2026-07-28; RFC 9728,
RFC 8707, RFC 6750, RFC 7591.
Loading