From bfffd69d60a8d0d35436afad63c8c4323b90e0b7 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 22 Apr 2026 09:23:50 +0200 Subject: [PATCH] fix(console): normalize empty OIDC redirect uri updates #12053 (#12067) # Which Problems Are Solved - Clearing OIDC redirect URI lists in the console does not persist correctly resulting in no changes error. # How the Problems Are Solved - Normalizes empty OIDC URI list updates to [""] before sending the save request. - Uses the application v2 update call for the affected OIDC config save path. # References - Closes #12053 --- .../apps/app-detail/app-detail.component.ts | 117 ++++++++++++------ .../src/app/services/application.service.ts | 16 +++ console/src/app/services/grpc.service.ts | 6 +- 3 files changed, 98 insertions(+), 41 deletions(-) create mode 100644 console/src/app/services/application.service.ts 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 d2aeb7874..ac18e5b35 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 @@ -6,8 +6,17 @@ import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; +import { MessageInitShape } from '@bufbuild/protobuf'; import { Buffer } from 'buffer'; -import { Duration } from 'google-protobuf/google/protobuf/duration_pb'; +import { LoginVersionSchema } from '@zitadel/proto/zitadel/application/v2/login_pb'; +import { UpdateApplicationRequestSchema } from '@zitadel/proto/zitadel/application/v2/application_service_pb'; +import { + OIDCApplicationType as OIDCV2ApplicationType, + OIDCAuthMethodType as OIDCV2AuthMethodType, + OIDCGrantType as OIDCV2GrantType, + OIDCResponseType as OIDCV2ResponseType, + OIDCTokenType as OIDCV2TokenType, +} from '@zitadel/proto/zitadel/application/v2/oidc_pb'; import { mergeMap, Subject, Subscription } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component'; @@ -35,7 +44,6 @@ import { import { GetOIDCInformationResponse, UpdateAPIAppConfigRequest, - UpdateOIDCAppConfigRequest, UpdateSAMLAppConfigRequest, } from 'src/app/proto/generated/zitadel/management_pb'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; @@ -43,6 +51,7 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; +import { ApplicationService } from 'src/app/services/application.service'; import { EnvironmentService } from 'src/app/services/environment.service'; import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component'; import { @@ -192,6 +201,7 @@ export class AppDetailComponent implements OnInit, OnDestroy { private _location: Location, private dialog: MatDialog, private mgmtService: ManagementService, + private applicationService: ApplicationService, private authService: GrpcAuthService, private router: Router, private breadcrumbService: BreadcrumbService, @@ -669,48 +679,60 @@ export class AppDetailComponent implements OnInit, OnDestroy { this.app.oidcConfig.devMode = !!this.devMode?.value; this.app.oidcConfig.skipNativeAppSuccessPage = !!this.skipNativeAppSuccessPage?.value; - const req = new UpdateOIDCAppConfigRequest(); - req.setProjectId(this.projectId); - req.setAppId(this.app.id); + const loginVersion: MessageInitShape = this.oidcLoginV2?.value + ? { + version: { + case: 'loginV2', + value: { baseUri: this.oidcLoginV2BaseURL?.value }, + }, + } + : { + version: { + case: 'loginV1', + value: {}, + }, + }; - // configuration - req.setResponseTypesList(this.app.oidcConfig.responseTypesList); - req.setAuthMethodType(this.app.oidcConfig.authMethodType); - req.setGrantTypesList(this.app.oidcConfig.grantTypesList); - req.setAppType(this.app.oidcConfig.appType); - req.setBackChannelLogoutUri(this.app.oidcConfig.backChannelLogoutUri); - const login = new LoginVersion(); - if (this.oidcLoginV2?.value) { - const loginV2 = new LoginV2(); - loginV2.setBaseUri(this.oidcLoginV2BaseURL?.value); - login.setLoginV2(loginV2); - } else { - login.setLoginV1(new LoginV1()); - } - req.setLoginVersion(login); + const req: MessageInitShape = { + projectId: this.projectId, + applicationId: this.app.id, + applicationType: { + case: 'oidcConfiguration', + value: { + // configuration + responseTypes: this.app.oidcConfig.responseTypesList.map((type) => this.toOIDCV2ResponseType(type)), + authMethodType: this.app.oidcConfig.authMethodType as unknown as OIDCV2AuthMethodType, + grantTypes: this.app.oidcConfig.grantTypesList as unknown as OIDCV2GrantType[], + applicationType: this.app.oidcConfig.appType as unknown as OIDCV2ApplicationType, + backChannelLogoutUri: this.app.oidcConfig.backChannelLogoutUri, + loginVersion, - // token - req.setAccessTokenType(this.app.oidcConfig.accessTokenType); - req.setAccessTokenRoleAssertion(this.app.oidcConfig.accessTokenRoleAssertion); - req.setIdTokenRoleAssertion(this.app.oidcConfig.idTokenRoleAssertion); - req.setIdTokenUserinfoAssertion(this.app.oidcConfig.idTokenUserinfoAssertion); + // token + accessTokenType: this.app.oidcConfig.accessTokenType as unknown as OIDCV2TokenType, + accessTokenRoleAssertion: this.app.oidcConfig.accessTokenRoleAssertion, + idTokenRoleAssertion: this.app.oidcConfig.idTokenRoleAssertion, + idTokenUserinfoAssertion: this.app.oidcConfig.idTokenUserinfoAssertion, - // redirects - req.setRedirectUrisList(this.app.oidcConfig.redirectUrisList); - req.setAdditionalOriginsList(this.app.oidcConfig.additionalOriginsList); - req.setPostLogoutRedirectUrisList(this.app.oidcConfig.postLogoutRedirectUrisList); - req.setDevMode(this.app.oidcConfig.devMode); - req.setSkipNativeAppSuccessPage(this.app.oidcConfig.skipNativeAppSuccessPage); + // redirects + redirectUris: this.normalizeOIDCListForUpdate(this.app.oidcConfig.redirectUrisList), + additionalOrigins: this.app.oidcConfig.additionalOriginsList, + postLogoutRedirectUris: this.normalizeOIDCListForUpdate(this.app.oidcConfig.postLogoutRedirectUrisList), + developmentMode: this.app.oidcConfig.devMode, + skipNativeAppSuccessPage: this.app.oidcConfig.skipNativeAppSuccessPage, + ...(this.clockSkewSeconds?.value + ? { + clockSkew: { + seconds: BigInt(Math.floor(this.clockSkewSeconds?.value)), + nanos: Math.floor(this.clockSkewSeconds?.value % 1) * 10000, + }, + } + : {}), + }, + }, + }; - if (this.clockSkewSeconds?.value) { - const dur = new Duration(); - dur.setSeconds(Math.floor(this.clockSkewSeconds?.value)); - dur.setNanos(Math.floor(this.clockSkewSeconds?.value % 1) * 10000); - req.setClockSkew(dur); - } - - this.mgmtService - .updateOIDCAppConfig(req) + this.applicationService + .updateApplication(req) .then(() => { if (this.app?.oidcConfig) { const config = { oidc: this.app.oidcConfig }; @@ -728,6 +750,21 @@ export class AppDetailComponent implements OnInit, OnDestroy { } } + private normalizeOIDCListForUpdate(values: string[]): string[] { + return values.length === 0 ? [''] : values; + } + + private toOIDCV2ResponseType(type: OIDCResponseType): OIDCV2ResponseType { + switch (type) { + case OIDCResponseType.OIDC_RESPONSE_TYPE_CODE: + return OIDCV2ResponseType.OIDC_RESPONSE_TYPE_CODE; + case OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN: + return OIDCV2ResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN; + case OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN: + return OIDCV2ResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN; + } + } + public saveAPIApp(): void { if (this.apiForm.valid && this.app?.apiConfig) { this.app.apiConfig.authMethodType = this.apiAuthMethodType?.value; diff --git a/console/src/app/services/application.service.ts b/console/src/app/services/application.service.ts new file mode 100644 index 000000000..1a9e0573d --- /dev/null +++ b/console/src/app/services/application.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { MessageInitShape } from '@bufbuild/protobuf'; +import { UpdateApplicationRequestSchema } from '@zitadel/proto/zitadel/application/v2/application_service_pb'; + +import { GrpcService } from './grpc.service'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplicationService { + constructor(private readonly grpcService: GrpcService) {} + + public updateApplication(req: MessageInitShape) { + return this.grpcService.application.updateApplication(req); + } +} diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts index 0b2d0fa90..514db5063 100644 --- a/console/src/app/services/grpc.service.ts +++ b/console/src/app/services/grpc.service.ts @@ -26,11 +26,13 @@ import { createAdminServiceClient, createAuthServiceClient, createManagementServ import { createGrpcWebTransport } from '@connectrpc/connect-web'; import { createClientFor } from '@zitadel/client'; -import { WebKeyService } from '@zitadel/proto/zitadel/webkey/v2/webkey_service_pb'; import { ActionService } from '@zitadel/proto/zitadel/action/v2/action_service_pb'; +import { ApplicationService } from '@zitadel/proto/zitadel/application/v2/application_service_pb'; +import { WebKeyService } from '@zitadel/proto/zitadel/webkey/v2/webkey_service_pb'; const createWebKeyServiceClient = createClientFor(WebKeyService); const createActionServiceClient = createClientFor(ActionService); +const createApplicationServiceClient = createClientFor(ApplicationService); @Injectable({ providedIn: 'root', @@ -49,6 +51,7 @@ export class GrpcService { public webKey!: ReturnType; public organizationNew!: ReturnType; public adminNew!: ReturnType; + public application!: ReturnType; public assets!: void; @@ -108,6 +111,7 @@ export class GrpcService { this.webKey = createWebKeyServiceClient(transport); this.organizationNew = createOrganizationServiceClient(transport); this.adminNew = createAdminServiceClient(transportOldAPIs); + this.application = createApplicationServiceClient(transport); const authConfig: AuthConfig = { scope: 'openid profile email',