Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
d1dc243
feat(auth): migrate OAuth 2.0 / OIDC provider from plugin to core
swicken Apr 17, 2026
c3bbf17
docs(auth): add dotAuth phase-2 design doc and implementation plan
swicken Apr 20, 2026
ba3e764
refactor(auth): rename AppSecrets key dotOAuth -> dotAuth
swicken Apr 20, 2026
ddb09cf
chore(auth): remove dotOAuth apps YAML; portlet is the sole editor
swicken Apr 20, 2026
3908c32
feat(auth): add dotAuth REST DTOs
swicken Apr 20, 2026
e9827ff
feat(auth): add DotAuthResource REST endpoints
swicken Apr 20, 2026
cd531d6
feat(auth): wire /v1/dotauth into DotRestApplication
swicken Apr 20, 2026
2dab72b
feat(auth): register dotAuth portlet in portlet.xml
swicken Apr 20, 2026
b1e54d4
feat(auth): add dotAuth i18n keys
swicken Apr 20, 2026
7ec57aa
feat(dot-auth): scaffold Nx library libs/portlets/dot-auth
swicken Apr 20, 2026
202beb1
feat(dot-auth): data-access service and models
swicken Apr 20, 2026
0fada55
feat(dot-auth): list SignalStore
swicken Apr 20, 2026
bedbb55
feat(dot-auth): list component, edit dialog, shell and routes
swicken Apr 20, 2026
1fb1163
feat(dot-auth): register dotAuth route in app shell
swicken Apr 20, 2026
3197618
feat(dot-auth): add startup task to register dotAuth portlet in Setti…
swicken Apr 20, 2026
0d2b16d
fix(dot-auth): rename p-inputSwitch to p-toggleswitch; apply lint aut…
swicken Apr 20, 2026
74b3c1e
fix(auth): lowercase host identifiers when looking up in appKeysByHos…
swicken Apr 20, 2026
a08ce10
fix(auth): lowercase APP_KEY lookup in listSites; add temp /v1/dotaut…
swicken Apr 20, 2026
95f816e
docs(auth): add dotAuth phase-3 SAML integration design
swicken Apr 20, 2026
b2fa265
docs(auth): add dotAuth phase-3 implementation plan
swicken Apr 20, 2026
63b70a9
feat(auth): add DotAuthProtocol enum
swicken Apr 20, 2026
d3ba7dd
feat(auth): add ProtocolHandler strategy interface
swicken Apr 20, 2026
eb6f0e3
refactor(auth): extract OAuthProtocolHandler from DotAuthResource
swicken Apr 20, 2026
317a54c
feat(auth): add SamlProtocolHandler with SAML_SECRET_KEYS
swicken Apr 20, 2026
60d4a49
feat(auth): add protocol field to DotAuth REST DTOs
swicken Apr 20, 2026
960deb1
chore(auth): update phase-3 tasks.json — tasks 0-4,6 complete
swicken Apr 21, 2026
7ce6ac2
feat(auth): DotAuthResource dispatches on protocol; dual-key read/wri…
swicken Apr 21, 2026
00ebc7d
feat(dot-auth): widen models to discriminated union on protocol
swicken Apr 21, 2026
5f0d023
feat(dot-auth): protocol column + composed status tag in list
swicken Apr 21, 2026
b708540
feat(dot-auth): protocol toggle + SAML fieldsets in edit dialog
swicken Apr 21, 2026
31c97cc
feat(dot-auth): i18n keys for SAML protocol + fieldsets
swicken Apr 21, 2026
a9c2a82
test(dot-auth): SAML coverage for list component + storeAlso fix stor…
swicken Apr 21, 2026
1254c4b
test(dot-auth): SAML + protocol-switch coverage for edit component
swicken Apr 21, 2026
6cba5ab
fix(auth): correct HostAPI import in DotAuthResourceTest
swicken Apr 21, 2026
66282e8
fix(dot-auth): use TextareaModule + pTextarea; stabilize list/store s…
swicken Apr 21, 2026
e87823c
fix(dot-auth): drop unused store variable in list component spec
swicken Apr 21, 2026
a88c245
feat(dot-auth): SAML custom attributes + metadata download button
swicken Apr 21, 2026
c7291cf
feat(dot-auth): widen edit dialog to min(96vw, 1040px) for SAML density
swicken Apr 21, 2026
1030ee8
feat(dot-auth): redirect /apps/dotsaml-config to the dotAuth portlet
swicken Apr 21, 2026
6144dac
feat(dot-auth): hide dotsaml-config from the Apps grid
swicken Apr 21, 2026
dc3f1fc
chore(auth): remove temp /v1/dotauth/debug endpoint
swicken Apr 21, 2026
a17de22
chore(auth): finalize phase-3 tasks.json — all tasks complete
swicken Apr 21, 2026
1b6deb6
refactor(dot-auth): address PR review findings
swicken Apr 22, 2026
e0bfa63
style(dot-auth): prettier-format shell template and route callback
swicken Apr 22, 2026
a7d176a
fix(oauth): harden OAuth/OIDC flow against PR-review security findings
swicken Apr 22, 2026
1814346
fix(dotauth): harden REST endpoints, startup task, and plugin path-co…
swicken Apr 22, 2026
2112384
refactor(dot-auth): align components with ANGULAR_STANDARDS
swicken Apr 22, 2026
e512498
test(dot-auth): close PR-review coverage gaps in the dotAuth spec suite
swicken Apr 22, 2026
455517d
chore(dot-auth): remove planning docs from PR scope
swicken Apr 22, 2026
b8648a5
fix(oauth): tighten OIDC/OAuth flow against alg, mode, session, and V…
swicken Apr 22, 2026
ba4101e
fix(dotauth): save-first atomicity, case-insensitive SYSTEM_HOST, SAM…
swicken Apr 22, 2026
d78dc9c
test(dotauth): normalize new backend tests to JUnit 5
swicken Apr 22, 2026
bf35891
fix(dot-auth): @for track order, loadSites recovery, edit-component e…
swicken Apr 22, 2026
f6c7b7f
fix(dot-apps): scope import-success Subject and rename export-button …
swicken Apr 22, 2026
a4fa787
feat(oauth): add BuildRolesStrategy and extract resolveOrProvisionUser
swicken Apr 23, 2026
15d8708
feat(oidc): expose validateIdTokenAndExtractClaims returning full cla…
swicken Apr 23, 2026
8affe53
feat(dotauth): add DotAuthSession model and cache for session-ref bea…
swicken Apr 23, 2026
11667cb
feat(dotauth): add OAuth exchange endpoint and session-ref credential…
swicken Apr 23, 2026
418c064
chore(build): enable maven build cache
swicken Apr 23, 2026
1921e26
chore(justfile): add dev-run-headless recipes for oauth exchange flag
swicken Apr 23, 2026
1c727d1
fix(dotauth): tighten OIDC iss check + stop leaking exception detail
swicken Apr 24, 2026
8466700
test(dotauth): cover the exchange guard paths, role-strategy, and OID…
swicken Apr 24, 2026
8d05a87
docs(dotauth): flag OAuth role-wipe default and logout-via-bearer tra…
swicken Apr 24, 2026
47bc33f
fix(dotauth): close role-sync race and clampExpirationDays misconfig …
swicken Apr 24, 2026
f006617
fix(auth): address dotAuth review findings
swicken Apr 24, 2026
b2d94e6
fix(auth): address jsanca review feedback
swicken Apr 24, 2026
5c1017b
fix(auth): avoid leaking OIDC validation details
swicken Apr 24, 2026
67ea75e
fix(oauth): sync JIT user profile names
swicken Apr 24, 2026
325e889
Hash OAuth user ids
swicken Apr 27, 2026
55a4175
feat(dotauth): add OAuth role sync strategy option
swicken Apr 28, 2026
6a7e494
proper error handling
swicken Apr 28, 2026
f0e4f5e
make things required
swicken Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,14 @@
<version>0.9.1</version>
</dependency>

