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
This commit is contained in:
Ramon
2026-04-22 09:23:50 +02:00
committed by GitHub
parent c0f7bfffdb
commit bfffd69d60
3 changed files with 98 additions and 41 deletions
@@ -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<typeof LoginVersionSchema> = 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<typeof UpdateApplicationRequestSchema> = {
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;
@@ -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<typeof UpdateApplicationRequestSchema>) {
return this.grpcService.application.updateApplication(req);
}
}
+5 -1
View File
@@ -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<typeof createWebKeyServiceClient>;
public organizationNew!: ReturnType<typeof createOrganizationServiceClient>;
public adminNew!: ReturnType<typeof createAdminServiceClient>;
public application!: ReturnType<typeof createApplicationServiceClient>;
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',