From 1728297d3c993b0f6c3e90f8f422de51204e7c04 Mon Sep 17 00:00:00 2001 From: Markus Heinemann Date: Mon, 4 Aug 2025 13:24:54 +0200 Subject: [PATCH 1/3] docs(oidc-playground): update scopes and default instance domain (#9995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved This PR resolves #4845 by enhancing the OIDC Playground: * set default instance domain to `http://localhost:8080` * openid checkbox is now disabled * add explanation texts for custom zitadel scopes # How the Problems Are Solved * The checkbox for the `openid` scope is set to `disabled` * The default value for the instance domain is update by using `setInstance` * A new map with explanation texts for the custom scopes is introduced. During the rendering process of the scope checkboxes the value from this map is displayed, if the scope exists as key. # Additional Changes During the local setup of the documentation webapp I got some react errors on the authrequest page. This issue has ben solved by refactoring the usage of an `useEffect` block. # Additional Context - Closes #4845 PS. I did not found any scripts for linting/formatting (e.g. eslint, prettier) for the docs project. This is a bit annoying because when I use my local configurations of eslint/prettier the whole file get's refactored with unnecessary changes (change of import order, indention etc.). It would be great to add some custom configurations to to make the development process easier and enforce a consistent coding style :) Co-authored-by: Markus Heinemann Co-authored-by: Tim Möhlmann --- docs/src/components/authrequest.jsx | 25 ++++++++++++++++++++++--- docs/src/pages/index.js | 1 - docs/src/utils/authrequest.js | 4 ++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/src/components/authrequest.jsx b/docs/src/components/authrequest.jsx index f864b1cbbb..4beab34e76 100644 --- a/docs/src/components/authrequest.jsx +++ b/docs/src/components/authrequest.jsx @@ -1,4 +1,4 @@ -import React, { Fragment, useContext, useEffect, useState } from "react"; +import { Fragment, useContext, useEffect, useState } from "react"; import { AuthRequestContext } from "../utils/authrequest"; import { Listbox } from "@headlessui/react"; import { Transition } from "@headlessui/react"; @@ -115,6 +115,14 @@ export function SetAuthRequest() { }`, ]; + const scopeExplanations = new Map([ + ['urn:zitadel:iam:org:project:id:zitadel:aud', 'Requested projectid will be added to the audience of the access token.'], + ['urn:zitadel:iam:user:metadata', 'Metadata of the user will be included in the token. The values are base64 encoded.'], + [`urn:zitadel:iam:org:id:${ + organizationId ? organizationId : "[organizationId]" + }`, 'Enforce that the user is a member of the selected organization.'] + ]); + const [scopeState, setScopeState] = useState( [true, true, true, false, false, false, false, false] // new Array(allScopes.length).fill(false) @@ -161,8 +169,13 @@ export function SetAuthRequest() { return input; }; - useEffect(async () => { - setCodeChallenge(await encodeCodeChallenge(codeVerifier)); + useEffect(() => { + const updateCodeChallange = async () => { + const newCodeChallange = await encodeCodeChallenge(codeVerifier) + setCodeChallenge(newCodeChallange); + } + + updateCodeChallange(); }, [codeVerifier]); useEffect(() => { @@ -559,6 +572,7 @@ export function SetAuthRequest() { name="scopes" value={`${scope}`} checked={scopeState[scopeIndex]} + disabled={scope === 'openid'} onChange={() => { toggleScope(scopeIndex); }} @@ -571,6 +585,11 @@ export function SetAuthRequest() { ) : null} + {scopeExplanations.has(scope) && ( + + {scopeExplanations.get(scope)} + + )} ); })} diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index 630a7d4fbd..729688a43b 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -4,7 +4,6 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; import ThemedImage from "@theme/ThemedImage"; import clsx from "clsx"; -import React from "react"; import Column from "../components/column"; import { diff --git a/docs/src/utils/authrequest.js b/docs/src/utils/authrequest.js index ee709ebabb..1a0ceba28c 100644 --- a/docs/src/utils/authrequest.js +++ b/docs/src/utils/authrequest.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useEffect, useState } from "react"; export const AuthRequestContext = React.createContext(null); @@ -34,7 +34,7 @@ export default ({ children }) => { const id_token_hint = params.get("id_token_hint"); const organization_id = params.get("organization_id"); - setInstance(instance_param ?? "https://mydomain-xyza.zitadel.cloud/"); + setInstance(instance_param ?? "http://localhost:8080/"); setClientId(client_id ?? "170086824411201793@yourapp"); setRedirectUri( redirect_uri ?? "http://localhost:8080/api/auth/callback/zitadel" From b93b6a8a1c20a3c3bd9856492d07410de9cac40f Mon Sep 17 00:00:00 2001 From: Mahdi JafariRaviz <56910785+mahdi-jfri@users.noreply.github.com> Date: Mon, 4 Aug 2025 08:16:18 -0400 Subject: [PATCH 2/3] fix: disable client id in oidc configuration (#10177) # Which Problems Are Solved This pr disables the client id in oidc configuration in console, as mentioned in #10149. # How the Problems Are Solved I re-disabled the field from inside the form. # Additional Context - Closes #10149. - Closes #8530 --- .../app/pages/projects/apps/app-detail/app-detail.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts b/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts index 4b11f0171c..236a83b706 100644 --- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts +++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts @@ -423,6 +423,7 @@ export class AppDetailComponent implements OnInit, OnDestroy { if (allowed) { this.oidcForm.enable(); + this.oidcForm.controls['clientId'].disable(); this.oidcTokenForm.enable(); this.apiForm.enable(); this.samlForm.enable(); From d4222d6fd658bcfd4d85989135e137f96db334a0 Mon Sep 17 00:00:00 2001 From: Zach Hirschtritt Date: Mon, 4 Aug 2025 09:33:01 -0400 Subject: [PATCH 3/3] fix: don't trigger session projection on notification handling (#10298) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved There is an outstanding bug wherein a session projection can fail to complete and an session OTP challenge is blocked because the projection doesn't exist. Not sure why the session projection can fail to persist - I can't find any error logs or failed events to crosscheck. However, I can clearly see the session events persisted with user/password checks and the OTP challenged added on the session - but no session projection on sessions8 table. This only seems to come up under somewhat higher loads - about 5 logins/s and only for about 1% of cases. (where a "login" is: authRequest, createSession, getAuthCodeWithSession, tokenExchange, and finally, otpSmsChallenge...💥). # How the Problems Are Solved This is only half a fix, but an important one as it can block login for affected users. Instead of triggering and checking the session projection on notification enqueuing, build a write model directly from the ES. # Additional Changes # Additional Context This doesn't touch the "legacy" notification handler as to limit the blast radius of this change. But might be worth adding there too. The test is difficult to update correctly so is somewhat incomplete. Any suggestions for refactoring or test helpers I'm missing would be welcome. Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com> --- .../notification/handlers/user_notifier.go | 11 ++-- .../handlers/user_notifier_test.go | 56 ++++++++----------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go index 6ca753caa9..6880d63bff 100644 --- a/internal/notification/handlers/user_notifier.go +++ b/internal/notification/handlers/user_notifier.go @@ -7,6 +7,7 @@ import ( http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/ui/console" "github.com/zitadel/zitadel/internal/api/ui/login" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -417,12 +418,14 @@ func (u *userNotifier) reduceSessionOTPSMSChallenged(event eventstore.Event) (*h if alreadyHandled { return nil } - s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "", nil) + + ctx, err = u.queries.Origin(ctx, e) if err != nil { return err } - ctx, err = u.queries.Origin(ctx, e) + sessionWriteModel := command.NewSessionWriteModel(e.Aggregate().ID, e.Aggregate().InstanceID) + err = u.queries.es.FilterToQueryReducer(ctx, sessionWriteModel) if err != nil { return err } @@ -432,8 +435,8 @@ func (u *userNotifier) reduceSessionOTPSMSChallenged(event eventstore.Event) (*h return u.queue.Insert(ctx, ¬ification.Request{ Aggregate: e.Aggregate(), - UserID: s.UserFactor.UserID, - UserResourceOwner: s.UserFactor.ResourceOwner, + UserID: sessionWriteModel.UserID, + UserResourceOwner: sessionWriteModel.UserResourceOwner, TriggeredAtOrigin: http_util.DomainContext(ctx).Origin(), EventType: e.EventType, NotificationType: domain.NotificationTypeSms, diff --git a/internal/notification/handlers/user_notifier_test.go b/internal/notification/handlers/user_notifier_test.go index 874fbdf9af..eae40472e3 100644 --- a/internal/notification/handlers/user_notifier_test.go +++ b/internal/notification/handlers/user_notifier_test.go @@ -1349,19 +1349,12 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { testCode := "testcode" _, code := cryptoValue(t, ctrl, testCode) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any(), nil).Return(&query.Session{ - ID: sessionID, - ResourceOwner: instanceID, - UserFactor: query.SessionUserFactor{ - UserID: userID, - ResourceOwner: orgID, - }, - }, nil) + queue.EXPECT().Insert( gomock.Any(), ¬ification.Request{ - UserID: userID, - UserResourceOwner: orgID, + UserID: "", // Empty since no session events are provided + UserResourceOwner: "", // Empty since no session events are provided TriggeredAtOrigin: eventOrigin, URLTemplate: "", Code: code, @@ -1387,11 +1380,15 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { gomock.Any(), gomock.Any(), ).Return(nil) + + mockQuerier := es_repo_mock.NewMockQuerier(ctrl) + mockQuerier.EXPECT().FilterToReducer(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + return fields{ queries: queries, queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + Querier: mockQuerier, }), }, args{ event: &session.OTPSMSChallengedEvent{ @@ -1421,19 +1418,12 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { IsPrimary: true, }}, }, nil) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any(), nil).Return(&query.Session{ - ID: sessionID, - ResourceOwner: instanceID, - UserFactor: query.SessionUserFactor{ - UserID: userID, - ResourceOwner: orgID, - }, - }, nil) + queue.EXPECT().Insert( gomock.Any(), ¬ification.Request{ - UserID: userID, - UserResourceOwner: orgID, + UserID: "", // Empty since no session events are provided + UserResourceOwner: "", // Empty since no session events are provided TriggeredAtOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), URLTemplate: "", Code: code, @@ -1459,11 +1449,15 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { gomock.Any(), gomock.Any(), ).Return(nil) + + mockQuerier := es_repo_mock.NewMockQuerier(ctrl) + mockQuerier.EXPECT().FilterToReducer(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + return fields{ queries: queries, queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + Querier: mockQuerier, }), }, args{ event: &session.OTPSMSChallengedEvent{ @@ -1484,19 +1478,11 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { { name: "external code", test: func(ctrl *gomock.Controller, queries *mock.MockQueries, queue *mock.MockQueue) (f fields, a args, w want) { - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any(), nil).Return(&query.Session{ - ID: sessionID, - ResourceOwner: instanceID, - UserFactor: query.SessionUserFactor{ - UserID: userID, - ResourceOwner: orgID, - }, - }, nil) queue.EXPECT().Insert( gomock.Any(), ¬ification.Request{ - UserID: userID, - UserResourceOwner: orgID, + UserID: "", // Empty since no session events are provided + UserResourceOwner: "", // Empty since no session events are provided TriggeredAtOrigin: eventOrigin, URLTemplate: "", Code: nil, @@ -1522,11 +1508,15 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { gomock.Any(), gomock.Any(), ).Return(nil) + + mockQuerier := es_repo_mock.NewMockQuerier(ctrl) + mockQuerier.EXPECT().FilterToReducer(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + return fields{ queries: queries, queue: queue, es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + Querier: mockQuerier, }), }, args{ event: &session.OTPSMSChallengedEvent{