<!-- Nimbus JOSE+JWT: id_token JWKS signature verification for the OIDC flow. -->
<!-- jjwt 0.9.x only handles HMAC; nimbus adds RS256/ES256 + RemoteJWKSet. -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37.3</version>
</dependency>

<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
Expand Down
7 changes: 7 additions & 0 deletions core-web/apps/dotcms-ui/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ const PORTLETS_ANGULAR: Route[] = [
path: 'usage',
loadChildren: () => import('@dotcms/portlets/dot-usage').then((m) => m.dotUsageRoutes)
},
{
path: 'dotAuth',
canActivate: [MenuGuardService],
canActivateChild: [MenuGuardService],
data: { reuseRoute: false },
loadChildren: () => import('@dotcms/portlets/dot-auth/portlet').then((m) => m.dotAuthRoutes)
},
{
path: 'tags',
canActivate: [MenuGuardService],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ const initialState: DotAppsImportExportDialogState = {
errorMessage: null
};

// Subject to emit when import succeeds
const importSuccessSubject = new Subject<void>();

export const DotAppsImportExportDialogStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
Expand All @@ -63,12 +60,14 @@ export const DotAppsImportExportDialogStore = signalStore(
return '';
})
})),
withProps(() => ({
/**
* Observable that emits when import succeeds
*/
importSuccess$: importSuccessSubject.asObservable()
})),
withProps(() => {
const importSuccessSubject = new Subject<void>();

return {
_importSuccessSubject: importSuccessSubject,
importSuccess$: importSuccessSubject.asObservable()
};
}),
withMethods((store) => {
const dotAppsService = inject(DotAppsService);
const dotMessageService = inject(DotMessageService);
Expand All @@ -77,7 +76,7 @@ export const DotAppsImportExportDialogStore = signalStore(
/**
* Open the export dialog
*/
openExport: (app: DotApp, site?: DotAppsSite) => {
openExport: (app: DotApp | null, site?: DotAppsSite) => {
patchState(store, {
visible: true,
action: dialogAction.EXPORT,
Expand Down Expand Up @@ -185,7 +184,7 @@ export const DotAppsImportExportDialogStore = signalStore(
next: (status: string) => {
if (status !== '400') {
patchState(store, initialState);
importSuccessSubject.next();
store._importSuccessSubject.next();
} else {
patchState(store, {
status: ComponentStatus.ERROR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<button
(click)="openExportDialog()"
[label]="'apps.confirmation.export.all.button' | dm"
[disabled]="!isExportButtonDisabled()"
[disabled]="!hasExportableApps()"
class="dot-apps-configuration__action_export_button"
pButton
link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe('DotAppsListComponent', () => {
describe('Export Button State', () => {
it('should enable export button when apps have configurations', () => {
// appsResponse has one app with configurationsCount: 1
expect(spectator.component.isExportButtonDisabled()).toBe(true);
expect(spectator.component.hasExportableApps()).toBe(true);
});

it('should disable export button when no apps have configurations', () => {
Expand All @@ -133,7 +133,32 @@ describe('DotAppsListComponent', () => {
spectator.component.reloadAppsData();
spectator.detectChanges();

expect(spectator.component.isExportButtonDisabled()).toBe(false);
expect(spectator.component.hasExportableApps()).toBe(false);
});

it('should enable export when only hidden SAML app has configurations', () => {
const appsWithOnlyHiddenConfig: DotApp[] = [
{ ...appsResponse[0], configurationsCount: 0 },
{ ...appsResponse[1], configurationsCount: 0 },
{
allowExtraParams: true,
configurationsCount: 1,
key: 'dotsaml-config',
name: 'SAML',
description: 'SAML config'
}
];
mockDotAppsService.get.mockReturnValue(of(appsWithOnlyHiddenConfig));

spectator.component.reloadAppsData();
spectator.detectChanges();

expect(spectator.component.hasExportableApps()).toBe(true);
expect(
spectator.component.state
.displayedApps()
.some((app) => app.key === 'dotsaml-config')
).toBe(false);
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { patchState, signalState } from '@ngrx/signals';
import { fromEvent as observableFromEvent } from 'rxjs';

import { Component, DestroyRef, ElementRef, AfterViewInit, inject, viewChild } from '@angular/core';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
DestroyRef,
ElementRef,
inject,
viewChild
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';

Expand All @@ -25,6 +33,16 @@ interface DotAppsListState {
displayedApps: DotApp[];
}

/**
* App keys whose config is edited through a dedicated portlet rather than the
* generic Apps UI. The cards are hidden from the grid; navigation to
* `/apps/<key>` is separately redirected by {@link dotAppsSamlRedirectGuard}.
*/
const HIDDEN_APP_KEYS: ReadonlySet<string> = new Set(['dotsaml-config']);

const withoutHiddenApps = (apps: DotApp[]): DotApp[] =>
apps.filter((app) => !HIDDEN_APP_KEYS.has(app.key));

@Component({
selector: 'dot-apps-list',
templateUrl: './dot-apps-list.component.html',
Expand All @@ -36,7 +54,8 @@ interface DotAppsListState {
DotAppsImportExportDialogComponent,
DotPortletBaseComponent,
DotMessagePipe
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DotAppsListComponent implements AfterViewInit {
readonly #route = inject(ActivatedRoute);
Expand Down Expand Up @@ -88,15 +107,16 @@ export class DotAppsListComponent implements AfterViewInit {
* Opens the Export dialog for all configurations
*/
openExportDialog(): void {
// For export all, we don't pass an app - the store handles this
this.#dialogStore.openExport(null as unknown as DotApp);
// For export all, we don't pass an app the store handles null to mean "all apps".
this.#dialogStore.openExport(null);
}

/**
* Checks if export button is disabled based on existing configurations
* True when at least one app has a saved configuration — the export button is
* enabled only in that case.
*/
isExportButtonDisabled(): boolean {
return this.state.allApps().filter((app: DotApp) => app.configurationsCount).length > 0;
hasExportableApps(): boolean {
return this.state.allApps().some((app: DotApp) => !!app.configurationsCount);
}

/**
Expand All @@ -114,7 +134,7 @@ export class DotAppsListComponent implements AfterViewInit {
private initAppsState(apps: DotApp[]): void {
patchState(this.state, {
allApps: apps,
displayedApps: apps
displayedApps: withoutHiddenApps(apps)
});

this.attachFilterEvents();
Expand All @@ -134,10 +154,13 @@ export class DotAppsListComponent implements AfterViewInit {
}

private filterApps(searchCriteria?: string): void {
this.#dotAppsService.get(searchCriteria).subscribe((apps: DotApp[]) => {
patchState(this.state, {
displayedApps: apps
this.#dotAppsService
.get(searchCriteria)
.pipe(take(1), takeUntilDestroyed(this.#destroyRef))
.subscribe((apps: DotApp[]) => {
patchState(this.state, {
displayedApps: withoutHiddenApps(apps)
});
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DotAiConfigDetailComponent } from './components/dot-ai-config-detail/do
import { DotAppsConfigurationComponent } from './components/dot-apps-configuration/dot-apps-configuration.component';
import { DotAppsConfigurationDetailComponent } from './components/dot-apps-configuration-detail/dot-apps-configuration-detail.component';
import { DotAppsListComponent } from './dot-apps-list/dot-apps-list.component';
import { dotAppsSamlRedirectGuard } from './guards/dot-apps-saml-redirect.guard';
import { DotAppsConfigurationDetailResolver } from './services/dot-apps-configuration-detail-resolver/dot-apps-configuration-detail-resolver.service';
import { DotAppsConfigurationResolver } from './services/dot-apps-configuration-resolver/dot-apps-configuration-resolver.service';
import { DotAppsListResolver } from './services/dot-apps-list-resolver/dot-apps-list-resolver.service';
Expand All @@ -23,6 +24,7 @@ export const dotAppsRoutes: Routes = [
{
component: DotAppsConfigurationDetailComponent,
path: ':appKey/create/:id',
canActivate: [dotAppsSamlRedirectGuard],
resolve: {
data: DotAppsConfigurationDetailResolver
},
Expand All @@ -31,6 +33,7 @@ export const dotAppsRoutes: Routes = [
{
component: DotAppsConfigurationDetailComponent,
path: ':appKey/edit/:id',
canActivate: [dotAppsSamlRedirectGuard],
resolve: {
data: DotAppsConfigurationDetailResolver
},
Expand All @@ -39,6 +42,7 @@ export const dotAppsRoutes: Routes = [
{
component: DotAppsConfigurationComponent,
path: ':appKey',
canActivate: [dotAppsSamlRedirectGuard],
resolve: {
data: DotAppsConfigurationResolver
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { inject } from '@angular/core';
import { CanActivateFn, Router, UrlTree } from '@angular/router';

/** AppSecrets key for SAML, mirrors `DotSamlProxyFactory.SAML_APP_CONFIG_KEY`. */
const SAML_APP_KEY = 'dotsaml-config';

/** Shell path of the dotAuth portlet, registered in `app.routes.ts`. */
const DOT_AUTH_PATH = '/dotAuth';

/**
* Guards the `/apps/:appKey` routes (list, create, edit): if a user lands on
* a SAML path — typically from a bookmark or an old docs link — route them to
* the dotAuth portlet, which is the sole editor for SAML config since phase-3.
*
* `dotsaml-config.yml` is kept in place so the SAML runtime's Apps descriptor
* lookup keeps working; the redirect only affects user navigation.
*/
export const dotAppsSamlRedirectGuard: CanActivateFn = (route): boolean | UrlTree => {
const appKey = route.paramMap.get('appKey');
if (appKey === SAML_APP_KEY) {
return inject(Router).parseUrl(DOT_AUTH_PATH);
}
return true;
};
1 change: 1 addition & 0 deletions core-web/libs/data-access/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './lib/can-deactivate/can-deactivate-guard.service';
export * from './lib/dot-ai/dot-ai.service';
export * from './lib/dot-apps/dot-apps.service';
export * from './lib/dot-alert-confirm/dot-alert-confirm.service';
export * from './lib/dot-auth/dot-auth.service';
export * from './lib/dot-analytics-search/dot-analytics-search.service';
export * from './lib/dot-analytics-tracker/dot-analytics-tracker.service';
export * from './lib/dot-categories/dot-categories.service';
Expand Down
Loading
Loading