From 382a97c30f69e9f3c2c5b73c1744a4efbc8cbdf6 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 4 Sep 2024 12:14:50 +0200 Subject: [PATCH] feat(oidc): end session by id_token_hint and without cookie (#8542) # Which Problems Are Solved The end_session_endpoint currently always requires the userAgent cookie to be able to terminate session created through the hosted login UI. Only tokens issued through the Login V2 can be used to directly terminate a specific session and without the need of a cookie. This PR adds the possibility to terminate a single V1 session or all V1 sessions belonging to the same user agent without the need of the userAgent cookie by providing an id_token as `id_token_hint` which contains the id of a V1 session as `sid`. # How the Problems Are Solved - #8525 added the `sid` claim for id_tokens issued through the login UI - The `sid` can now be checked for the `V1_` prefix and queries for either the userAgentID and depending on the `OIDCSingleV1SessionTermination` flag all userIDs of active session from the same user agent id - The `OIDCSingleV1SessionTermination` flag is added with default value false to keep the existing behavior of terminating all sessions even in case of providing an id_token_hint # Additional Changes - pass `context.Context` into session view functions for querying the database with that context # Additional Context - relates to #8499 - closes #8501 --- .../features/features.component.html | 55 ++++++++++++++++ .../components/features/features.component.ts | 37 +++++------ console/src/assets/i18n/bg.json | 2 + console/src/assets/i18n/cs.json | 2 + console/src/assets/i18n/de.json | 2 + console/src/assets/i18n/en.json | 2 + console/src/assets/i18n/es.json | 2 + console/src/assets/i18n/fr.json | 2 + console/src/assets/i18n/it.json | 2 + console/src/assets/i18n/ja.json | 2 + console/src/assets/i18n/mk.json | 2 + console/src/assets/i18n/nl.json | 2 + console/src/assets/i18n/pl.json | 2 + console/src/assets/i18n/pt.json | 2 + console/src/assets/i18n/ru.json | 2 + console/src/assets/i18n/sv.json | 2 + console/src/assets/i18n/zh.json | 2 + internal/api/grpc/feature/v2/converter.go | 4 ++ .../api/grpc/feature/v2/converter_test.go | 22 +++++++ internal/api/grpc/feature/v2beta/converter.go | 4 ++ .../api/grpc/feature/v2beta/converter_test.go | 20 ++++++ internal/api/oidc/auth_request.go | 36 ++++++++++- .../eventsourcing/eventstore/auth_request.go | 8 +-- .../eventstore/auth_request_test.go | 12 ++-- .../eventsourcing/eventstore/user.go | 10 ++- .../eventsourcing/eventstore/user_session.go | 2 +- .../eventsourcing/view/user_session.go | 16 +++-- internal/auth/repository/user.go | 2 + internal/command/instance_features.go | 4 +- internal/command/instance_features_model.go | 5 ++ internal/command/instance_features_test.go | 10 +++ internal/command/system_features.go | 4 +- internal/command/system_features_model.go | 5 ++ internal/command/system_features_test.go | 10 +++ internal/feature/feature.go | 2 + internal/feature/key_enumer.go | 12 ++-- internal/query/instance_features.go | 1 + internal/query/instance_features_model.go | 4 ++ .../query/projection/instance_features.go | 4 ++ internal/query/system_features.go | 1 + internal/query/system_features_model.go | 3 + .../feature/feature_v2/eventstore.go | 2 + .../repository/feature/feature_v2/feature.go | 2 + .../view/active_user_ids_by_session_id.sql | 11 ++++ .../repository/view/model/user_session.go | 5 ++ .../view/user_agent_by_user_session_id.sql | 7 +++ .../user/repository/view/user_session_view.go | 63 +++++++++++++++++-- proto/zitadel/feature/v2/instance.proto | 14 +++++ proto/zitadel/feature/v2/system.proto | 14 +++++ proto/zitadel/feature/v2beta/instance.proto | 14 +++++ proto/zitadel/feature/v2beta/system.proto | 14 +++++ 51 files changed, 422 insertions(+), 47 deletions(-) create mode 100644 internal/user/repository/view/active_user_ids_by_session_id.sql create mode 100644 internal/user/repository/view/user_agent_by_user_session_id.sql diff --git a/console/src/app/components/features/features.component.html b/console/src/app/components/features/features.component.html index 05113c17f..e66356921 100644 --- a/console/src/app/components/features/features.component.html +++ b/console/src/app/components/features/features.component.html @@ -348,6 +348,61 @@ {{ 'SETTING.FEATURES.ACTIONS_DESCRIPTION' | translate }} + +
+ {{ 'SETTING.FEATURES.OIDCSINGLEV1SESSIONTERMINATION' | translate }} +
+ + +
+ {{ 'SETTING.FEATURES.STATES.INHERITED' | translate }} + +
+
+
+
+ +
+ {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }} +
+
+ +
+ {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }} +
+
+
+
+ {{ + 'SETTING.FEATURES.OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION' | translate + }} +
diff --git a/console/src/app/components/features/features.component.ts b/console/src/app/components/features/features.component.ts index 74750d9cc..327e9d279 100644 --- a/console/src/app/components/features/features.component.ts +++ b/console/src/app/components/features/features.component.ts @@ -38,6 +38,7 @@ type ToggleStates = { userSchema?: FeatureState; oidcTokenExchange?: FeatureState; actions?: FeatureState; + oidcSingleV1SessionTermination?: FeatureState; }; @Component({ @@ -135,6 +136,12 @@ export class FeaturesComponent implements OnDestroy { req.setActions(this.toggleStates?.actions?.state === ToggleState.ENABLED); changed = true; } + if (this.toggleStates?.oidcSingleV1SessionTermination?.state !== ToggleState.INHERITED) { + req.setOidcSingleV1SessionTermination( + this.toggleStates?.oidcSingleV1SessionTermination?.state === ToggleState.ENABLED, + ); + changed = true; + } if (changed) { this.featureService @@ -215,6 +222,16 @@ export class FeaturesComponent implements OnDestroy { ? ToggleState.ENABLED : ToggleState.DISABLED, }, + oidcSingleV1SessionTermination: { + source: this.featureData.oidcSingleV1SessionTermination?.source || Source.SOURCE_SYSTEM, + state: + this.featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_SYSTEM || + this.featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_UNSPECIFIED + ? ToggleState.INHERITED + : !!this.featureData.oidcSingleV1SessionTermination?.enabled + ? ToggleState.ENABLED + : ToggleState.DISABLED, + }, }; }); } @@ -232,24 +249,4 @@ export class FeaturesComponent implements OnDestroy { this.toast.showError(error); }); } - - public saveFeatures(): void { - if (this.featureData) { - const req = new SetInstanceFeaturesRequest(); - req.setLoginDefaultOrg(!!this.featureData.loginDefaultOrg?.enabled); - req.setOidcLegacyIntrospection(!!this.featureData.oidcLegacyIntrospection?.enabled); - req.setOidcTokenExchange(!!this.featureData.oidcTokenExchange?.enabled); - req.setOidcTriggerIntrospectionProjections(!!this.featureData.oidcTriggerIntrospectionProjections?.enabled); - req.setUserSchema(!!this.featureData.userSchema?.enabled); - - this.featureService - .setInstanceFeatures(req) - .then(() => { - this.toast.showInfo('POLICY.TOAST.SET', true); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - } } diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 07370e57a..dd048242c 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1468,6 +1468,8 @@ "USERSCHEMA_DESCRIPTION": "Потребителските схеми позволяват управление на данните за схемите на потребителите. Ако е активиран флагът, ще можете да използвате новото API и неговите функции.", "ACTIONS": "Действия", "ACTIONS_DESCRIPTION": "Действия v2 позволяват управление на выполнения на данни и цели. Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Завършване на сесия", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Ако флагът е активиран, ще можете да прекратите единична сесия от UI за вход, като предоставите id_token с `sid` претенция като id_token_hint на крайната точка на end_session. Имайте предвид, че в момента всички сесии от същия потребителски агент (браузър) се прекратяват в UI за вход. Сесиите, управлявани чрез API на сесията, вече позволяват прекратяването на единични сесии.", "STATES": { "INHERITED": "Наследено", "ENABLED": "Активирано", diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index 3613ffda0..fd50d9702 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -1469,6 +1469,8 @@ "USERSCHEMA_DESCRIPTION": "Schémata uživatelů umožňují spravovat datová schémata uživatelů. Pokud je příznak povolen, budete moci používat nové API a jeho funkce.", "ACTIONS": "Akce", "ACTIONS_DESCRIPTION": "Akce v2 umožňují správu datových provedení a cílů. Pokud je tento příznak povolen, budete moci používat nové API a jeho funkce.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 ukončení relace", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Pokud je příznak aktivován, budete moci ukončit jedinou relaci z rozhraní pro přihlášení zadáním id_token s nárokem `sid` jako id_token_hint na koncovém bodu end_session. Poznamenejte si, že v současné době jsou v rozhraní pro přihlášení ukončeny všechny relace ze stejného uživatelského agenta (prohlížeče). Relace spravované prostřednictvím rozhraní API relace již umožňují ukončení jednotlivých relací.", "STATES": { "INHERITED": "Děděno", "ENABLED": "Povoleno", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index fb57c583b..e284375a4 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1469,6 +1469,8 @@ "USERSCHEMA_DESCRIPTION": "Benutzerschemata ermöglichen das Verwalten von Datenschemata von Benutzern. Wenn die Flagge aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", "ACTIONS": "Aktionen", "ACTIONS_DESCRIPTION": "Aktionen v2 ermöglichen die Verwaltung von Datenausführungen und Zielen. Wenn das Flag aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Sitzungsbeendigung", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Wenn das Flag aktiviert ist, können Sie eine einzelne Sitzung über die Login-Benutzeroberfläche beenden, indem Sie einen id_token mit einem `sid` Claim als id_token_hint am Endpunkt end_session übergeben. Beachten Sie, dass derzeit alle Sitzungen desselben Benutzeragenten (Browser) in der Login-Benutzeroberfläche beendet werden. Sitzungen, die über die Session API verwaltet werden, ermöglichen bereits die Beendigung einzelner Sitzungen.", "STATES": { "INHERITED": "Erben", "ENABLED": "Aktiviert", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index c1acf1e3b..527026674 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1469,6 +1469,8 @@ "USERSCHEMA_DESCRIPTION": "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features.", "ACTIONS": "Actions", "ACTIONS_DESCRIPTION": "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Session Termination", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions.", "STATES": { "INHERITED": "Inherit", "ENABLED": "Enabled", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 6b07d2b69..bc2cc37d9 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1470,6 +1470,8 @@ "USERSCHEMA_DESCRIPTION": "Los esquemas de usuario permiten gestionar los esquemas de datos de los usuarios. Si se activa la bandera, podrás utilizar la nueva API y sus funciones.", "ACTIONS": "Acciones", "ACTIONS_DESCRIPTION": "Acciones v2 permite administrar las ejecuciones y objetivos de datos. Si la bandera está habilitada, podrá utilizar la nueva API y sus funciones.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Terminación de sesión", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Si la bandera está habilitada, podrá terminar una sesión única desde la interfaz de usuario de inicio de sesión proporcionando un id_token con una reclamación `sid` como id_token_hint en el punto final de end_session. Tenga en cuenta que actualmente se terminan todas las sesiones del mismo agente de usuario (navegador) en la interfaz de usuario de inicio de sesión. Las sesiones administradas a través de la API de sesión ya permiten la terminación de sesiones individuales.", "STATES": { "INHERITED": "Heredado", "ENABLED": "Habilitado", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 61d2b61f9..5fb378f0b 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1469,6 +1469,8 @@ "USERSCHEMA_DESCRIPTION": "Les schémas utilisateur permettent de gérer les schémas de données des utilisateurs. Si le drapeau est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", "ACTIONS": "Actions", "ACTIONS_DESCRIPTION": "Les actions v2 permettent de gérer les exécutions et les cibles de données. Si l'indicateur est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Fin de session", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Si l'indicateur est activé, vous pourrez terminer une seule session à partir de l'interface utilisateur de connexion en fournissant un id_token avec une revendication `sid` en tant que id_token_hint sur le point de terminaison end_session. Notez que toutes les sessions du même agent utilisateur (navigateur) sont actuellement terminées dans l'interface utilisateur de connexion. Les sessions gérées via l'API de session permettent déjà la terminaison de sessions individuelles.", "STATES": { "INHERITED": "Hérité", "ENABLED": "Activé", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index a43d20c85..c368862af 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1469,6 +1469,8 @@ "USERSCHEMA_DESCRIPTION": "Gli schemi utente consentono di gestire gli schemi di dati degli utenti. Se la flag è attivata, sarà possibile utilizzare la nuova API e le sue funzionalità.", "ACTIONS": "Azioni", "ACTIONS_DESCRIPTION": "Le azioni v2 consentono di gestire le esecuzioni e gli obiettivi dei dati. Se l'indicatore è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Terminazione sessione", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Se il flag è abilitato, sarai in grado di terminare una singola sessione dall'interfaccia utente di accesso fornendo un id_token con una richiesta `sid` come id_token_hint nel punto finale di end_session. Tieni presente che attualmente tutte le sessioni dello stesso agente utente (browser) vengono terminate nell'interfaccia utente di accesso. Le sessioni gestite tramite l'API di sessione consentono già la terminazione di singole sessioni.", "STATES": { "INHERITED": "Predefinito", "ENABLED": "Abilitato", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 12b0a553d..387df3bb1 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1469,6 +1469,8 @@ "USERSCHEMA_DESCRIPTION": "ユーザー スキーマを使用すると、ユーザーのデータスキーマを管理できます。フラグが有効になっている場合、新しい APIとその機能を使用できます。", "ACTIONS": "アクション", "ACTIONS_DESCRIPTION": "Actions v2は、データの実行とターゲットを管理できます。フラグが有効になっている場合、新しい APIとその機能を使用できます。", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 セッション終了", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "フラグが有効になっている場合、id_token を `sid` クレームと共に id_token_hint として end_session エンドポイントに提供することで、ログイン UI から単一のセッションを終了できるようになります。 現在、同じユーザー エージェント (ブラウザ) からのすべてのセッションがログイン UI で終了することに注意してください。 セッション API を通じて管理されるセッションは、すでに単一のセッションの終了を許可しています。", "STATES": { "INHERITED": "継承", "ENABLED": "有効", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index a9e14a8ea..c0723fc2a 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1470,6 +1470,8 @@ "USERSCHEMA_DESCRIPTION": "Корисничките шеми овозможуваат управување со податоци шеми на корисникот. Ако знамето е овозможено, ќе можете да го користите новиот API и неговите функции.", "ACTIONS": "Акции", "ACTIONS_DESCRIPTION": "Акциите v2 овозможуваат управување со извршување на податоци и цели. Ако знамето е овозможено, ќе можете да го користите новиот API и неговите функции.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Завршување на сесија", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Ако ознаката е активирана, ќе можете да ја завршите единечна сесија од корисничкиот интерфејс за најава, со обезбедување id_token со `sid` побарување како id_token_hint на крајната точка на end_session. Имајте предвид дека во моментов сите сесии од истиот кориснички агент (прелистувач) се завршуваат во корисничкиот интерфејс за најава. Сесиите управувани преку API на сесија веќе дозволуваат завршување на единечни сесии.", "STATES": { "INHERITED": "Наследи", "ENABLED": "Овозможено", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index 4b6f402d5..9c592cda9 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -1468,6 +1468,8 @@ "USERSCHEMA_DESCRIPTION": "Met gebruikerschema's kunt u de dataschema's van gebruikers beheren. Als de vlag is ingeschakeld, kunt u de nieuwe API en zijn functies gebruiken.", "ACTIONS": "Acties", "ACTIONS_DESCRIPTION": "Actions v2 maken het mogelijk om data-uitvoeringen en doelen te beheren. Als de vlag is ingeschakeld, kunt u de nieuwe API en zijn functies gebruiken.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Sessiebeëindiging", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Als het vlagje is ingeschakeld, kunt u een enkele sessie beëindigen via de login-gebruikersinterface door een id_token met een `sid`-claim als id_token_hint op het eindpunt end_session te verstrekken. Houd er rekening mee dat momenteel alle sessies van dezelfde gebruikersagent (browser) worden beëindigd in de login-gebruikersinterface. Sessies die worden beheerd via de Session API staan al toe om individuele sessies te beëindigen.", "STATES": { "INHERITED": "Overgenomen", "ENABLED": "Ingeschakeld", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 332717fca..2592828fe 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1468,6 +1468,8 @@ "USERSCHEMA_DESCRIPTION": "Schematy użytkowników umożliwiają zarządzanie schematami danych użytkowników. Jeśli flaga jest włączona, będziesz mógł korzystać z nowego interfejsu API i jego funkcji.", "ACTIONS": "Akcje", "ACTIONS_DESCRIPTION": "Akcje v2 umożliwiają zarządzanie wykonaniami danych i celami. Jeżeli flaga jest włączona, będziesz mógł korzystać z nowego interfejsu API i jego funkcji.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Zakończenie sesji", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł zakończyć pojedynczą sesję z interfejsu użytkownika logowania, podając id_token z roszczeniem `sid` jako id_token_hint w punkcie końcowym end_session. Należy pamiętać, że obecnie wszystkie sesje z tego samego agenta użytkownika (przeglądarki) są kończone w interfejsie użytkownika logowania. Sesje zarządzane za pomocą interfejsu API sesji już pozwalają na zakończenie pojedynczych sesji.", "STATES": { "INHERITED": "Dziedziczony", "ENABLED": "Włączony", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index a87eeb3e1..93739f187 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1470,6 +1470,8 @@ "USERSCHEMAS_DESCRIPTION": "Esquemas de Usuário permitem gerenciar esquemas de dados do usuário. Se o sinalizador estiver ativado, você poderá usar a nova API e seus recursos.", "ACTIONS": "Ações", "ACTIONS_DESCRIPTION": "Actions v2 permitem gerenciar execuções e destinos de dados. Se a flag estiver habilitada, você poderá usar a nova API e seus recursos.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Término de sessão", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Se a bandeira estiver habilitada, você poderá encerrar uma sessão única da interface do usuário de login fornecendo um id_token com uma reivindicação `sid como id_token_hint no ponto final de end_session. Observe que atualmente todas as sessões do mesmo agente de usuário (navegador) são encerradas na interface do usuário de login. As sessões gerenciadas por meio da API de sessão já permitem o encerramento de sessões individuais.", "STATES": { "INHERITED": "Herdade", "ENABLED": "Habilitado", diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index 8aedd616a..a2ed9ff36 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -1521,6 +1521,8 @@ "USERSCHEMA_DESCRIPTION": "Схемы пользователей позволяют управлять схемами данных пользователей. Если флаг включен, вы сможете использовать новый API и его функции.", "ACTIONS": "Действия", "ACTIONS_DESCRIPTION": "Actions v2 позволяют управлять выполнением данных и целевыми объектами. Если флаг включен, вы сможете использовать новый API и его функции.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Окончание сеанса", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Если флаг включен, вы сможете завершить отдельный сеанс из интерфейса пользователя входа, предоставив id_token с претензией `sid` в качестве id_token_hint на конечной точке end_session. Обратите внимание, что в настоящее время все сеансы одного и того же пользовательского агента (браузера) завершаются в интерфейсе пользователя входа. Сеансы, управляемые через API сеанса, уже позволяют завершать отдельные сеансы.", "STATES": { "INHERITED": "Наследовать", "ENABLED": "Включено", diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 15b4bb899..cc7bc2259 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -1473,6 +1473,8 @@ "USERSCHEMA_DESCRIPTION": "Användarscheman tillåter att hantera datascheman för användare. Om flaggan är aktiverad kommer du att kunna använda det nya API:et och dess funktioner.", "ACTIONS": "Åtgärder", "ACTIONS_DESCRIPTION": "Åtgärder v2 tillåter att hantera dataexekveringar och mål. Om flaggan är aktiverad kommer du att kunna använda det nya API:et och dess funktioner.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 Session avslutning", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Om flaggan är aktiverad, kan du avsluta en enskild session från inloggningsgränssnittet genom att ange en id_token med ett `sid`-krav som id_token_hint på slutpunkten end_session. Observera att för närvarande alla sessioner från samma användaragent (webbläsare) avslutas i inloggningsgränssnittet. Sessioner som hanteras via Session API tillåter redan avslutning av enskilda sessioner.", "STATES": { "INHERITED": "Ärv", "ENABLED": "Aktiverad", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 7fd76ef5a..061f7faac 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1469,6 +1469,8 @@ "USERSCHEMA_DESCRIPTION": "用户架构允许管理用户的数据架构。如果启用此标志,您将可以使用新的 API 及其功能。", "ACTIONS": "操作", "ACTIONS_DESCRIPTION": "Actions v2 可以管理数据执行和目标。如果启用此标志,您将可以使用新的 API 及其功能。", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Single V1 终止会话", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "如果启用了标志,您可以通过在 end_session 端点上提供带有 `sid` 声明的 id_token 作为 id_token_hint 来从登录 UI 终止单个会话。 请注意,目前所有来自同一用户代理(浏览器)的会话都在登录 UI 中终止。 通过会话 API 管理的会话已经允许终止单个会话。", "STATES": { "INHERITED": "继承", "ENABLED": "已启用", diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index 533baaf53..5817b47c4 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -17,6 +17,7 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command. Actions: req.Actions, TokenExchange: req.OidcTokenExchange, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), + OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, } } @@ -30,6 +31,7 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), + OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), } } @@ -44,6 +46,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), WebKey: req.WebKey, DebugOIDCParentError: req.DebugOidcParentError, + OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, } } @@ -59,6 +62,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), WebKey: featureSourceToFlagPb(&f.WebKey), DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), + OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), } } diff --git a/internal/api/grpc/feature/v2/converter_test.go b/internal/api/grpc/feature/v2/converter_test.go index f88b96ea5..b0b21c3c2 100644 --- a/internal/api/grpc/feature/v2/converter_test.go +++ b/internal/api/grpc/feature/v2/converter_test.go @@ -25,6 +25,7 @@ func Test_systemFeaturesToCommand(t *testing.T) { Actions: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), ImprovedPerformance: nil, + OidcSingleV1SessionTermination: gu.Ptr(true), } want := &command.SystemFeatures{ LoginDefaultOrg: gu.Ptr(true), @@ -34,6 +35,7 @@ func Test_systemFeaturesToCommand(t *testing.T) { Actions: gu.Ptr(true), TokenExchange: gu.Ptr(true), ImprovedPerformance: nil, + OIDCSingleV1SessionTermination: gu.Ptr(true), } got := systemFeaturesToCommand(arg) assert.Equal(t, want, got) @@ -74,6 +76,10 @@ func Test_systemFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID}, }, + OIDCSingleV1SessionTermination: query.FeatureSource[bool]{ + Level: feature.LevelSystem, + Value: true, + }, } want := &feature_pb.GetSystemFeaturesResponse{ Details: &object.Details{ @@ -109,6 +115,10 @@ func Test_systemFeaturesToPb(t *testing.T) { ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, }, + OidcSingleV1SessionTermination: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, } got := systemFeaturesToPb(arg) assert.Equal(t, want, got) @@ -124,6 +134,8 @@ func Test_instanceFeaturesToCommand(t *testing.T) { Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), + DebugOidcParentError: gu.Ptr(true), + OidcSingleV1SessionTermination: gu.Ptr(true), } want := &command.InstanceFeatures{ LoginDefaultOrg: gu.Ptr(true), @@ -134,6 +146,8 @@ func Test_instanceFeaturesToCommand(t *testing.T) { Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), + DebugOIDCParentError: gu.Ptr(true), + OIDCSingleV1SessionTermination: gu.Ptr(true), } got := instanceFeaturesToCommand(arg) assert.Equal(t, want, got) @@ -178,6 +192,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelInstance, Value: true, }, + OIDCSingleV1SessionTermination: query.FeatureSource[bool]{ + Level: feature.LevelInstance, + Value: true, + }, } want := &feature_pb.GetInstanceFeaturesResponse{ Details: &object.Details{ @@ -221,6 +239,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: false, Source: feature_pb.Source_SOURCE_UNSPECIFIED, }, + OidcSingleV1SessionTermination: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_INSTANCE, + }, } got := instanceFeaturesToPb(arg) assert.Equal(t, want, got) diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go index 63738672e..39f2284be 100644 --- a/internal/api/grpc/feature/v2beta/converter.go +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -17,6 +17,7 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command. Actions: req.Actions, TokenExchange: req.OidcTokenExchange, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), + OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, } } @@ -30,6 +31,7 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), + OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), } } @@ -44,6 +46,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), WebKey: req.WebKey, DebugOIDCParentError: req.DebugOidcParentError, + OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, } } @@ -59,6 +62,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), WebKey: featureSourceToFlagPb(&f.WebKey), DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), + OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), } } diff --git a/internal/api/grpc/feature/v2beta/converter_test.go b/internal/api/grpc/feature/v2beta/converter_test.go index 80be16007..2896d8f77 100644 --- a/internal/api/grpc/feature/v2beta/converter_test.go +++ b/internal/api/grpc/feature/v2beta/converter_test.go @@ -25,6 +25,7 @@ func Test_systemFeaturesToCommand(t *testing.T) { Actions: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), ImprovedPerformance: nil, + OidcSingleV1SessionTermination: gu.Ptr(true), } want := &command.SystemFeatures{ LoginDefaultOrg: gu.Ptr(true), @@ -34,6 +35,7 @@ func Test_systemFeaturesToCommand(t *testing.T) { Actions: gu.Ptr(true), TokenExchange: gu.Ptr(true), ImprovedPerformance: nil, + OIDCSingleV1SessionTermination: gu.Ptr(true), } got := systemFeaturesToCommand(arg) assert.Equal(t, want, got) @@ -74,6 +76,10 @@ func Test_systemFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID}, }, + OIDCSingleV1SessionTermination: query.FeatureSource[bool]{ + Level: feature.LevelSystem, + Value: true, + }, } want := &feature_pb.GetSystemFeaturesResponse{ Details: &object.Details{ @@ -109,6 +115,10 @@ func Test_systemFeaturesToPb(t *testing.T) { ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, }, + OidcSingleV1SessionTermination: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, } got := systemFeaturesToPb(arg) assert.Equal(t, want, got) @@ -124,6 +134,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) { Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), + OidcSingleV1SessionTermination: gu.Ptr(true), } want := &command.InstanceFeatures{ LoginDefaultOrg: gu.Ptr(true), @@ -134,6 +145,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) { Actions: gu.Ptr(true), ImprovedPerformance: nil, WebKey: gu.Ptr(true), + OIDCSingleV1SessionTermination: gu.Ptr(true), } got := instanceFeaturesToCommand(arg) assert.Equal(t, want, got) @@ -178,6 +190,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelInstance, Value: true, }, + OIDCSingleV1SessionTermination: query.FeatureSource[bool]{ + Level: feature.LevelInstance, + Value: true, + }, } want := &feature_pb.GetInstanceFeaturesResponse{ Details: &object.Details{ @@ -221,6 +237,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: false, Source: feature_pb.Source_SOURCE_UNSPECIFIED, }, + OidcSingleV1SessionTermination: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_INSTANCE, + }, } got := instanceFeaturesToPb(arg) assert.Equal(t, want, got) diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 474e12810..dc402036f 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -16,6 +16,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" + "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/handler" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -245,11 +246,20 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR } // If there is no login client header and no id_token_hint or the id_token_hint does not have a session ID, - // do a v1 Terminate session. + // do a v1 Terminate session (which terminates all sessions of the user agent, identified by cookie). if endSessionRequest.IDTokenHintClaims == nil || endSessionRequest.IDTokenHintClaims.SessionID == "" { return endSessionRequest.RedirectURI, o.TerminateSession(ctx, endSessionRequest.UserID, endSessionRequest.ClientID) } + // If the sessionID is prefixed by V1, we also terminate a v1 session. + if strings.HasPrefix(endSessionRequest.IDTokenHintClaims.SessionID, handler.IDPrefixV1) { + err = o.terminateV1Session(ctx, endSessionRequest.UserID, endSessionRequest.IDTokenHintClaims.SessionID) + if err != nil { + return "", err + } + return endSessionRequest.RedirectURI, nil + } + // terminate the v2 session of the id_token_hint _, err = o.command.TerminateSessionWithoutTokenCheck(ctx, endSessionRequest.IDTokenHintClaims.SessionID) if err != nil { @@ -258,6 +268,30 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR return endSessionRequest.RedirectURI, nil } +// terminateV1Session terminates "v1" sessions created through the login UI. +// Depending on the flag, we either terminate a single session or all of the user agent +func (o *OPStorage) terminateV1Session(ctx context.Context, userID, sessionID string) error { + ctx = authz.SetCtxData(ctx, authz.CtxData{UserID: userID}) + // if the flag is active we only terminate the specific session + if authz.GetFeatures(ctx).OIDCSingleV1SessionTermination { + userAgentID, err := o.repo.UserAgentIDBySessionID(ctx, sessionID) + if err != nil { + return err + } + return o.command.HumansSignOut(ctx, userAgentID, []string{userID}) + } + // otherwise we search for all active sessions within the same user agent of the current session id + userAgentID, userIDs, err := o.repo.ActiveUserIDsBySessionID(ctx, sessionID) + if err != nil { + logging.WithError(err).Error("error retrieving user sessions") + return err + } + if len(userIDs) == 0 { + return nil + } + return o.command.HumansSignOut(ctx, userAgentID, userIDs) +} + func (o *OPStorage) RevokeToken(ctx context.Context, token, userID, clientID string) (err *oidc.Error) { ctx, span := tracing.NewSpan(ctx) defer func() { diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 8791bb4f6..ca1cc5a03 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -74,8 +74,8 @@ type privacyPolicyProvider interface { } type userSessionViewProvider interface { - UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) - UserSessionsByAgentID(string, string) ([]*user_view_model.UserSessionView, error) + UserSessionByIDs(context.Context, string, string, string) (*user_view_model.UserSessionView, error) + UserSessionsByAgentID(context.Context, string, string) ([]*user_view_model.UserSessionView, error) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*query.CurrentState, error) } @@ -1533,7 +1533,7 @@ func userSessionsByUserAgentID(ctx context.Context, provider userSessionViewProv ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - session, err := provider.UserSessionsByAgentID(agentID, instanceID) + session, err := provider.UserSessionsByAgentID(ctx, agentID, instanceID) if err != nil { return nil, err } @@ -1573,7 +1573,7 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve OnError(err). Errorf("could not get current sequence for userSessionByIDs") - session, err := provider.UserSessionByIDs(agentID, user.ID, instanceID) + session, err := provider.UserSessionByIDs(ctx, agentID, user.ID, instanceID) if err != nil { if !zerrors.IsNotFound(err) { return nil, err diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 14d495582..9ed47762b 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -34,11 +34,11 @@ var ( type mockViewNoUserSession struct{} -func (m *mockViewNoUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) { +func (m *mockViewNoUserSession) UserSessionByIDs(context.Context, string, string, string) (*user_view_model.UserSessionView, error) { return nil, zerrors.ThrowNotFound(nil, "id", "user session not found") } -func (m *mockViewNoUserSession) UserSessionsByAgentID(string, string) ([]*user_view_model.UserSessionView, error) { +func (m *mockViewNoUserSession) UserSessionsByAgentID(context.Context, string, string) ([]*user_view_model.UserSessionView, error) { return nil, nil } @@ -48,11 +48,11 @@ func (m *mockViewNoUserSession) GetLatestUserSessionSequence(ctx context.Context type mockViewErrUserSession struct{} -func (m *mockViewErrUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) { +func (m *mockViewErrUserSession) UserSessionByIDs(context.Context, string, string, string) (*user_view_model.UserSessionView, error) { return nil, zerrors.ThrowInternal(nil, "id", "internal error") } -func (m *mockViewErrUserSession) UserSessionsByAgentID(string, string) ([]*user_view_model.UserSessionView, error) { +func (m *mockViewErrUserSession) UserSessionsByAgentID(context.Context, string, string) ([]*user_view_model.UserSessionView, error) { return nil, zerrors.ThrowInternal(nil, "id", "internal error") } @@ -76,7 +76,7 @@ type mockUser struct { SessionState domain.UserSessionState } -func (m *mockViewUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) { +func (m *mockViewUserSession) UserSessionByIDs(context.Context, string, string, string) (*user_view_model.UserSessionView, error) { return &user_view_model.UserSessionView{ ExternalLoginVerification: sql.NullTime{Time: m.ExternalLoginVerification}, PasswordlessVerification: sql.NullTime{Time: m.PasswordlessVerification}, @@ -86,7 +86,7 @@ func (m *mockViewUserSession) UserSessionByIDs(string, string, string) (*user_vi }, nil } -func (m *mockViewUserSession) UserSessionsByAgentID(string, string) ([]*user_view_model.UserSessionView, error) { +func (m *mockViewUserSession) UserSessionsByAgentID(context.Context, string, string) ([]*user_view_model.UserSessionView, error) { sessions := make([]*user_view_model.UserSessionView, len(m.Users)) for i, user := range m.Users { sessions[i] = &user_view_model.UserSessionView{ diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go index f35b2f0cf..0a33199b0 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user.go +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -28,7 +28,7 @@ func (repo *UserRepo) Health(ctx context.Context) error { } func (repo *UserRepo) UserSessionUserIDsByAgentID(ctx context.Context, agentID string) ([]string, error) { - userSessions, err := repo.View.UserSessionsByAgentID(agentID, authz.GetInstance(ctx).InstanceID()) + userSessions, err := repo.View.UserSessionsByAgentID(ctx, agentID, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } @@ -41,6 +41,14 @@ func (repo *UserRepo) UserSessionUserIDsByAgentID(ctx context.Context, agentID s return userIDs, nil } +func (repo *UserRepo) UserAgentIDBySessionID(ctx context.Context, sessionID string) (string, error) { + return repo.View.UserAgentIDBySessionID(ctx, sessionID, authz.GetInstance(ctx).InstanceID()) +} + +func (repo *UserRepo) ActiveUserIDsBySessionID(ctx context.Context, sessionID string) (userAgentID string, userIDs []string, err error) { + return repo.View.ActiveUserIDsBySessionID(ctx, sessionID, authz.GetInstance(ctx).InstanceID()) +} + func (repo *UserRepo) UserEventsByID(ctx context.Context, id string, changeDate time.Time, eventTypes []eventstore.EventType) ([]eventstore.Event, error) { query, err := usr_view.UserByIDQuery(id, authz.GetInstance(ctx).InstanceID(), changeDate, eventTypes) if err != nil { diff --git a/internal/auth/repository/eventsourcing/eventstore/user_session.go b/internal/auth/repository/eventsourcing/eventstore/user_session.go index 8bacef942..683123200 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user_session.go +++ b/internal/auth/repository/eventsourcing/eventstore/user_session.go @@ -14,7 +14,7 @@ type UserSessionRepo struct { } func (repo *UserSessionRepo) GetMyUserSessions(ctx context.Context) ([]*usr_model.UserSessionView, error) { - userSessions, err := repo.View.UserSessionsByAgentID(authz.GetCtxData(ctx).AgentID, authz.GetInstance(ctx).InstanceID()) + userSessions, err := repo.View.UserSessionsByAgentID(ctx, authz.GetCtxData(ctx).AgentID, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } diff --git a/internal/auth/repository/eventsourcing/view/user_session.go b/internal/auth/repository/eventsourcing/view/user_session.go index 893e5872a..f25deb99e 100644 --- a/internal/auth/repository/eventsourcing/view/user_session.go +++ b/internal/auth/repository/eventsourcing/view/user_session.go @@ -12,12 +12,20 @@ const ( userSessionTable = "auth.user_sessions" ) -func (v *View) UserSessionByIDs(agentID, userID, instanceID string) (*model.UserSessionView, error) { - return view.UserSessionByIDs(v.client, agentID, userID, instanceID) +func (v *View) UserSessionByIDs(ctx context.Context, agentID, userID, instanceID string) (*model.UserSessionView, error) { + return view.UserSessionByIDs(ctx, v.client, agentID, userID, instanceID) } -func (v *View) UserSessionsByAgentID(agentID, instanceID string) ([]*model.UserSessionView, error) { - return view.UserSessionsByAgentID(v.client, agentID, instanceID) +func (v *View) UserSessionsByAgentID(ctx context.Context, agentID, instanceID string) ([]*model.UserSessionView, error) { + return view.UserSessionsByAgentID(ctx, v.client, agentID, instanceID) +} + +func (v *View) UserAgentIDBySessionID(ctx context.Context, sessionID, instanceID string) (string, error) { + return view.UserAgentIDBySessionID(ctx, v.client, sessionID, instanceID) +} + +func (v *View) ActiveUserIDsBySessionID(ctx context.Context, sessionID, instanceID string) (userAgentID string, userIDs []string, err error) { + return view.ActiveUserIDsBySessionID(ctx, v.client, sessionID, instanceID) } func (v *View) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (_ *query.CurrentState, err error) { diff --git a/internal/auth/repository/user.go b/internal/auth/repository/user.go index 05be0346c..6f373ec12 100644 --- a/internal/auth/repository/user.go +++ b/internal/auth/repository/user.go @@ -6,4 +6,6 @@ import ( type UserRepository interface { UserSessionUserIDsByAgentID(ctx context.Context, agentID string) ([]string, error) + UserAgentIDBySessionID(ctx context.Context, sessionID string) (string, error) + ActiveUserIDsBySessionID(ctx context.Context, sessionID string) (userAgentID string, userIDs []string, err error) } diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go index 4fcf87b67..9517c148b 100644 --- a/internal/command/instance_features.go +++ b/internal/command/instance_features.go @@ -25,6 +25,7 @@ type InstanceFeatures struct { ImprovedPerformance []feature.ImprovedPerformanceType WebKey *bool DebugOIDCParentError *bool + OIDCSingleV1SessionTermination *bool } func (m *InstanceFeatures) isEmpty() bool { @@ -37,7 +38,8 @@ func (m *InstanceFeatures) isEmpty() bool { // nil check to allow unset improvements m.ImprovedPerformance == nil && m.WebKey == nil && - m.DebugOIDCParentError == nil + m.DebugOIDCParentError == nil && + m.OIDCSingleV1SessionTermination == nil } func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) { diff --git a/internal/command/instance_features_model.go b/internal/command/instance_features_model.go index 417b14dba..218b62864 100644 --- a/internal/command/instance_features_model.go +++ b/internal/command/instance_features_model.go @@ -69,6 +69,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceImprovedPerformanceEventType, feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, + feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, ). Builder().ResourceOwner(m.ResourceOwner) } @@ -108,6 +109,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an case feature.KeyDebugOIDCParentError: v := value.(bool) features.DebugOIDCParentError = &v + case feature.KeyOIDCSingleV1SessionTermination: + v := value.(bool) + features.OIDCSingleV1SessionTermination = &v } } @@ -123,5 +127,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DebugOIDCParentError, f.DebugOIDCParentError, feature_v2.InstanceDebugOIDCParentErrorEventType) + cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType) return cmds } diff --git a/internal/command/instance_features_test.go b/internal/command/instance_features_test.go index 6ca59b00e..e6b6bb434 100644 --- a/internal/command/instance_features_test.go +++ b/internal/command/instance_features_test.go @@ -208,6 +208,10 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceActionsEventType, true, ), + feature_v2.NewSetEvent[bool]( + ctx, aggregate, + feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, true, + ), ), ), args: args{ctx, &InstanceFeatures{ @@ -216,6 +220,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), Actions: gu.Ptr(true), + OIDCSingleV1SessionTermination: gu.Ptr(true), }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", @@ -246,6 +251,10 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceLegacyIntrospectionEventType, true, )), + feature_v2.NewSetEvent[bool]( + context.Background(), aggregate, + feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, false, + ), ), expectPush( feature_v2.NewSetEvent[bool]( @@ -262,6 +271,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { LoginDefaultOrg: gu.Ptr(true), TriggerIntrospectionProjections: gu.Ptr(false), LegacyIntrospection: gu.Ptr(true), + OIDCSingleV1SessionTermination: gu.Ptr(false), }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", diff --git a/internal/command/system_features.go b/internal/command/system_features.go index 5aa7c8dda..1dcb3765a 100644 --- a/internal/command/system_features.go +++ b/internal/command/system_features.go @@ -17,6 +17,7 @@ type SystemFeatures struct { UserSchema *bool Actions *bool ImprovedPerformance []feature.ImprovedPerformanceType + OIDCSingleV1SessionTermination *bool } func (m *SystemFeatures) isEmpty() bool { @@ -27,7 +28,8 @@ func (m *SystemFeatures) isEmpty() bool { m.TokenExchange == nil && m.Actions == nil && // nil check to allow unset improvements - m.ImprovedPerformance == nil + m.ImprovedPerformance == nil && + m.OIDCSingleV1SessionTermination == nil } func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) { diff --git a/internal/command/system_features_model.go b/internal/command/system_features_model.go index 46b36d15a..4c169ec69 100644 --- a/internal/command/system_features_model.go +++ b/internal/command/system_features_model.go @@ -60,6 +60,7 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.SystemTokenExchangeEventType, feature_v2.SystemActionsEventType, feature_v2.SystemImprovedPerformanceEventType, + feature_v2.SystemOIDCSingleV1SessionTerminationEventType, ). Builder().ResourceOwner(m.ResourceOwner) } @@ -92,6 +93,9 @@ func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) { features.Actions = &v case feature.KeyImprovedPerformance: features.ImprovedPerformance = value.([]feature.ImprovedPerformanceType) + case feature.KeyOIDCSingleV1SessionTermination: + v := value.(bool) + features.OIDCSingleV1SessionTermination = &v } } @@ -105,6 +109,7 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.SystemTokenExchangeEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.SystemActionsEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.SystemImprovedPerformanceEventType) + cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.SystemOIDCSingleV1SessionTerminationEventType) return cmds } diff --git a/internal/command/system_features_test.go b/internal/command/system_features_test.go index d6d0d6b5c..9c5f4cc2a 100644 --- a/internal/command/system_features_test.go +++ b/internal/command/system_features_test.go @@ -176,6 +176,10 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemActionsEventType, true, ), + feature_v2.NewSetEvent[bool]( + context.Background(), aggregate, + feature_v2.SystemOIDCSingleV1SessionTerminationEventType, true, + ), ), ), args: args{context.Background(), &SystemFeatures{ @@ -184,6 +188,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) { LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), Actions: gu.Ptr(true), + OIDCSingleV1SessionTermination: gu.Ptr(true), }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", @@ -232,6 +237,10 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemActionsEventType, false, ), + feature_v2.NewSetEvent[bool]( + context.Background(), aggregate, + feature_v2.SystemOIDCSingleV1SessionTerminationEventType, false, + ), ), ), args: args{context.Background(), &SystemFeatures{ @@ -240,6 +249,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) { LegacyIntrospection: gu.Ptr(true), UserSchema: gu.Ptr(true), Actions: gu.Ptr(false), + OIDCSingleV1SessionTermination: gu.Ptr(false), }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", diff --git a/internal/feature/feature.go b/internal/feature/feature.go index b4b2c21d4..de7cdd802 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -16,6 +16,7 @@ const ( KeyImprovedPerformance KeyWebKey KeyDebugOIDCParentError + KeyOIDCSingleV1SessionTermination ) //go:generate enumer -type Level -transform snake -trimprefix Level @@ -41,6 +42,7 @@ type Features struct { ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"` WebKey bool `json:"web_key,omitempty"` DebugOIDCParentError bool `json:"debug_oidc_parent_error,omitempty"` + OIDCSingleV1SessionTermination bool `json:"terminate_single_v1_session,omitempty"` } type ImprovedPerformanceType int32 diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go index c6d12fcb7..cbe3c5bf7 100644 --- a/internal/feature/key_enumer.go +++ b/internal/feature/key_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_error" +const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_errorterminate_single_v1_session" -var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163} +var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163, 190} -const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_error" +const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_errorterminate_single_v1_session" func (i Key) String() string { if i < 0 || i >= Key(len(_KeyIndex)-1) { @@ -34,9 +34,10 @@ func _KeyNoOp() { _ = x[KeyImprovedPerformance-(7)] _ = x[KeyWebKey-(8)] _ = x[KeyDebugOIDCParentError-(9)] + _ = x[KeyOIDCSingleV1SessionTermination-(10)] } -var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError} +var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination} var _KeyNameToValueMap = map[string]Key{ _KeyName[0:11]: KeyUnspecified, @@ -59,6 +60,8 @@ var _KeyNameToValueMap = map[string]Key{ _KeyLowerName[133:140]: KeyWebKey, _KeyName[140:163]: KeyDebugOIDCParentError, _KeyLowerName[140:163]: KeyDebugOIDCParentError, + _KeyName[163:190]: KeyOIDCSingleV1SessionTermination, + _KeyLowerName[163:190]: KeyOIDCSingleV1SessionTermination, } var _KeyNames = []string{ @@ -72,6 +75,7 @@ var _KeyNames = []string{ _KeyName[113:133], _KeyName[133:140], _KeyName[140:163], + _KeyName[163:190], } // KeyString retrieves an enum value from the enum constants string name. diff --git a/internal/query/instance_features.go b/internal/query/instance_features.go index 40bea9d8b..5f501faea 100644 --- a/internal/query/instance_features.go +++ b/internal/query/instance_features.go @@ -18,6 +18,7 @@ type InstanceFeatures struct { ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] WebKey FeatureSource[bool] DebugOIDCParentError FeatureSource[bool] + OIDCSingleV1SessionTermination FeatureSource[bool] } func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) { diff --git a/internal/query/instance_features_model.go b/internal/query/instance_features_model.go index 134aafa67..d1a083319 100644 --- a/internal/query/instance_features_model.go +++ b/internal/query/instance_features_model.go @@ -69,6 +69,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceImprovedPerformanceEventType, feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, + feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, ). Builder().ResourceOwner(m.ResourceOwner) } @@ -92,6 +93,7 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool { m.instance.TokenExchange = m.system.TokenExchange m.instance.Actions = m.system.Actions m.instance.ImprovedPerformance = m.system.ImprovedPerformance + m.instance.OIDCSingleV1SessionTermination = m.system.OIDCSingleV1SessionTermination return true } @@ -121,6 +123,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ features.WebKey.set(level, event.Value) case feature.KeyDebugOIDCParentError: features.DebugOIDCParentError.set(level, event.Value) + case feature.KeyOIDCSingleV1SessionTermination: + features.OIDCSingleV1SessionTermination.set(level, event.Value) } return nil } diff --git a/internal/query/projection/instance_features.go b/internal/query/projection/instance_features.go index c0aaa8c3a..3eb2073c0 100644 --- a/internal/query/projection/instance_features.go +++ b/internal/query/projection/instance_features.go @@ -96,6 +96,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.InstanceDebugOIDCParentErrorEventType, Reduce: reduceInstanceSetFeature[bool], }, + { + Event: feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, + Reduce: reduceInstanceSetFeature[bool], + }, { Event: instance.InstanceRemovedEventType, Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol), diff --git a/internal/query/system_features.go b/internal/query/system_features.go index 33e5f14b3..940cb8fec 100644 --- a/internal/query/system_features.go +++ b/internal/query/system_features.go @@ -27,6 +27,7 @@ type SystemFeatures struct { TokenExchange FeatureSource[bool] Actions FeatureSource[bool] ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] + OIDCSingleV1SessionTermination FeatureSource[bool] } func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) { diff --git a/internal/query/system_features_model.go b/internal/query/system_features_model.go index 9b185c3e8..efb18f43b 100644 --- a/internal/query/system_features_model.go +++ b/internal/query/system_features_model.go @@ -57,6 +57,7 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.SystemTokenExchangeEventType, feature_v2.SystemActionsEventType, feature_v2.SystemImprovedPerformanceEventType, + feature_v2.SystemOIDCSingleV1SessionTerminationEventType, ). Builder().ResourceOwner(m.ResourceOwner) } @@ -88,6 +89,8 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S features.Actions.set(level, event.Value) case feature.KeyImprovedPerformance: features.ImprovedPerformance.set(level, event.Value) + case feature.KeyOIDCSingleV1SessionTermination: + features.OIDCSingleV1SessionTermination.set(level, event.Value) } return nil } diff --git a/internal/repository/feature/feature_v2/eventstore.go b/internal/repository/feature/feature_v2/eventstore.go index eebed8a3b..09988bb97 100644 --- a/internal/repository/feature/feature_v2/eventstore.go +++ b/internal/repository/feature/feature_v2/eventstore.go @@ -14,6 +14,7 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SystemTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) + eventstore.RegisterFilterEventMapper(AggregateType, InstanceOIDCSingleV1SessionTerminationEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceResetEventType, eventstore.GenericEventMapper[ResetEvent]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]]) @@ -25,4 +26,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceDebugOIDCParentErrorEventType, eventstore.GenericEventMapper[SetEvent[bool]]) + eventstore.RegisterFilterEventMapper(AggregateType, InstanceOIDCSingleV1SessionTerminationEventType, eventstore.GenericEventMapper[SetEvent[bool]]) } diff --git a/internal/repository/feature/feature_v2/feature.go b/internal/repository/feature/feature_v2/feature.go index 4c056c235..b88fba1a3 100644 --- a/internal/repository/feature/feature_v2/feature.go +++ b/internal/repository/feature/feature_v2/feature.go @@ -19,6 +19,7 @@ var ( SystemTokenExchangeEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTokenExchange) SystemActionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyActions) SystemImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyImprovedPerformance) + SystemOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyOIDCSingleV1SessionTermination) InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance) InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg) @@ -30,6 +31,7 @@ var ( InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance) InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey) InstanceDebugOIDCParentErrorEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDebugOIDCParentError) + InstanceOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyOIDCSingleV1SessionTermination) ) const ( diff --git a/internal/user/repository/view/active_user_ids_by_session_id.sql b/internal/user/repository/view/active_user_ids_by_session_id.sql new file mode 100644 index 000000000..b7c5aaebb --- /dev/null +++ b/internal/user/repository/view/active_user_ids_by_session_id.sql @@ -0,0 +1,11 @@ +SELECT + s.user_agent_id, + s.user_id +FROM auth.user_sessions s + JOIN auth.user_sessions s2 + ON s.instance_id = s2.instance_id + AND s.user_agent_id = s2.user_agent_id +WHERE + s2.id = $1 + AND s.instance_id = $2 + AND s.state = 0; diff --git a/internal/user/repository/view/model/user_session.go b/internal/user/repository/view/model/user_session.go index 03ecc592b..2c7c7891e 100644 --- a/internal/user/repository/view/model/user_session.go +++ b/internal/user/repository/view/model/user_session.go @@ -63,6 +63,11 @@ type UserSessionView struct { ID sql.NullString `json:"id" gorm:"-"` } +type ActiveUserAgentUserIDs struct { + UserAgentID string + UserIDs []string +} + type userAgentIDPayload struct { ID string `json:"userAgentID"` } diff --git a/internal/user/repository/view/user_agent_by_user_session_id.sql b/internal/user/repository/view/user_agent_by_user_session_id.sql new file mode 100644 index 000000000..c53bbb91e --- /dev/null +++ b/internal/user/repository/view/user_agent_by_user_session_id.sql @@ -0,0 +1,7 @@ +SELECT + s.user_agent_id +FROM auth.user_sessions s +WHERE + s.id = $1 + AND s.instance_id = $2 +LIMIT 1; \ No newline at end of file diff --git a/internal/user/repository/view/user_session_view.go b/internal/user/repository/view/user_session_view.go index 35c1bfc74..f0b956e05 100644 --- a/internal/user/repository/view/user_session_view.go +++ b/internal/user/repository/view/user_session_view.go @@ -1,6 +1,7 @@ package view import ( + "context" "database/sql" _ "embed" "errors" @@ -16,8 +17,15 @@ var userSessionByIDQuery string //go:embed user_sessions_by_user_agent.sql var userSessionsByUserAgentQuery string -func UserSessionByIDs(db *database.DB, agentID, userID, instanceID string) (userSession *model.UserSessionView, err error) { - err = db.QueryRow( +//go:embed user_agent_by_user_session_id.sql +var userAgentByUserSessionIDQuery string + +//go:embed active_user_ids_by_session_id.sql +var activeUserIDsBySessionIDQuery string + +func UserSessionByIDs(ctx context.Context, db *database.DB, agentID, userID, instanceID string) (userSession *model.UserSessionView, err error) { + err = db.QueryRowContext( + ctx, func(row *sql.Row) error { userSession, err = scanUserSession(row) return err @@ -29,8 +37,10 @@ func UserSessionByIDs(db *database.DB, agentID, userID, instanceID string) (user ) return userSession, err } -func UserSessionsByAgentID(db *database.DB, agentID, instanceID string) (userSessions []*model.UserSessionView, err error) { - err = db.Query( + +func UserSessionsByAgentID(ctx context.Context, db *database.DB, agentID, instanceID string) (userSessions []*model.UserSessionView, err error) { + err = db.QueryContext( + ctx, func(rows *sql.Rows) error { userSessions, err = scanUserSessions(rows) return err @@ -42,6 +52,51 @@ func UserSessionsByAgentID(db *database.DB, agentID, instanceID string) (userSes return userSessions, err } +func UserAgentIDBySessionID(ctx context.Context, db *database.DB, sessionID, instanceID string) (userAgentID string, err error) { + err = db.QueryRowContext( + ctx, + func(row *sql.Row) error { + return row.Scan(&userAgentID) + }, + userAgentByUserSessionIDQuery, + sessionID, + instanceID, + ) + return userAgentID, err +} + +// ActiveUserIDsBySessionID returns all userIDs with an active session on the same user agent (its id is also returned) based on a sessionID +func ActiveUserIDsBySessionID(ctx context.Context, db *database.DB, sessionID, instanceID string) (userAgentID string, userIDs []string, err error) { + err = db.QueryContext( + ctx, + func(rows *sql.Rows) error { + userAgentID, userIDs, err = scanActiveUserAgentUserIDs(rows) + return err + }, + activeUserIDsBySessionIDQuery, + sessionID, + instanceID, + ) + return userAgentID, userIDs, err +} + +func scanActiveUserAgentUserIDs(rows *sql.Rows) (userAgentID string, userIDs []string, err error) { + for rows.Next() { + var userID string + err := rows.Scan( + &userAgentID, + &userID) + if err != nil { + return "", nil, err + } + userIDs = append(userIDs, userID) + } + if err := rows.Close(); err != nil { + return "", nil, zerrors.ThrowInternal(err, "VIEW-Sbrws", "Errors.Query.CloseRows") + } + return userAgentID, userIDs, nil +} + func scanUserSession(row *sql.Row) (*model.UserSessionView, error) { session := new(model.UserSessionView) err := row.Scan( diff --git a/proto/zitadel/feature/v2/instance.proto b/proto/zitadel/feature/v2/instance.proto index 159c63990..4dc261f06 100644 --- a/proto/zitadel/feature/v2/instance.proto +++ b/proto/zitadel/feature/v2/instance.proto @@ -72,6 +72,13 @@ message SetInstanceFeaturesRequest{ description: "Return parent errors to OIDC clients for debugging purposes. Parent errors may contain sensitive data or unwanted details about the system status of zitadel. Only enable if really needed."; } ]; + + optional bool oidc_single_v1_session_termination = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions."; + } + ]; } message SetInstanceFeaturesResponse { @@ -157,4 +164,11 @@ message GetInstanceFeaturesResponse { description: "Return parent errors to OIDC clients for debugging purposes. Parent errors may contain sensitive data or unwanted details about the system status of zitadel. Only enable if really needed."; } ]; + + FeatureFlag oidc_single_v1_session_termination = 11 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions."; + } + ]; } diff --git a/proto/zitadel/feature/v2/system.proto b/proto/zitadel/feature/v2/system.proto index 48b3ff56d..29d8824da 100644 --- a/proto/zitadel/feature/v2/system.proto +++ b/proto/zitadel/feature/v2/system.proto @@ -61,6 +61,13 @@ message SetSystemFeaturesRequest{ description: "Improves performance of specified execution paths."; } ]; + + optional bool oidc_single_v1_session_termination = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions."; + } + ]; } message SetSystemFeaturesResponse { @@ -125,4 +132,11 @@ message GetSystemFeaturesResponse { description: "Improves performance of specified execution paths."; } ]; + + FeatureFlag oidc_single_v1_session_termination = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions."; + } + ]; } diff --git a/proto/zitadel/feature/v2beta/instance.proto b/proto/zitadel/feature/v2beta/instance.proto index 2059abf17..865a1d230 100644 --- a/proto/zitadel/feature/v2beta/instance.proto +++ b/proto/zitadel/feature/v2beta/instance.proto @@ -72,6 +72,13 @@ message SetInstanceFeaturesRequest{ description: "Return parent errors to OIDC clients for debugging purposes. Parent errors may contain sensitive data or unwanted details about the system status of zitadel. Only enable if really needed."; } ]; + + optional bool oidc_single_v1_session_termination = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions."; + } + ]; } message SetInstanceFeaturesResponse { @@ -157,4 +164,11 @@ message GetInstanceFeaturesResponse { description: "Return parent errors to OIDC clients for debugging purposes. Parent errors may contain sensitive data or unwanted details about the system status of zitadel. Only enable if really needed."; } ]; + + FeatureFlag oidc_single_v1_session_termination = 11 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions."; + } + ]; } diff --git a/proto/zitadel/feature/v2beta/system.proto b/proto/zitadel/feature/v2beta/system.proto index 048f25391..98b37ad89 100644 --- a/proto/zitadel/feature/v2beta/system.proto +++ b/proto/zitadel/feature/v2beta/system.proto @@ -61,6 +61,13 @@ message SetSystemFeaturesRequest{ description: "Improves performance of specified execution paths."; } ]; + + optional bool oidc_single_v1_session_termination = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions."; + } + ]; } message SetSystemFeaturesResponse { @@ -125,4 +132,11 @@ message GetSystemFeaturesResponse { description: "Improves performance of specified execution paths."; } ]; + + FeatureFlag oidc_single_v1_session_termination = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "If the flag is enabled, you'll be able to terminate a single session from the login UI by providing an id_token with a `sid` claim as id_token_hint on the end_session endpoint. Note that currently all sessions from the same user agent (browser) are terminated in the login UI. Sessions managed through the Session API already allow the termination of single sessions."; + } + ]; }