forked from fa/breadcrumb-the-shire
feat(auth): add microsoft auto-remember policy with tenant override and configurable remember TTL
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"task_id": "AUTH-MICROSOFT-REMEMBER-001",
|
||||
"status": "completed",
|
||||
"executed_at": "2026-03-10T00:00:00Z",
|
||||
"steps": [
|
||||
{
|
||||
"id": "S1",
|
||||
"title": "Datenmodell und Settings-Keys festlegen",
|
||||
"status": "completed",
|
||||
"changes": [
|
||||
"lib/Service/Settings/SettingKeys.php — added MICROSOFT_AUTO_REMEMBER_DEFAULT_KEY and REMEMBER_TOKEN_LIFETIME_DAYS_KEY",
|
||||
"db/init/init.sql — added auto_remember_mode column to tenant_auth_microsoft, added 2 settings seed rows",
|
||||
"db/updates/2026-03-10-microsoft-auto-remember.sql — NEW idempotent update script"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S2",
|
||||
"title": "Settings-Gateway fuer Login-Persistenz implementieren",
|
||||
"status": "completed",
|
||||
"changes": [
|
||||
"lib/Service/Settings/SettingsLoginPersistenceGateway.php — NEW gateway (bool+int with validation, fallbacks)",
|
||||
"lib/Service/Settings/SettingServicesFactory.php — added createSettingsLoginPersistenceGateway()",
|
||||
"lib/App/Container/Registrars/SettingsRegistrar.php — registered gateway + injected into AdminSettingsService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S3",
|
||||
"title": "Tenant-Sso-Service auf Override-Policy erweitern",
|
||||
"status": "completed",
|
||||
"changes": [
|
||||
"lib/Service/Auth/AuthSettingsGateway.php — added SettingsLoginPersistenceGateway dep + 2 delegate methods",
|
||||
"lib/Service/Auth/AuthGatewayFactory.php — added createSettingsLoginPersistenceGateway() + wired to AuthSettingsGateway",
|
||||
"lib/Repository/Tenant/TenantMicrosoftAuthRepository.php — added auto_remember_mode to SELECT/UPSERT",
|
||||
"lib/Service/Auth/TenantSsoService.php — added auto_remember_mode to get/save, normalizeAutoRememberMode(), shouldAutoRememberOnMicrosoftLogin()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S4",
|
||||
"title": "Admin-Settings-Flow erweitern",
|
||||
"status": "completed",
|
||||
"changes": [
|
||||
"lib/Service/Settings/AdminSettingsService.php — added SettingsLoginPersistenceGateway to constructor, buildPageData, updateFromAdmin, audit arrays"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S5",
|
||||
"title": "Tenant-SSO-UI und AuthZ-Feldschutz erweitern",
|
||||
"status": "completed",
|
||||
"changes": [
|
||||
"pages/admin/tenants/_form.phtml — added tri-state select for microsoft_auto_remember_mode in SSO tab",
|
||||
"lib/Service/Access/TenantAuthorizationPolicy.php — added microsoft_auto_remember_mode to containsSsoFields()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S6",
|
||||
"title": "RememberMeService auf dynamische TTL umstellen",
|
||||
"status": "completed",
|
||||
"changes": [
|
||||
"lib/Service/Auth/RememberMeService.php — added nullable AuthSettingsGateway param, modified lifetimeSeconds()",
|
||||
"lib/Service/Auth/AuthServicesFactory.php — passes AuthSettingsGateway to RememberMeService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S7",
|
||||
"title": "Microsoft-Callback um Policy-gesteuertes Remember erweitern",
|
||||
"status": "completed",
|
||||
"changes": [
|
||||
"pages/auth/microsoft/callback().php — added shouldAutoRememberOnMicrosoftLogin + rememberUser call after successful login"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S8",
|
||||
"title": "Tests erweitern und Gates absichern",
|
||||
"status": "completed",
|
||||
"changes": [
|
||||
"tests/Service/Settings/SettingsLoginPersistenceGatewayTest.php — NEW (16 tests: bool/int getter/setter, validation, fallback, seconds computation)",
|
||||
"tests/Service/Settings/AdminSettingsServiceSessionPolicyTest.php — extended with 3 login persistence tests",
|
||||
"tests/Service/Auth/TenantSsoServiceTest.php — extended with 6 shouldAutoRemember + saveTenantMicrosoftAuth tests",
|
||||
"tests/Service/Access/TenantAuthorizationPolicyTest.php — extended with 1 auto_remember_mode SSO field test",
|
||||
"tests/Service/Auth/RememberMeServiceTest.php — extended with 2 dynamic TTL tests + updated helper",
|
||||
"i18n/default_de.json — added 12 translation keys",
|
||||
"i18n/default_en.json — added 12 translation keys",
|
||||
"pages/admin/settings/index(default).phtml — added Login persistence card in Security tab"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S9",
|
||||
"title": "Review-Guard-Findings beheben (Re-Run)",
|
||||
"status": "completed",
|
||||
"changes": [
|
||||
"lib/App/Container/Registrars/SettingsRegistrar.php — fixed import order (ordered_imports)",
|
||||
"lib/Service/Auth/AuthGatewayFactory.php — fixed import order (ordered_imports)",
|
||||
"tests/Service/Settings/AdminSettingsServiceSessionPolicyTest.php — fixed import order (ordered_imports)",
|
||||
"pages/admin/tenants/create().php — added auto_remember_mode to POST repopulation for SSO fields",
|
||||
"pages/admin/tenants/edit($id).php — added auto_remember_mode to POST repopulation for SSO fields"
|
||||
]
|
||||
}
|
||||
],
|
||||
"quality_gates": [
|
||||
{
|
||||
"id": "QG-001",
|
||||
"description": "PHPUnit",
|
||||
"status": "PASS",
|
||||
"evidence": "555 tests, 13476 assertions, 0 failures"
|
||||
},
|
||||
{
|
||||
"id": "QG-002",
|
||||
"description": "PHPStan level 5",
|
||||
"status": "PASS",
|
||||
"evidence": "No errors"
|
||||
},
|
||||
{
|
||||
"id": "QG-003",
|
||||
"description": "CoreStarterkitContractTest",
|
||||
"status": "PASS",
|
||||
"evidence": "11 tests, 6253 assertions, 0 failures"
|
||||
},
|
||||
{
|
||||
"id": "QG-005",
|
||||
"description": "Browser smoke",
|
||||
"status": "PASS",
|
||||
"evidence": "Manually tested: Admin Settings Security tab, Tenant SSO form, and Microsoft login callback verified in browser"
|
||||
},
|
||||
{
|
||||
"id": "QG-006",
|
||||
"description": "PHP CS Fixer",
|
||||
"status": "PASS",
|
||||
"evidence": "530 files checked, 0 fixable issues, exit code 0"
|
||||
}
|
||||
],
|
||||
"success_criteria": [
|
||||
{
|
||||
"id": "SC-001",
|
||||
"status": "PASS",
|
||||
"evidence": "SettingsLoginPersistenceGateway implements isMicrosoftAutoRememberDefault() (fallback false) and getRememberTokenLifetimeDays() (fallback 30, range 1-365). Both included in buildPageData(). 16 unit tests cover all paths."
|
||||
},
|
||||
{
|
||||
"id": "SC-002",
|
||||
"status": "PASS",
|
||||
"evidence": "tenant_auth_microsoft.auto_remember_mode column (NULL/0/1) added. TenantSsoService get/save include the field. Tenant form has tri-state select. 6 tests cover policy resolution."
|
||||
},
|
||||
{
|
||||
"id": "SC-003",
|
||||
"status": "PASS",
|
||||
"evidence": "Microsoft callback calls shouldAutoRememberOnMicrosoftLogin() after loginUserById success only. force_off and global-off+inherit correctly prevent token creation."
|
||||
},
|
||||
{
|
||||
"id": "SC-004",
|
||||
"status": "PASS",
|
||||
"evidence": "RememberMeService.lifetimeSeconds() reads from AuthSettingsGateway when available, falls back to 30-day constant. Token creation and rotation both use lifetimeSeconds(). 2 tests verify dynamic TTL."
|
||||
},
|
||||
{
|
||||
"id": "SC-005",
|
||||
"status": "PASS",
|
||||
"evidence": "All visible text uses t(). 12 keys added to de/en. CSRF/PRG/AuthZ unchanged. containsSsoFields() includes microsoft_auto_remember_mode."
|
||||
},
|
||||
{
|
||||
"id": "SC-006",
|
||||
"status": "PASS",
|
||||
"evidence": "QG-001 (555 tests PASS), QG-002 (0 errors), QG-003 (11 contract tests PASS), QG-006 (CS Fixer PASS, 0 fixable issues, exit 0). New test file + 4 extended test files."
|
||||
}
|
||||
],
|
||||
"resolved_findings": [
|
||||
{
|
||||
"id": "RG-001",
|
||||
"resolution": "Ran composer cs:check (QG-006). Fixed 3 ordered_imports violations in SettingsRegistrar.php, AuthGatewayFactory.php, AdminSettingsServiceSessionPolicyTest.php. Fixed pre-existing blank_line_after_opening_tag in config/settings.php. CS check now exits 0 with 0 fixable issues."
|
||||
},
|
||||
{
|
||||
"id": "RG-002",
|
||||
"resolution": "Added 'auto_remember_mode' => $post['microsoft_auto_remember_mode'] ?? null to the SSO field repopulation array in pages/admin/tenants/create().php. Selection now survives validation errors."
|
||||
},
|
||||
{
|
||||
"id": "RG-003",
|
||||
"resolution": "Added 'auto_remember_mode' => $post['microsoft_auto_remember_mode'] ?? null to the SSO field repopulation array in pages/admin/tenants/edit($id).php. Selection now survives validation errors."
|
||||
}
|
||||
],
|
||||
"open_items": []
|
||||
}
|
||||
10
agent-system/runs/AUTH-MICROSOFT-REMEMBER-001/finalize.json
Normal file
10
agent-system/runs/AUTH-MICROSOFT-REMEMBER-001/finalize.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"task_id": "AUTH-MICROSOFT-REMEMBER-001",
|
||||
"ready_to_finalize": true,
|
||||
"guard_review": "pass",
|
||||
"acceptance_review": "pass",
|
||||
"ci_status": "pass",
|
||||
"final_action": "commit",
|
||||
"commit_message": "feat(auth): add microsoft auto-remember policy with tenant override and configurable remember TTL",
|
||||
"notes": "Guard review pass, acceptance review pass, and all reported quality gates are pass."
|
||||
}
|
||||
225
agent-system/runs/AUTH-MICROSOFT-REMEMBER-001/plan.json
Normal file
225
agent-system/runs/AUTH-MICROSOFT-REMEMBER-001/plan.json
Normal file
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"task_id": "AUTH-MICROSOFT-REMEMBER-001",
|
||||
"summary": "Implementiere steuerbares Auto-Remember fuer erfolgreiche Microsoft-Logins mit globalem Default + Tenant-Override (inherit/force_on/force_off) und fuehre eine globale, konfigurierbare Remember-Token-TTL (in Tagen) fuer alle Remember-Tokens ein. Ziel-Runtime-Kontext: agent-system/runs/AUTH-MICROSOFT-REMEMBER-001/plan.json.",
|
||||
"assumptions": [
|
||||
"Bestehendes Default-Verhalten bleibt unveraendert, bis Konfiguration aktiv gesetzt wird (globaler Auto-Remember-Default = aus).",
|
||||
"Die neue Remember-TTL gilt fuer alle Remember-Tokens (lokaler Login und Microsoft-basierter Remember-Token).",
|
||||
"Es werden keine API-Endpunkte oder OpenAPI-Vertraege geaendert; Scope ist Web-Auth + Admin-Settings/Tenant-UI + Service/Repository.",
|
||||
"Microsoft-OIDC-Handshake bleibt unveraendert; erweitert wird nur das Verhalten nach erfolgreichem Login."
|
||||
],
|
||||
"scope": {
|
||||
"in": [
|
||||
"Globales Settings-Modell fuer Microsoft Auto-Remember Default (bool) und Remember-Token-TTL in Tagen (int) inkl. Validation/Fallbacks.",
|
||||
"Tenant-spezifischer Microsoft-Override mit drei Zustaenden: inherit, force_on, force_off.",
|
||||
"Microsoft-Callback-Flow: Remember-Token wird bei erfolgreicher Microsoft-Anmeldung gemaess effektiver Policy gesetzt.",
|
||||
"RememberMeService-Lifetime auf konfigurierbare TTL umstellen (statt fixer 30 Tage).",
|
||||
"Admin-Settings-UI und Tenant-SSO-UI um neue Steuerungsfelder erweitern inkl. i18n de/en.",
|
||||
"Idempotente DB-Updates in db/updates fuer bestehende Installationen sowie Anpassung von db/init/init.sql fuer Fresh-Install.",
|
||||
"Automatisierte Tests und Quality-Gates fuer neue Logik, Settings-Validierung, Policy-Prioritaet und UI/Contract-Integritaet."
|
||||
],
|
||||
"out": [
|
||||
"Aenderungen an Session Idle/Absolute Timeout-Logik.",
|
||||
"Aenderungen an OIDC Discovery/Token Exchange/JWT Validierung.",
|
||||
"Neues API-Interface fuer diese Konfiguration.",
|
||||
"Aenderungen am bestehenden lokalen Remember-Checkbox-UX im Login-Formular.",
|
||||
"Breite Refactors ausserhalb Auth/Settings/Tenant-SSO."
|
||||
]
|
||||
},
|
||||
"guardrails": {
|
||||
"required_guard_ids": [
|
||||
"GR-CORE-001",
|
||||
"GR-CORE-003",
|
||||
"GR-CORE-005",
|
||||
"GR-CORE-010",
|
||||
"GR-CORE-011",
|
||||
"GR-CORE-012",
|
||||
"GR-SEC-001",
|
||||
"GR-SEC-002",
|
||||
"GR-SEC-003",
|
||||
"GR-UI-009",
|
||||
"GR-UI-010",
|
||||
"GR-UI-013",
|
||||
"GR-UI-015",
|
||||
"GR-TEST-001",
|
||||
"GR-TEST-002",
|
||||
"GR-LANG-002"
|
||||
],
|
||||
"required_quality_gate_ids": [
|
||||
"QG-001",
|
||||
"QG-002",
|
||||
"QG-003",
|
||||
"QG-005",
|
||||
"QG-006"
|
||||
]
|
||||
},
|
||||
"success_criteria": [
|
||||
{
|
||||
"id": "SC-001",
|
||||
"criterion": "Globale Settings fuer Microsoft Auto-Remember Default und Remember-Token-TTL sind im Settings-Layer implementiert, validiert und mit stabilen Fallbacks verfuegbar (Default: Auto-Remember aus, TTL 30 Tage)."
|
||||
},
|
||||
{
|
||||
"id": "SC-002",
|
||||
"criterion": "Tenant-Override fuer Microsoft Auto-Remember ist als 3-stufiges Modell (inherit/force_on/force_off) durchgaengig gespeichert, editierbar und im Runtime-Verhalten wirksam."
|
||||
},
|
||||
{
|
||||
"id": "SC-003",
|
||||
"criterion": "Bei erfolgreicher Microsoft-Anmeldung wird genau dann ein Remember-Token gesetzt, wenn die effektive Policy dies erlaubt; bei force_off oder global aus + inherit wird kein Token gesetzt."
|
||||
},
|
||||
{
|
||||
"id": "SC-004",
|
||||
"criterion": "RememberMeService nutzt die konfigurierbare TTL fuer Token-Erstellung und Token-Rotation; die feste 30-Tage-Konstante steuert das Verhalten nicht mehr."
|
||||
},
|
||||
{
|
||||
"id": "SC-005",
|
||||
"criterion": "Admin-Settings- und Tenant-SSO-Formulare enthalten die neuen Felder barrierearm und vollständig lokalisiert (de/en), ohne AuthZ-/CSRF-/PRG-Regression."
|
||||
},
|
||||
{
|
||||
"id": "SC-006",
|
||||
"criterion": "Neue und angepasste PHPUnit-Tests decken Settings-Validierung, Tenant-Prioritaet und Remember-Policy-Verhalten ab; alle Pflicht-Gates QG-001/002/003/005/006 sind mit Evidenz PASS."
|
||||
}
|
||||
],
|
||||
"implementation_steps": [
|
||||
{
|
||||
"id": "S1",
|
||||
"title": "Datenmodell und Settings-Keys festlegen",
|
||||
"description": "Fuehre zwei globale Settings-Keys ein: microsoft_auto_remember_default (bool, default 0) und remember_token_lifetime_days (int, default 30). Erweitere tenant_auth_microsoft um ein tri-state-faehiges Feld fuer Tenant-Override (NULL=inherit, 1=force_on, 0=force_off). Erstelle idempotentes SQL-Update in db/updates mit Guards fuer bestehende Installationen und aktualisiere db/init/init.sql fuer Neuinstallationen.",
|
||||
"guard_refs": [
|
||||
"GR-CORE-010",
|
||||
"GR-CORE-011",
|
||||
"GR-SEC-003"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S2",
|
||||
"title": "Settings-Gateway fuer Login-Persistenz implementieren",
|
||||
"description": "Implementiere einen dedizierten Settings-Gateway fuer Login-Persistenz (bool + TTL inkl. Grenzen, z. B. 1..365 Tage) auf Basis von SettingsMetadataGateway. Binde ihn in SettingServicesFactory ein und expose die Read-Methoden ueber AuthSettingsGateway fuer Auth-Services.",
|
||||
"guard_refs": [
|
||||
"GR-CORE-001",
|
||||
"GR-TEST-002",
|
||||
"GR-CORE-011"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S3",
|
||||
"title": "Tenant-Sso-Service auf Override-Policy erweitern",
|
||||
"description": "Erweitere TenantMicrosoftAuthRepository Select/Upsert um das neue Override-Feld und normalisiere im TenantSsoService die Eingabe auf inherit/force_on/force_off. Implementiere eine eindeutige Policy-Resolution-Methode shouldAutoRememberOnMicrosoftLogin(tenantId) mit Prioritaet: force_on > force_off > global default bei inherit.",
|
||||
"guard_refs": [
|
||||
"GR-CORE-001",
|
||||
"GR-SEC-003",
|
||||
"GR-TEST-001",
|
||||
"GR-TEST-002"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S4",
|
||||
"title": "Admin-Settings-Flow erweitern",
|
||||
"description": "Ergaenze AdminSettingsService buildPageData und updateFromAdmin um die neuen globalen Felder inkl. Validierung, Fehlerkeys und Persistenz. Erweitere pages/admin/settings/index(default).phtml um UI-Felder fuer globalen Microsoft-Auto-Remember-Default und Remember-TTL (nummerisch). Halte CSRF-Pruefung und PRG-Fluss unveraendert.",
|
||||
"guard_refs": [
|
||||
"GR-SEC-001",
|
||||
"GR-CORE-012",
|
||||
"GR-UI-009",
|
||||
"GR-UI-010",
|
||||
"GR-UI-015"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S5",
|
||||
"title": "Tenant-SSO-UI und AuthZ-Feldschutz erweitern",
|
||||
"description": "Ergaenze pages/admin/tenants/_form.phtml um ein 3-stufiges Auswahlfeld fuer Microsoft Auto-Remember Override (inherit/force_on/force_off) innerhalb des SSO-Bereichs. Erweitere create/edit-Flows fuer Formular-Repopulation und erweitere TenantAuthorizationPolicy::containsSsoFields um das neue Feld, damit unberechtigte SSO-Aenderungen weiterhin serverseitig blockiert werden.",
|
||||
"guard_refs": [
|
||||
"GR-CORE-005",
|
||||
"GR-SEC-001",
|
||||
"GR-UI-009",
|
||||
"GR-UI-013",
|
||||
"GR-UI-015"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S6",
|
||||
"title": "RememberMeService auf dynamische TTL umstellen",
|
||||
"description": "Entferne die harte Lifetime-Konstante als verhaltensbestimmende Quelle und nutze statt dessen den neuen Settings-Wert fuer rememberUser() und Token-Rotation in autoLoginFromCookie(). Stelle sicher, dass ungueltige Werte nicht zu unendlichen oder negativen Laufzeiten fuehren (Gateway-validierte Grenzen + Fallback).",
|
||||
"guard_refs": [
|
||||
"GR-CORE-001",
|
||||
"GR-TEST-001",
|
||||
"GR-TEST-002"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S7",
|
||||
"title": "Microsoft-Callback um Policy-gesteuertes Remember erweitern",
|
||||
"description": "Erweitere pages/auth/microsoft/callback().php nach erfolgreichem loginUserById um policy-gesteuerten Aufruf von rememberUser(userId), basierend auf der effektiven TenantSsoService-Policy. Fehlerpfade, Redirects und bestehendes Session-Timestamp-Verhalten bleiben unveraendert.",
|
||||
"guard_refs": [
|
||||
"GR-CORE-003",
|
||||
"GR-SEC-001",
|
||||
"GR-CORE-005"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S8",
|
||||
"title": "Tests erweitern und Gates absichern",
|
||||
"description": "Erweitere/erstelle PHPUnit-Tests fuer Settings-Gateway, AdminSettingsService, TenantSsoService, TenantAuthorizationPolicy und RememberMeService mit Fokus auf Defaults, Grenzen, Prioritaetsauflosung und side effects. Fuehre Pflicht-Gates aus und dokumentiere Evidenz pro SC-ID.",
|
||||
"guard_refs": [
|
||||
"GR-TEST-001",
|
||||
"GR-TEST-002",
|
||||
"GR-LANG-002",
|
||||
"GR-UI-009"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
"Neuer Test: tests/Service/Settings/SettingsLoginPersistenceGatewayTest.php fuer bool+TTL Fallback, Validierungsgrenzen und Persistenz.",
|
||||
"Erweiterung: tests/Service/Settings/AdminSettingsServiceSessionPolicyTest.php um neue Felder microsoft_auto_remember_default und remember_token_lifetime_days (gueltig/ungueltig).",
|
||||
"Erweiterung: tests/Service/Auth/TenantSsoServiceTest.php fuer Override-Normalisierung und effektive Policy-Prioritaet (force_on/force_off/inherit+global).",
|
||||
"Erweiterung: tests/Service/Access/TenantAuthorizationPolicyTest.php um neuen SSO-Feldschutz fuer microsoft_auto_remember_mode.",
|
||||
"Erweiterung: tests/Service/Auth/RememberMeServiceTest.php fuer dynamische TTL in rememberUser() und Token-Rotation.",
|
||||
"QG-001: docker compose exec php vendor/bin/phpunit",
|
||||
"QG-002: docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress",
|
||||
"QG-003: docker compose exec php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php",
|
||||
"QG-005: Manueller Browser-Smoke fuer Admin Settings + Tenant SSO + Microsoft Login Callback (keine neuen JS-Fehler, kein A11y-Regressionsverhalten).",
|
||||
"QG-006: docker compose exec php composer cs:check"
|
||||
],
|
||||
"acceptance_checks": [
|
||||
"SC-001: In Settings-Layer sind microsoft_auto_remember_default und remember_token_lifetime_days mit dokumentierten Defaults/Fallbacks verfuegbar und in buildPageData sichtbar.",
|
||||
"SC-002: Tenant-Form speichert und laedt den 3-stufigen Override stabil; effective policy entspricht exakt der Prioritaetsregel.",
|
||||
"SC-003: Microsoft-Callback setzt Remember-Token nur bei effektiver Policy=true und nie in Fehler-/Denied-Pfaden.",
|
||||
"SC-004: Remember-Tokens erhalten expires_at entsprechend konfigurierter TTL (nicht mehr fest 30 Tage) in Erzeugung und Rotation.",
|
||||
"SC-005: Neue sichtbare Texte laufen ueber t() und sind in i18n/default_de.json und i18n/default_en.json vorhanden; CSRF/PRG/AuthZ-Verhalten bleibt intakt.",
|
||||
"SC-006: Alle geplanten Testfaelle laufen stabil und QG-001/002/003/005/006 sind mit PASS dokumentiert."
|
||||
],
|
||||
"ux_notes": {
|
||||
"affected_patterns": [
|
||||
"Admin settings cards in pages/admin/settings/index(default).phtml",
|
||||
"Tenant SSO section in pages/admin/tenants/_form.phtml",
|
||||
"Existing tenant SSO toggle behavior in web/js/components/app-tenant-sso-toggle.js"
|
||||
],
|
||||
"ui_states_required": [
|
||||
"loading",
|
||||
"empty",
|
||||
"error",
|
||||
"success"
|
||||
],
|
||||
"a11y_touchpoints": [
|
||||
"Neue Checkbox/Number-Input in globalen Einstellungen (Labels, Hilfe, Fehleranzeige).",
|
||||
"Neues tri-state Feld im Tenant-SSO-Formular (semantische Feldbezeichnung und Keyboard-Bedienbarkeit).",
|
||||
"Fehlerdarstellung via bestehende notice/form-error Muster ohne Fokus-Regression."
|
||||
]
|
||||
},
|
||||
"risks": [
|
||||
{
|
||||
"risk": "Falsch konfigurierte TTL kann zu ungewollt langen Persistenzzeiten fuehren.",
|
||||
"mitigation": "Strikte Validierungsgrenzen im Settings-Gateway (z. B. 1..365), klarer Default 30 und Admin-Validierungsfehler bei ungueltigen Werten."
|
||||
},
|
||||
{
|
||||
"risk": "DB-Migration fuer bestehende Installationen kann fehlschlagen oder nicht idempotent sein.",
|
||||
"mitigation": "Idempotentes db/updates Script mit information_schema-Guard und Contract-Pruefung ueber DatabaseUpdatesContractTest + QG-003."
|
||||
},
|
||||
{
|
||||
"risk": "Tri-state Semantik kann in UI und Persistenz inkonsistent werden.",
|
||||
"mitigation": "Einheitliche Normalisierung im Service, explizite Mapping-Regeln (NULL/1/0) und gezielte Unit-Tests fuer jeden Zustand."
|
||||
},
|
||||
{
|
||||
"risk": "Callback-Erweiterung koennte unbeabsichtigt Erfolgs-/Fehlerfluesse beeinflussen.",
|
||||
"mitigation": "Token-Set nur nach erfolgreichem loginUserById und unveraenderte Redirect-/Flash-Fehlerpfade; manueller SSO-Smoke in QG-005."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"task_id": "AUTH-MICROSOFT-REMEMBER-001",
|
||||
"verdict": "pass",
|
||||
"checked_criterion_ids": [
|
||||
"SC-001",
|
||||
"SC-002",
|
||||
"SC-003",
|
||||
"SC-004",
|
||||
"SC-005",
|
||||
"SC-006"
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"criterion_id": "SC-001",
|
||||
"criterion": "Globale Settings fuer Microsoft Auto-Remember Default und Remember-Token-TTL sind im Settings-Layer implementiert, validiert und mit stabilen Fallbacks verfuegbar (Default: Auto-Remember aus, TTL 30 Tage).",
|
||||
"result": "pass",
|
||||
"evidence": "SettingsLoginPersistenceGateway implementiert isMicrosoftAutoRememberDefault() mit Fallback false sowie getRememberTokenLifetimeDays() mit Fallback 30 und Range 1..365; Seeds sind in db/init/init.sql und db/updates/2026-03-10-microsoft-auto-remember.sql vorhanden; AdminSettingsService::buildPageData() liefert beide Werte; SettingsLoginPersistenceGatewayTest deckt Fallback/Validierung ab."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-002",
|
||||
"criterion": "Tenant-Override fuer Microsoft Auto-Remember ist als 3-stufiges Modell (inherit/force_on/force_off) durchgaengig gespeichert, editierbar und im Runtime-Verhalten wirksam.",
|
||||
"result": "pass",
|
||||
"evidence": "tenant_auth_microsoft.auto_remember_mode (NULL/0/1) ist im Schema vorhanden; TenantMicrosoftAuthRepository liest/schreibt auto_remember_mode; TenantSsoService normalisiert Eingaben (inherit/0/1) und nutzt sie in shouldAutoRememberOnMicrosoftLogin(); Tenant-Form enthaelt das 3-stufige Select-Feld; Create/Edit repopulieren auto_remember_mode; TenantSsoServiceTest prueft Persistenz und Prioritaet."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-003",
|
||||
"criterion": "Bei erfolgreicher Microsoft-Anmeldung wird genau dann ein Remember-Token gesetzt, wenn die effektive Policy dies erlaubt; bei force_off oder global aus + inherit wird kein Token gesetzt.",
|
||||
"result": "pass",
|
||||
"evidence": "In pages/auth/microsoft/callback().php erfolgt rememberUser() nur nach erfolgreichem loginUserById() und nur wenn TenantSsoService::shouldAutoRememberOnMicrosoftLogin() true liefert; die Policy-Methode liefert force_on=true, force_off=false, inherit=globaler Default; TenantSsoServiceTest deckt diese Policy-Faelle ab."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-004",
|
||||
"criterion": "RememberMeService nutzt die konfigurierbare TTL fuer Token-Erstellung und Token-Rotation; die feste 30-Tage-Konstante steuert das Verhalten nicht mehr.",
|
||||
"result": "pass",
|
||||
"evidence": "RememberMeService verwendet lifetimeSeconds() sowohl bei create() als auch bei updateToken() (Rotation); lifetimeSeconds() liest getRememberTokenLifetimeDays() aus AuthSettingsGateway; AuthServicesFactory injiziert dieses Gateway in die Runtime-Instanz; RememberMeServiceTest verifiziert konfigurierte TTL und Fallback-Verhalten."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-005",
|
||||
"criterion": "Admin-Settings- und Tenant-SSO-Formulare enthalten die neuen Felder barrierearm und vollstaendig lokalisiert (de/en), ohne AuthZ-/CSRF-/PRG-Regression.",
|
||||
"result": "pass",
|
||||
"evidence": "Admin-Settings-Form enthaelt remember_token_lifetime_days und microsoft_auto_remember_default; Tenant-SSO-Form enthaelt microsoft_auto_remember_mode; neue sichtbare Labels/Hinweise laufen ueber t() und sind in i18n/default_de.json sowie i18n/default_en.json vorhanden; CSRF+PRG bleiben in pages/admin/settings/index().php sowie Tenant create/edit POST-Flow erhalten; TenantAuthorizationPolicy::containsSsoFields() enthaelt microsoft_auto_remember_mode und ist durch TenantAuthorizationPolicyTest abgesichert."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-006",
|
||||
"criterion": "Neue und angepasste PHPUnit-Tests decken Settings-Validierung, Tenant-Prioritaet und Remember-Policy-Verhalten ab; alle Pflicht-Gates QG-001/002/003/005/006 sind mit Evidenz PASS.",
|
||||
"result": "pass",
|
||||
"evidence": "Codeseitige Testabdeckung ist vorhanden (neue/erweiterte Tests fuer SettingsLoginPersistenceGateway, AdminSettingsServiceSessionPolicy, TenantSsoService, RememberMeService, TenantAuthorizationPolicy). Aktuelle Gate-Runs: QG-001 PASS (docker compose exec php vendor/bin/phpunit -> 555 Tests, 13476 Assertions, Exit 0), QG-002 PASS (phpstan: No errors), QG-003 PASS (CoreStarterkitContractTest: 11 Tests, 6253 Assertions), QG-006 PASS (composer cs:check: 0 fixable issues). QG-005 ist im execution-report als PASS dokumentiert (manueller Browser-Smoke fuer Admin Settings Security Tab, Tenant SSO und Microsoft Callback)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"task_id": "AUTH-MICROSOFT-REMEMBER-001",
|
||||
"verdict": "pass",
|
||||
"checked_guard_ids": [
|
||||
"GR-CORE-001",
|
||||
"GR-CORE-003",
|
||||
"GR-CORE-005",
|
||||
"GR-CORE-010",
|
||||
"GR-CORE-011",
|
||||
"GR-CORE-012",
|
||||
"GR-SEC-001",
|
||||
"GR-SEC-002",
|
||||
"GR-SEC-003",
|
||||
"GR-UI-009",
|
||||
"GR-UI-010",
|
||||
"GR-UI-013",
|
||||
"GR-UI-015",
|
||||
"GR-TEST-001",
|
||||
"GR-TEST-002",
|
||||
"GR-LANG-002"
|
||||
],
|
||||
"checked_quality_gate_ids": [
|
||||
"QG-001",
|
||||
"QG-002",
|
||||
"QG-003",
|
||||
"QG-005",
|
||||
"QG-006"
|
||||
],
|
||||
"findings": []
|
||||
}
|
||||
@@ -13,5 +13,5 @@ return [
|
||||
http://127.0.0.1:8080',
|
||||
'frontend_telemetry_enabled' => '1',
|
||||
'frontend_telemetry_sample_rate' => '0.2',
|
||||
'frontend_telemetry_allowed_events' => 'warn_once,ajax_error',
|
||||
'frontend_telemetry_allowed_events' => 'ajax_error,warn_once',
|
||||
];
|
||||
|
||||
@@ -204,6 +204,7 @@ CREATE TABLE IF NOT EXISTS `tenant_auth_microsoft` (
|
||||
`use_shared_app` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`client_id_override` VARCHAR(191) NULL,
|
||||
`client_secret_override_enc` TEXT NULL,
|
||||
`auto_remember_mode` TINYINT(1) NULL DEFAULT NULL,
|
||||
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`modified` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
@@ -895,6 +896,8 @@ VALUES
|
||||
('system_audit_retention_days', '365', 'setting.system_audit_retention_days'),
|
||||
('frontend_telemetry_enabled', '0', 'setting.frontend_telemetry_enabled'),
|
||||
('frontend_telemetry_sample_rate', '0.2', 'setting.frontend_telemetry_sample_rate'),
|
||||
('frontend_telemetry_allowed_events', 'warn_once,ajax_error', 'setting.frontend_telemetry_allowed_events')
|
||||
('frontend_telemetry_allowed_events', 'warn_once,ajax_error', 'setting.frontend_telemetry_allowed_events'),
|
||||
('microsoft_auto_remember_default', '0', 'setting.microsoft_auto_remember_default'),
|
||||
('remember_token_lifetime_days', '30', 'setting.remember_token_lifetime_days')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`key` = `key`;
|
||||
|
||||
24
db/updates/2026-03-10-microsoft-auto-remember.sql
Normal file
24
db/updates/2026-03-10-microsoft-auto-remember.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- Idempotent update: add Microsoft auto-remember and token TTL settings,
|
||||
-- plus tenant-level auto_remember_mode column.
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `description`)
|
||||
VALUES
|
||||
('microsoft_auto_remember_default', '0', 'setting.microsoft_auto_remember_default'),
|
||||
('remember_token_lifetime_days', '30', 'setting.remember_token_lifetime_days')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`key` = `key`;
|
||||
|
||||
-- Add auto_remember_mode to tenant_auth_microsoft (NULL = inherit global default).
|
||||
SET @col_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'tenant_auth_microsoft'
|
||||
AND COLUMN_NAME = 'auto_remember_mode'
|
||||
);
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `tenant_auth_microsoft` ADD COLUMN `auto_remember_mode` TINYINT(1) NULL DEFAULT NULL AFTER `client_secret_override_enc`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -1258,5 +1258,17 @@
|
||||
"Login progress": "Anmeldefortschritt",
|
||||
"Your session has expired. Please log in again.": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an. Mit „Angemeldet bleiben“ erfolgt die Anmeldung ggf. automatisch.",
|
||||
"Failed to update role permissions": "Rollenberechtigungen konnten nicht aktualisiert werden",
|
||||
"Failed to update permission roles": "Berechtigungsrollen konnten nicht aktualisiert werden"
|
||||
"Failed to update permission roles": "Berechtigungsrollen konnten nicht aktualisiert werden",
|
||||
"Login persistence": "Anmeldespeicherung",
|
||||
"Controls remember-me token lifetime and Microsoft auto-remember behavior.": "Steuert die Lebensdauer von Angemeldet-bleiben-Tokens und das automatische Anmeldeverhalten bei Microsoft-Login.",
|
||||
"Remember token lifetime (days)": "Token-Lebensdauer Angemeldet-bleiben (Tage)",
|
||||
"Allowed range: 1-365 days (default: 30)": "Erlaubter Bereich: 1–365 Tage (Standard: 30)",
|
||||
"Microsoft Auto-Remember default": "Microsoft Auto-Anmeldung Standard",
|
||||
"When enabled, successful Microsoft logins automatically persist a remember-me token (unless overridden per tenant).": "Wenn aktiviert, setzen erfolgreiche Microsoft-Anmeldungen automatisch ein Angemeldet-bleiben-Token (sofern nicht pro Mandant überschrieben).",
|
||||
"Microsoft Auto-Remember": "Microsoft Auto-Anmeldung",
|
||||
"Inherit global default": "Globalen Standard verwenden",
|
||||
"Force on": "Erzwingen (an)",
|
||||
"Force off": "Erzwingen (aus)",
|
||||
"Controls whether Microsoft logins automatically persist a remember-me token.": "Steuert, ob Microsoft-Anmeldungen automatisch ein Angemeldet-bleiben-Token setzen.",
|
||||
"Remember token lifetime is invalid": "Token-Lebensdauer ist ungültig"
|
||||
}
|
||||
|
||||
@@ -1258,5 +1258,17 @@
|
||||
"Login progress": "Login progress",
|
||||
"Your session has expired. Please log in again.": "Your session timed out. Please sign in again. If you chose Remember me, sign-in can happen automatically.",
|
||||
"Failed to update role permissions": "Failed to update role permissions",
|
||||
"Failed to update permission roles": "Failed to update permission roles"
|
||||
"Failed to update permission roles": "Failed to update permission roles",
|
||||
"Login persistence": "Login persistence",
|
||||
"Controls remember-me token lifetime and Microsoft auto-remember behavior.": "Controls remember-me token lifetime and Microsoft auto-remember behavior.",
|
||||
"Remember token lifetime (days)": "Remember token lifetime (days)",
|
||||
"Allowed range: 1-365 days (default: 30)": "Allowed range: 1-365 days (default: 30)",
|
||||
"Microsoft Auto-Remember default": "Microsoft Auto-Remember default",
|
||||
"When enabled, successful Microsoft logins automatically persist a remember-me token (unless overridden per tenant).": "When enabled, successful Microsoft logins automatically persist a remember-me token (unless overridden per tenant).",
|
||||
"Microsoft Auto-Remember": "Microsoft Auto-Remember",
|
||||
"Inherit global default": "Inherit global default",
|
||||
"Force on": "Force on",
|
||||
"Force off": "Force off",
|
||||
"Controls whether Microsoft logins automatically persist a remember-me token.": "Controls whether Microsoft logins automatically persist a remember-me token.",
|
||||
"Remember token lifetime is invalid": "Remember token lifetime is invalid"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use MintyPHP\Service\Settings\SettingsAppGateway;
|
||||
use MintyPHP\Service\Settings\SettingsDefaultsGateway;
|
||||
use MintyPHP\Service\Settings\SettingServicesFactory;
|
||||
use MintyPHP\Service\Settings\SettingsFrontendTelemetryGateway;
|
||||
use MintyPHP\Service\Settings\SettingsLoginPersistenceGateway;
|
||||
use MintyPHP\Service\Settings\SettingsMetadataGateway;
|
||||
use MintyPHP\Service\Settings\SettingsMicrosoftGateway;
|
||||
use MintyPHP\Service\Settings\SettingsSessionGateway;
|
||||
@@ -42,6 +43,7 @@ final class SettingsRegistrar implements ContainerRegistrar
|
||||
$container->set(SettingsFrontendTelemetryGateway::class, static fn (AppContainer $c): SettingsFrontendTelemetryGateway => $c->get(SettingServicesFactory::class)->createSettingsFrontendTelemetryGateway());
|
||||
$container->set(SettingsMicrosoftGateway::class, static fn (AppContainer $c): SettingsMicrosoftGateway => $c->get(SettingServicesFactory::class)->createSettingsMicrosoftGateway());
|
||||
$container->set(SettingsSmtpGateway::class, static fn (AppContainer $c): SettingsSmtpGateway => $c->get(SettingServicesFactory::class)->createSettingsSmtpGateway());
|
||||
$container->set(SettingsLoginPersistenceGateway::class, static fn (AppContainer $c): SettingsLoginPersistenceGateway => $c->get(SettingServicesFactory::class)->createSettingsLoginPersistenceGateway());
|
||||
$container->set(SettingCacheService::class, static fn (AppContainer $c): SettingCacheService => $c->get(SettingServicesFactory::class)->createSettingCacheService());
|
||||
$container->set(AdminSettingsService::class, static fn (AppContainer $c): AdminSettingsService => new AdminSettingsService(
|
||||
$c->get(SettingsDefaultsGateway::class),
|
||||
@@ -53,6 +55,7 @@ final class SettingsRegistrar implements ContainerRegistrar
|
||||
$c->get(SettingsFrontendTelemetryGateway::class),
|
||||
$c->get(SettingsMicrosoftGateway::class),
|
||||
$c->get(SettingsSmtpGateway::class),
|
||||
$c->get(SettingsLoginPersistenceGateway::class),
|
||||
$c->get(SettingsMetadataGateway::class),
|
||||
$c->get(SettingCacheService::class),
|
||||
$c->get(TenantService::class),
|
||||
|
||||
@@ -20,7 +20,7 @@ class TenantMicrosoftAuthRepository implements TenantMicrosoftAuthRepositoryInte
|
||||
public function findByTenantId(int $tenantId): ?array
|
||||
{
|
||||
$row = DB::selectOne(
|
||||
'select id, tenant_id, enabled, enforce_microsoft_login, sync_profile_on_login, sync_profile_fields, entra_tenant_id, allowed_domains, use_shared_app, client_id_override, client_secret_override_enc, created, modified from tenant_auth_microsoft where tenant_id = ? limit 1',
|
||||
'select id, tenant_id, enabled, enforce_microsoft_login, sync_profile_on_login, sync_profile_fields, entra_tenant_id, allowed_domains, use_shared_app, client_id_override, client_secret_override_enc, auto_remember_mode, created, modified from tenant_auth_microsoft where tenant_id = ? limit 1',
|
||||
(string) $tenantId
|
||||
);
|
||||
return $this->unwrap($row);
|
||||
@@ -36,7 +36,7 @@ class TenantMicrosoftAuthRepository implements TenantMicrosoftAuthRepositoryInte
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($tenantIds), '?'));
|
||||
$rows = DB::select(
|
||||
'select id, tenant_id, enabled, enforce_microsoft_login, sync_profile_on_login, sync_profile_fields, entra_tenant_id, allowed_domains, use_shared_app, client_id_override, client_secret_override_enc, created, modified from tenant_auth_microsoft where tenant_id in (' . $placeholders . ')',
|
||||
'select id, tenant_id, enabled, enforce_microsoft_login, sync_profile_on_login, sync_profile_fields, entra_tenant_id, allowed_domains, use_shared_app, client_id_override, client_secret_override_enc, auto_remember_mode, created, modified from tenant_auth_microsoft where tenant_id in (' . $placeholders . ')',
|
||||
...array_map('strval', $tenantIds)
|
||||
);
|
||||
|
||||
@@ -62,11 +62,14 @@ class TenantMicrosoftAuthRepository implements TenantMicrosoftAuthRepositoryInte
|
||||
|
||||
public function upsertByTenantId(int $tenantId, array $data): bool
|
||||
{
|
||||
$autoRememberMode = $data['auto_remember_mode'] ?? null;
|
||||
$autoRememberModeParam = $autoRememberMode !== null ? (string) (int) $autoRememberMode : null;
|
||||
|
||||
$result = DB::update(
|
||||
'insert into tenant_auth_microsoft (tenant_id, enabled, enforce_microsoft_login, sync_profile_on_login, sync_profile_fields, entra_tenant_id, allowed_domains, use_shared_app, client_id_override, client_secret_override_enc, created) values (?,?,?,?,?,?,?,?,?,?,NOW()) ' .
|
||||
'insert into tenant_auth_microsoft (tenant_id, enabled, enforce_microsoft_login, sync_profile_on_login, sync_profile_fields, entra_tenant_id, allowed_domains, use_shared_app, client_id_override, client_secret_override_enc, auto_remember_mode, created) values (?,?,?,?,?,?,?,?,?,?,?,NOW()) ' .
|
||||
'on duplicate key update enabled = values(enabled), enforce_microsoft_login = values(enforce_microsoft_login), sync_profile_on_login = values(sync_profile_on_login), sync_profile_fields = values(sync_profile_fields), entra_tenant_id = values(entra_tenant_id), ' .
|
||||
'allowed_domains = values(allowed_domains), use_shared_app = values(use_shared_app), client_id_override = values(client_id_override), ' .
|
||||
'client_secret_override_enc = values(client_secret_override_enc), modified = NOW()',
|
||||
'client_secret_override_enc = values(client_secret_override_enc), auto_remember_mode = values(auto_remember_mode), modified = NOW()',
|
||||
(string) $tenantId,
|
||||
!empty($data['enabled']) ? '1' : '0',
|
||||
!empty($data['enforce_microsoft_login']) ? '1' : '0',
|
||||
@@ -76,7 +79,8 @@ class TenantMicrosoftAuthRepository implements TenantMicrosoftAuthRepositoryInte
|
||||
$data['allowed_domains'] ?? null,
|
||||
!empty($data['use_shared_app']) ? '1' : '0',
|
||||
$data['client_id_override'] ?? null,
|
||||
$data['client_secret_override_enc'] ?? null
|
||||
$data['client_secret_override_enc'] ?? null,
|
||||
$autoRememberModeParam
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
|
||||
@@ -199,6 +199,7 @@ class TenantAuthorizationPolicy implements AuthorizationPolicyInterface
|
||||
'client_id_override',
|
||||
'client_secret_override',
|
||||
'clear_client_secret_override',
|
||||
'microsoft_auto_remember_mode',
|
||||
];
|
||||
|
||||
foreach ($ssoFields as $field) {
|
||||
|
||||
@@ -8,6 +8,7 @@ use MintyPHP\Service\Settings\SettingsApiPolicyGateway;
|
||||
use MintyPHP\Service\Settings\SettingsAppGateway;
|
||||
use MintyPHP\Service\Settings\SettingsDefaultsGateway;
|
||||
use MintyPHP\Service\Settings\SettingServicesFactory;
|
||||
use MintyPHP\Service\Settings\SettingsLoginPersistenceGateway;
|
||||
use MintyPHP\Service\Settings\SettingsMicrosoftGateway;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
@@ -18,6 +19,7 @@ class AuthGatewayFactory
|
||||
private ?SettingsApiPolicyGateway $settingsApiPolicyGateway = null;
|
||||
private ?SettingsDefaultsGateway $settingsDefaultsGateway = null;
|
||||
private ?SettingsAppGateway $settingsAppGateway = null;
|
||||
private ?SettingsLoginPersistenceGateway $settingsLoginPersistenceGateway = null;
|
||||
private ?PermissionGateway $permissionGateway = null;
|
||||
private ?AuthSettingsGateway $authSettingsGateway = null;
|
||||
private ?AuthScopeGateway $authScopeGateway = null;
|
||||
@@ -44,7 +46,8 @@ class AuthGatewayFactory
|
||||
$this->createSettingsMicrosoftGateway(),
|
||||
$this->createSettingsApiPolicyGateway(),
|
||||
$this->createSettingsDefaultsGateway(),
|
||||
$this->createSettingsAppGateway()
|
||||
$this->createSettingsAppGateway(),
|
||||
$this->createSettingsLoginPersistenceGateway()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,4 +124,8 @@ class AuthGatewayFactory
|
||||
return $this->settingsAppGateway ??= $this->settingServicesFactory->createSettingsAppGateway();
|
||||
}
|
||||
|
||||
private function createSettingsLoginPersistenceGateway(): SettingsLoginPersistenceGateway
|
||||
{
|
||||
return $this->settingsLoginPersistenceGateway ??= $this->settingServicesFactory->createSettingsLoginPersistenceGateway();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,8 @@ class AuthServicesFactory
|
||||
$this->createAuthSessionTenantContextService(),
|
||||
$this->sessionStore,
|
||||
$this->cookieStore,
|
||||
$this->requestRuntime
|
||||
$this->requestRuntime,
|
||||
$this->authGatewayFactory->createAuthSettingsGateway()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace MintyPHP\Service\Auth;
|
||||
use MintyPHP\Service\Settings\SettingsApiPolicyGateway;
|
||||
use MintyPHP\Service\Settings\SettingsAppGateway;
|
||||
use MintyPHP\Service\Settings\SettingsDefaultsGateway;
|
||||
use MintyPHP\Service\Settings\SettingsLoginPersistenceGateway;
|
||||
use MintyPHP\Service\Settings\SettingsMicrosoftGateway;
|
||||
|
||||
class AuthSettingsGateway
|
||||
@@ -13,7 +14,8 @@ class AuthSettingsGateway
|
||||
private readonly SettingsMicrosoftGateway $settingsMicrosoftGateway,
|
||||
private readonly SettingsApiPolicyGateway $settingsApiPolicyGateway,
|
||||
private readonly SettingsDefaultsGateway $settingsDefaultsGateway,
|
||||
private readonly SettingsAppGateway $settingsAppGateway
|
||||
private readonly SettingsAppGateway $settingsAppGateway,
|
||||
private readonly SettingsLoginPersistenceGateway $settingsLoginPersistenceGateway
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -56,4 +58,14 @@ class AuthSettingsGateway
|
||||
{
|
||||
return (int) $this->settingsDefaultsGateway->getDefaultDepartmentId();
|
||||
}
|
||||
|
||||
public function isMicrosoftAutoRememberDefault(): bool
|
||||
{
|
||||
return $this->settingsLoginPersistenceGateway->isMicrosoftAutoRememberDefault();
|
||||
}
|
||||
|
||||
public function getRememberTokenLifetimeDays(): int
|
||||
{
|
||||
return $this->settingsLoginPersistenceGateway->getRememberTokenLifetimeDays();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ class RememberMeService
|
||||
private readonly AuthSessionTenantContextService $authSessionTenantContextService,
|
||||
private readonly SessionStoreInterface $sessionStore,
|
||||
private readonly CookieStoreInterface $cookieStore,
|
||||
private readonly RequestRuntimeInterface $requestRuntime
|
||||
private readonly RequestRuntimeInterface $requestRuntime,
|
||||
private readonly ?AuthSettingsGateway $authSettingsGateway = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -175,7 +176,11 @@ class RememberMeService
|
||||
|
||||
private function lifetimeSeconds(): int
|
||||
{
|
||||
return self::LIFETIME_DAYS * 24 * 60 * 60;
|
||||
if ($this->authSettingsGateway !== null) {
|
||||
return $this->authSettingsGateway->getRememberTokenLifetimeDays() * 86400;
|
||||
}
|
||||
|
||||
return self::LIFETIME_DAYS * 86400;
|
||||
}
|
||||
|
||||
private function cookieName(): string
|
||||
|
||||
@@ -93,6 +93,9 @@ class TenantSsoService
|
||||
'use_shared_app' => !array_key_exists('use_shared_app', $row) || !empty($row['use_shared_app']),
|
||||
'client_id_override' => trim((string) ($row['client_id_override'] ?? '')),
|
||||
'client_secret_override_enc' => trim((string) ($row['client_secret_override_enc'] ?? '')),
|
||||
'auto_remember_mode' => array_key_exists('auto_remember_mode', $row) && $row['auto_remember_mode'] !== null
|
||||
? (int) $row['auto_remember_mode']
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -282,6 +285,8 @@ class TenantSsoService
|
||||
return ['ok' => false, 'errors' => $errors];
|
||||
}
|
||||
|
||||
$autoRememberMode = $this->normalizeAutoRememberMode($input['microsoft_auto_remember_mode'] ?? null);
|
||||
|
||||
$saved = $this->tenantMicrosoftAuthRepository->upsertByTenantId($tenantId, [
|
||||
'enabled' => $enabled,
|
||||
'enforce_microsoft_login' => $enabled ? $enforce : false,
|
||||
@@ -292,6 +297,7 @@ class TenantSsoService
|
||||
'use_shared_app' => $useSharedApp,
|
||||
'client_id_override' => $useSharedApp ? null : ($clientIdOverride !== '' ? $clientIdOverride : null),
|
||||
'client_secret_override_enc' => $useSharedApp ? null : ($clientSecretOverrideEnc !== '' ? $clientSecretOverrideEnc : null),
|
||||
'auto_remember_mode' => $autoRememberMode,
|
||||
]);
|
||||
|
||||
if (!$saved) {
|
||||
@@ -366,6 +372,29 @@ class TenantSsoService
|
||||
|| in_array('avatar', $fields, true);
|
||||
}
|
||||
|
||||
public function shouldAutoRememberOnMicrosoftLogin(int $tenantId): bool
|
||||
{
|
||||
$auth = $this->getTenantMicrosoftAuth($tenantId);
|
||||
$mode = $auth['auto_remember_mode'] ?? null;
|
||||
if ($mode === 1) {
|
||||
return true;
|
||||
}
|
||||
if ($mode === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->settingsGateway->isMicrosoftAutoRememberDefault();
|
||||
}
|
||||
|
||||
private function normalizeAutoRememberMode(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '' || $value === 'inherit') {
|
||||
return null;
|
||||
}
|
||||
$intVal = (int) $value;
|
||||
return in_array($intVal, [0, 1], true) ? $intVal : null;
|
||||
}
|
||||
|
||||
private function normalizeDomains(string $raw): string
|
||||
{
|
||||
$parts = preg_split('/[,\n;\r]+/', $raw) ?: [];
|
||||
|
||||
@@ -21,6 +21,7 @@ class AdminSettingsService
|
||||
private readonly SettingsFrontendTelemetryGateway $settingsFrontendTelemetryGateway,
|
||||
private readonly SettingsMicrosoftGateway $settingsMicrosoftGateway,
|
||||
private readonly SettingsSmtpGateway $settingsSmtpGateway,
|
||||
private readonly SettingsLoginPersistenceGateway $settingsLoginPersistenceGateway,
|
||||
private readonly SettingsMetadataGateway $settingsMetadataGateway,
|
||||
private readonly SettingCacheService $settingCacheService,
|
||||
private readonly TenantService $tenantService,
|
||||
@@ -64,6 +65,8 @@ class AdminSettingsService
|
||||
'user_inactivity_delete_days' => $this->settingsUserLifecycleGateway->getUserInactivityDeleteDays(),
|
||||
'session_idle_timeout_minutes' => $this->settingsSessionGateway->getSessionIdleTimeoutMinutes(),
|
||||
'session_absolute_timeout_hours' => $this->settingsSessionGateway->getSessionAbsoluteTimeoutHours(),
|
||||
'microsoft_auto_remember_default' => $this->settingsLoginPersistenceGateway->isMicrosoftAutoRememberDefault(),
|
||||
'remember_token_lifetime_days' => $this->settingsLoginPersistenceGateway->getRememberTokenLifetimeDays(),
|
||||
'api_cors_allowed_origins' => $this->settingsApiPolicyGateway->getApiCorsAllowedOriginsText(),
|
||||
'system_audit_enabled' => $this->settingsSystemAuditGateway->isSystemAuditEnabled(),
|
||||
'system_audit_retention_days' => $this->settingsSystemAuditGateway->getSystemAuditRetentionDays(),
|
||||
@@ -95,6 +98,8 @@ class AdminSettingsService
|
||||
'user_inactivity_delete_days' => $this->settingsMetadataGateway->get(SettingKeys::USER_INACTIVITY_DELETE_DAYS_KEY),
|
||||
'session_idle_timeout_minutes' => $this->settingsMetadataGateway->get(SettingKeys::SESSION_IDLE_TIMEOUT_MINUTES_KEY),
|
||||
'session_absolute_timeout_hours' => $this->settingsMetadataGateway->get(SettingKeys::SESSION_ABSOLUTE_TIMEOUT_HOURS_KEY),
|
||||
'microsoft_auto_remember_default' => $this->settingsMetadataGateway->get(SettingKeys::MICROSOFT_AUTO_REMEMBER_DEFAULT_KEY),
|
||||
'remember_token_lifetime_days' => $this->settingsMetadataGateway->get(SettingKeys::REMEMBER_TOKEN_LIFETIME_DAYS_KEY),
|
||||
'api_cors_allowed_origins' => $this->settingsMetadataGateway->get(SettingKeys::API_CORS_ALLOWED_ORIGINS_KEY),
|
||||
'system_audit_enabled' => $this->settingsMetadataGateway->get(SettingKeys::SYSTEM_AUDIT_ENABLED_KEY),
|
||||
'system_audit_retention_days' => $this->settingsMetadataGateway->get(SettingKeys::SYSTEM_AUDIT_RETENTION_DAYS_KEY),
|
||||
@@ -133,6 +138,8 @@ class AdminSettingsService
|
||||
$userInactivityDeleteDays = (int) ($post['user_inactivity_delete_days'] ?? 0);
|
||||
$sessionIdleTimeoutMinutes = (int) ($post['session_idle_timeout_minutes'] ?? 0);
|
||||
$sessionAbsoluteTimeoutHours = (int) ($post['session_absolute_timeout_hours'] ?? 0);
|
||||
$microsoftAutoRememberDefault = isset($post['microsoft_auto_remember_default']);
|
||||
$rememberTokenLifetimeDays = (int) ($post['remember_token_lifetime_days'] ?? 0);
|
||||
$apiCorsAllowedOrigins = $this->normalizeScalarText($post['api_cors_allowed_origins'] ?? '');
|
||||
$systemAuditEnabled = isset($post['system_audit_enabled']);
|
||||
$systemAuditRetentionDays = (int) ($post['system_audit_retention_days'] ?? 0);
|
||||
@@ -167,6 +174,8 @@ class AdminSettingsService
|
||||
'user_inactivity_delete_days' => $this->settingsUserLifecycleGateway->getUserInactivityDeleteDays(),
|
||||
'session_idle_timeout_minutes' => $this->settingsSessionGateway->getSessionIdleTimeoutMinutes(),
|
||||
'session_absolute_timeout_hours' => $this->settingsSessionGateway->getSessionAbsoluteTimeoutHours(),
|
||||
'microsoft_auto_remember_default' => $this->settingsLoginPersistenceGateway->isMicrosoftAutoRememberDefault(),
|
||||
'remember_token_lifetime_days' => $this->settingsLoginPersistenceGateway->getRememberTokenLifetimeDays(),
|
||||
'system_audit_enabled' => $this->settingsSystemAuditGateway->isSystemAuditEnabled(),
|
||||
'system_audit_retention_days' => $this->settingsSystemAuditGateway->getSystemAuditRetentionDays(),
|
||||
'frontend_telemetry_enabled' => $this->settingsFrontendTelemetryGateway->isFrontendTelemetryEnabled(),
|
||||
@@ -232,6 +241,15 @@ class AdminSettingsService
|
||||
$errors[] = ['message' => 'Session absolute timeout is invalid', 'key' => 'session_absolute_timeout_invalid'];
|
||||
}
|
||||
|
||||
$this->settingsLoginPersistenceGateway->setMicrosoftAutoRememberDefault($microsoftAutoRememberDefault);
|
||||
|
||||
$rememberTtlOk = $this->settingsLoginPersistenceGateway->setRememberTokenLifetimeDays(
|
||||
$rememberTokenLifetimeDays > 0 ? $rememberTokenLifetimeDays : null
|
||||
);
|
||||
if (!$rememberTtlOk) {
|
||||
$errors[] = ['message' => 'Remember token lifetime is invalid', 'key' => 'remember_token_lifetime_days_invalid'];
|
||||
}
|
||||
|
||||
$apiCorsOriginsOk = $this->settingsApiPolicyGateway->setApiCorsAllowedOrigins($apiCorsAllowedOrigins);
|
||||
if (!$apiCorsOriginsOk) {
|
||||
$errors[] = ['message' => 'CORS allowed origins are invalid', 'key' => 'api_cors_allowed_origins_invalid'];
|
||||
@@ -336,6 +354,8 @@ class AdminSettingsService
|
||||
'user_inactivity_delete_days' => $this->settingsUserLifecycleGateway->getUserInactivityDeleteDays(),
|
||||
'session_idle_timeout_minutes' => $this->settingsSessionGateway->getSessionIdleTimeoutMinutes(),
|
||||
'session_absolute_timeout_hours' => $this->settingsSessionGateway->getSessionAbsoluteTimeoutHours(),
|
||||
'microsoft_auto_remember_default' => $this->settingsLoginPersistenceGateway->isMicrosoftAutoRememberDefault(),
|
||||
'remember_token_lifetime_days' => $this->settingsLoginPersistenceGateway->getRememberTokenLifetimeDays(),
|
||||
'system_audit_enabled' => $this->settingsSystemAuditGateway->isSystemAuditEnabled(),
|
||||
'system_audit_retention_days' => $this->settingsSystemAuditGateway->getSystemAuditRetentionDays(),
|
||||
'frontend_telemetry_enabled' => $this->settingsFrontendTelemetryGateway->isFrontendTelemetryEnabled(),
|
||||
|
||||
@@ -35,6 +35,8 @@ final class SettingKeys
|
||||
public const FRONTEND_TELEMETRY_ALLOWED_EVENTS_KEY = 'frontend_telemetry_allowed_events';
|
||||
public const SESSION_IDLE_TIMEOUT_MINUTES_KEY = 'session_idle_timeout_minutes';
|
||||
public const SESSION_ABSOLUTE_TIMEOUT_HOURS_KEY = 'session_absolute_timeout_hours';
|
||||
public const MICROSOFT_AUTO_REMEMBER_DEFAULT_KEY = 'microsoft_auto_remember_default';
|
||||
public const REMEMBER_TOKEN_LIFETIME_DAYS_KEY = 'remember_token_lifetime_days';
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ class SettingServicesFactory
|
||||
private ?SettingsMicrosoftGateway $settingsMicrosoftGateway = null;
|
||||
private ?SettingsSessionGateway $settingsSessionGateway = null;
|
||||
private ?SettingsSmtpGateway $settingsSmtpGateway = null;
|
||||
private ?SettingsLoginPersistenceGateway $settingsLoginPersistenceGateway = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly SettingRepositoryFactory $settingRepositoryFactory
|
||||
@@ -94,6 +95,11 @@ class SettingServicesFactory
|
||||
return $this->settingsSmtpGateway ??= new SettingsSmtpGateway($this->createSettingsMetadataGateway());
|
||||
}
|
||||
|
||||
public function createSettingsLoginPersistenceGateway(): SettingsLoginPersistenceGateway
|
||||
{
|
||||
return $this->settingsLoginPersistenceGateway ??= new SettingsLoginPersistenceGateway($this->createSettingsMetadataGateway());
|
||||
}
|
||||
|
||||
public function createSettingCacheService(): SettingCacheService
|
||||
{
|
||||
return $this->settingCacheService ??= new SettingCacheService();
|
||||
|
||||
62
lib/Service/Settings/SettingsLoginPersistenceGateway.php
Normal file
62
lib/Service/Settings/SettingsLoginPersistenceGateway.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Settings;
|
||||
|
||||
class SettingsLoginPersistenceGateway
|
||||
{
|
||||
private const AUTO_REMEMBER_FALLBACK = false;
|
||||
private const LIFETIME_DAYS_FALLBACK = 30;
|
||||
private const LIFETIME_DAYS_MIN = 1;
|
||||
private const LIFETIME_DAYS_MAX = 365;
|
||||
|
||||
public function __construct(private readonly SettingsMetadataGateway $settingsMetadataGateway)
|
||||
{
|
||||
}
|
||||
|
||||
public function isMicrosoftAutoRememberDefault(): bool
|
||||
{
|
||||
$value = strtolower(trim((string) ($this->settingsMetadataGateway->getValue(SettingKeys::MICROSOFT_AUTO_REMEMBER_DEFAULT_KEY) ?? '')));
|
||||
if ($value === '') {
|
||||
return self::AUTO_REMEMBER_FALLBACK;
|
||||
}
|
||||
|
||||
return in_array($value, ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
public function setMicrosoftAutoRememberDefault(bool $enabled, ?string $description = null): bool
|
||||
{
|
||||
$desc = $description ?? 'setting.microsoft_auto_remember_default';
|
||||
return $this->settingsMetadataGateway->set(SettingKeys::MICROSOFT_AUTO_REMEMBER_DEFAULT_KEY, $enabled ? '1' : '0', $desc);
|
||||
}
|
||||
|
||||
public function getRememberTokenLifetimeDays(): int
|
||||
{
|
||||
$value = $this->settingsMetadataGateway->getInt(SettingKeys::REMEMBER_TOKEN_LIFETIME_DAYS_KEY);
|
||||
if ($value !== null && $this->isValidLifetime($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return self::LIFETIME_DAYS_FALLBACK;
|
||||
}
|
||||
|
||||
public function setRememberTokenLifetimeDays(?int $days, ?string $description = null): bool
|
||||
{
|
||||
$value = $days ?? self::LIFETIME_DAYS_FALLBACK;
|
||||
if (!$this->isValidLifetime($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$desc = $description ?? 'setting.remember_token_lifetime_days';
|
||||
return $this->settingsMetadataGateway->set(SettingKeys::REMEMBER_TOKEN_LIFETIME_DAYS_KEY, (string) $value, $desc);
|
||||
}
|
||||
|
||||
public function getRememberTokenLifetimeSeconds(): int
|
||||
{
|
||||
return $this->getRememberTokenLifetimeDays() * 86400;
|
||||
}
|
||||
|
||||
private function isValidLifetime(int $days): bool
|
||||
{
|
||||
return $days >= self::LIFETIME_DAYS_MIN && $days <= self::LIFETIME_DAYS_MAX;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ $userInactivityDeactivateDays = (int) ($values['user_inactivity_deactivate_days'
|
||||
$userInactivityDeleteDays = (int) ($values['user_inactivity_delete_days'] ?? 365);
|
||||
$sessionIdleTimeoutMinutes = (int) ($values['session_idle_timeout_minutes'] ?? 30);
|
||||
$sessionAbsoluteTimeoutHours = (int) ($values['session_absolute_timeout_hours'] ?? 8);
|
||||
$microsoftAutoRememberDefault = !empty($values['microsoft_auto_remember_default']);
|
||||
$rememberTokenLifetimeDays = (int) ($values['remember_token_lifetime_days'] ?? 30);
|
||||
$apiCorsAllowedOrigins = (string) ($values['api_cors_allowed_origins'] ?? '');
|
||||
$systemAuditEnabled = !empty($values['system_audit_enabled']);
|
||||
$systemAuditRetentionDays = (int) ($values['system_audit_retention_days'] ?? 365);
|
||||
@@ -74,6 +76,8 @@ $userInactivityDeactivateDaysDesc = $settings['user_inactivity_deactivate_days']
|
||||
$userInactivityDeleteDaysDesc = $settings['user_inactivity_delete_days']['description'] ?? 'setting.user_inactivity_delete_days';
|
||||
$sessionIdleTimeoutMinutesDesc = $settings['session_idle_timeout_minutes']['description'] ?? 'setting.session_idle_timeout_minutes';
|
||||
$sessionAbsoluteTimeoutHoursDesc = $settings['session_absolute_timeout_hours']['description'] ?? 'setting.session_absolute_timeout_hours';
|
||||
$microsoftAutoRememberDefaultDesc = $settings['microsoft_auto_remember_default']['description'] ?? 'setting.microsoft_auto_remember_default';
|
||||
$rememberTokenLifetimeDaysDesc = $settings['remember_token_lifetime_days']['description'] ?? 'setting.remember_token_lifetime_days';
|
||||
$apiCorsAllowedOriginsDesc = $settings['api_cors_allowed_origins']['description'] ?? 'setting.api_cors_allowed_origins';
|
||||
$systemAuditEnabledDesc = $settings['system_audit_enabled']['description'] ?? 'setting.system_audit_enabled';
|
||||
$systemAuditRetentionDaysDesc = $settings['system_audit_retention_days']['description'] ?? 'setting.system_audit_retention_days';
|
||||
@@ -379,6 +383,42 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="app-details-card" name="settings-security-login-persistence" data-details-key="settings-security-login-persistence">
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Login persistence')); ?></span>
|
||||
<span class="app-details-card-summary-meta">
|
||||
<span class="badge" data-variant="neutral"><?php e((string) $rememberTokenLifetimeDays); ?></span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<blockquote data-variant="info">
|
||||
<small><?php e(t('Controls remember-me token lifetime and Microsoft auto-remember behavior.')); ?></small>
|
||||
</blockquote>
|
||||
<div class="grid">
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Remember token lifetime (days)')); ?></span>
|
||||
<input type="number" name="remember_token_lifetime_days"
|
||||
value="<?php e((string) $rememberTokenLifetimeDays); ?>" min="1" max="365" step="1"
|
||||
<?php e($readonlyAttr); ?>>
|
||||
<small><?php e(t($rememberTokenLifetimeDaysDesc)); ?></small>
|
||||
<small class="muted"><?php e(t('Allowed range: 1-365 days (default: 30)')); ?></small>
|
||||
</label>
|
||||
<fieldset>
|
||||
<legend>
|
||||
<small><?php e(t($microsoftAutoRememberDefaultDesc)); ?></small>
|
||||
</legend>
|
||||
<label class="app-field">
|
||||
<input type="checkbox" name="microsoft_auto_remember_default" value="1"
|
||||
<?php e($microsoftAutoRememberDefault ? 'checked' : ''); ?>
|
||||
<?php e($disabledAttr); ?>>
|
||||
<span><?php e(t('Microsoft Auto-Remember default')); ?></span>
|
||||
</label>
|
||||
<small class="muted"><?php e(t('When enabled, successful Microsoft logins automatically persist a remember-me token (unless overridden per tenant).')); ?></small>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details class="app-details-card" name="settings-security-user-lifecycle" data-details-key="settings-security-user-lifecycle">
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('User lifecycle policy')); ?></span>
|
||||
|
||||
@@ -482,6 +482,19 @@ $ssoSyncNeedsGraph = !empty($ssoUiState['sync_needs_graph']);
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset data-tenant-sso-when-enabled>
|
||||
<legend><small><?php e(t('Login persistence')); ?></small></legend>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Microsoft Auto-Remember')); ?></span>
|
||||
<select name="microsoft_auto_remember_mode" <?php e($readonlyAttr); ?>>
|
||||
<option value="inherit" <?php if (($form['auto_remember_mode'] ?? null) === null) echo 'selected'; ?>><?php e(t('Inherit global default')); ?></option>
|
||||
<option value="1" <?php if (($form['auto_remember_mode'] ?? null) === 1 || ($form['auto_remember_mode'] ?? null) === '1') echo 'selected'; ?>><?php e(t('Force on')); ?></option>
|
||||
<option value="0" <?php if (($form['auto_remember_mode'] ?? null) === 0 || ($form['auto_remember_mode'] ?? null) === '0') echo 'selected'; ?>><?php e(t('Force off')); ?></option>
|
||||
</select>
|
||||
<small class="muted"><?php e(t('Controls whether Microsoft logins automatically persist a remember-me token.')); ?></small>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset data-tenant-sso-when-enabled>
|
||||
<legend><small><?php e(t('Profile sync (optional)')); ?></small></legend>
|
||||
<label class="app-field">
|
||||
|
||||
@@ -92,6 +92,7 @@ if ($request->hasBody('description')) {
|
||||
'allowed_domains' => trim((string) ($post['allowed_domains'] ?? '')),
|
||||
'use_shared_app' => !empty($post['use_shared_app']) ? '1' : '0',
|
||||
'client_id_override' => trim((string) ($post['client_id_override'] ?? '')),
|
||||
'auto_remember_mode' => $post['microsoft_auto_remember_mode'] ?? null,
|
||||
]);
|
||||
$ssoUiState = $tenantSsoService->buildMicrosoftUiState(0, $post);
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@ if ($request->hasBody('description')) {
|
||||
'allowed_domains' => trim((string) ($post['allowed_domains'] ?? '')),
|
||||
'use_shared_app' => !empty($post['use_shared_app']) ? '1' : '0',
|
||||
'client_id_override' => trim((string) ($post['client_id_override'] ?? '')),
|
||||
'auto_remember_mode' => $post['microsoft_auto_remember_mode'] ?? null,
|
||||
]);
|
||||
$ssoUiState = $tenantSsoService->buildMicrosoftUiState($tenantId, $post);
|
||||
}
|
||||
|
||||
@@ -61,4 +61,9 @@ if (!($loginResult['ok'] ?? false)) {
|
||||
$sessionStore = app(SessionStoreInterface::class);
|
||||
$sessionStore->set('session_started_at', time());
|
||||
$sessionStore->set('session_last_activity', time());
|
||||
|
||||
if ($tenantSsoService->shouldAutoRememberOnMicrosoftLogin($tenantId)) {
|
||||
$authServicesFactory->createRememberMeService()->rememberUser($userId);
|
||||
}
|
||||
|
||||
Router::redirect('admin');
|
||||
|
||||
@@ -186,6 +186,28 @@ class TenantAuthorizationPolicyTest extends TestCase
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testEditSubmitDeniesAutoRememberModeWithoutSsoPermission(): void
|
||||
{
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
11 => [PermissionService::TENANTS_VIEW, PermissionService::TENANTS_UPDATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
->method('canAccess')
|
||||
->with('tenants', 22, 11)
|
||||
->willReturn(true);
|
||||
|
||||
$policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway);
|
||||
$decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_SUBMIT, [
|
||||
'actor_user_id' => 11,
|
||||
'target_tenant_id' => 22,
|
||||
'input' => ['microsoft_auto_remember_mode' => '1'],
|
||||
]);
|
||||
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testCustomFieldsManageRequiresPermissionAndScope(): void
|
||||
{
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
|
||||
@@ -10,6 +10,7 @@ use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
||||
use MintyPHP\Service\Auth\AuthPermissionGateway;
|
||||
use MintyPHP\Service\Auth\AuthSessionTenantContextService;
|
||||
use MintyPHP\Service\Auth\AuthSettingsGateway;
|
||||
use MintyPHP\Service\Auth\RememberMeService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -427,6 +428,81 @@ class RememberMeServiceTest extends TestCase
|
||||
$this->assertSame(15, $service->expireAllTokensByAdmin());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Dynamic TTL via AuthSettingsGateway
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testRememberUserUsesConfiguredTtlFromGateway(): void
|
||||
{
|
||||
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
||||
$settingsGateway->method('getRememberTokenLifetimeDays')->willReturn(7);
|
||||
|
||||
$capturedExpiresAt = '';
|
||||
$tokenRepo = $this->createMock(RememberTokenRepositoryInterface::class);
|
||||
$tokenRepo->expects($this->once())
|
||||
->method('create')
|
||||
->with(
|
||||
42,
|
||||
$this->anything(),
|
||||
$this->anything(),
|
||||
$this->callback(function (string $e) use (&$capturedExpiresAt): bool {
|
||||
$capturedExpiresAt = $e;
|
||||
return strtotime($e . ' UTC') > time();
|
||||
})
|
||||
)
|
||||
->willReturn(1);
|
||||
|
||||
$cookieStore = $this->createMock(CookieStoreInterface::class);
|
||||
$cookieStore->expects($this->once())
|
||||
->method('set')
|
||||
->with(
|
||||
'remember',
|
||||
$this->anything(),
|
||||
$this->callback(fn (array $opts): bool => $opts['expires'] > time()
|
||||
&& $opts['expires'] <= time() + (7 * 86400) + 10)
|
||||
);
|
||||
|
||||
$service = $this->newService(
|
||||
tokenRepository: $tokenRepo,
|
||||
cookieStore: $cookieStore,
|
||||
authSettingsGateway: $settingsGateway
|
||||
);
|
||||
$service->rememberUser(42);
|
||||
|
||||
// Verify expiration is ~7 days from now (not 30)
|
||||
$expiresTimestamp = strtotime($capturedExpiresAt . ' UTC');
|
||||
$this->assertGreaterThan(time() + (6 * 86400), $expiresTimestamp);
|
||||
$this->assertLessThanOrEqual(time() + (7 * 86400) + 10, $expiresTimestamp);
|
||||
}
|
||||
|
||||
public function testRememberUserFallsBackTo30DaysWhenNoGateway(): void
|
||||
{
|
||||
$capturedExpiresAt = '';
|
||||
$tokenRepo = $this->createMock(RememberTokenRepositoryInterface::class);
|
||||
$tokenRepo->expects($this->once())
|
||||
->method('create')
|
||||
->with(
|
||||
42,
|
||||
$this->anything(),
|
||||
$this->anything(),
|
||||
$this->callback(function (string $e) use (&$capturedExpiresAt): bool {
|
||||
$capturedExpiresAt = $e;
|
||||
return true;
|
||||
})
|
||||
)
|
||||
->willReturn(1);
|
||||
|
||||
$cookieStore = $this->createMock(CookieStoreInterface::class);
|
||||
$cookieStore->method('set');
|
||||
|
||||
$service = $this->newService(tokenRepository: $tokenRepo, cookieStore: $cookieStore);
|
||||
$service->rememberUser(42);
|
||||
|
||||
$expiresTimestamp = strtotime($capturedExpiresAt . ' UTC');
|
||||
$this->assertGreaterThan(time() + (29 * 86400), $expiresTimestamp);
|
||||
$this->assertLessThanOrEqual(time() + (30 * 86400) + 10, $expiresTimestamp);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Helper
|
||||
// ---------------------------------------------------------------
|
||||
@@ -439,7 +515,8 @@ class RememberMeServiceTest extends TestCase
|
||||
?AuthSessionTenantContextService $authSessionTenantContextService = null,
|
||||
?SessionStoreInterface $sessionStore = null,
|
||||
?CookieStoreInterface $cookieStore = null,
|
||||
?RequestRuntimeInterface $requestRuntime = null
|
||||
?RequestRuntimeInterface $requestRuntime = null,
|
||||
?AuthSettingsGateway $authSettingsGateway = null
|
||||
): RememberMeService {
|
||||
// RequestRuntimeInterface has a method named "method()" which PHPUnit 12
|
||||
// cannot mock (reserved name). Use a simple anonymous-class stub instead.
|
||||
@@ -483,7 +560,8 @@ class RememberMeServiceTest extends TestCase
|
||||
$authSessionTenantContextService ?? $this->createMock(AuthSessionTenantContextService::class),
|
||||
$sessionStore ?? $this->createMock(SessionStoreInterface::class),
|
||||
$cookieStore ?? $this->createMock(CookieStoreInterface::class),
|
||||
$requestRuntime
|
||||
$requestRuntime,
|
||||
$authSettingsGateway
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +278,144 @@ class TenantSsoServiceTest extends TestCase
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// shouldAutoRememberOnMicrosoftLogin — policy resolution
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testShouldAutoRememberReturnsTrueWhenForceOn(): void
|
||||
{
|
||||
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
||||
$repository->method('findByTenantId')->willReturn([
|
||||
'auto_remember_mode' => 1,
|
||||
]);
|
||||
|
||||
$service = $this->newService($repository);
|
||||
$this->assertTrue($service->shouldAutoRememberOnMicrosoftLogin(5));
|
||||
}
|
||||
|
||||
public function testShouldAutoRememberReturnsFalseWhenForceOff(): void
|
||||
{
|
||||
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
||||
$repository->method('findByTenantId')->willReturn([
|
||||
'auto_remember_mode' => 0,
|
||||
]);
|
||||
|
||||
$service = $this->newService($repository);
|
||||
$this->assertFalse($service->shouldAutoRememberOnMicrosoftLogin(5));
|
||||
}
|
||||
|
||||
public function testShouldAutoRememberInheritUsesGlobalDefaultTrue(): void
|
||||
{
|
||||
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
||||
$repository->method('findByTenantId')->willReturn([
|
||||
'auto_remember_mode' => null,
|
||||
]);
|
||||
|
||||
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
||||
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('');
|
||||
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('');
|
||||
$settingsGateway->method('getMicrosoftAuthority')->willReturn('');
|
||||
$settingsGateway->method('isMicrosoftAutoRememberDefault')->willReturn(true);
|
||||
|
||||
$service = $this->newService($repository, null, $settingsGateway);
|
||||
$this->assertTrue($service->shouldAutoRememberOnMicrosoftLogin(5));
|
||||
}
|
||||
|
||||
public function testShouldAutoRememberInheritUsesGlobalDefaultFalse(): void
|
||||
{
|
||||
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
||||
$repository->method('findByTenantId')->willReturn([
|
||||
'auto_remember_mode' => null,
|
||||
]);
|
||||
|
||||
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
||||
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('');
|
||||
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('');
|
||||
$settingsGateway->method('getMicrosoftAuthority')->willReturn('');
|
||||
$settingsGateway->method('isMicrosoftAutoRememberDefault')->willReturn(false);
|
||||
|
||||
$service = $this->newService($repository, null, $settingsGateway);
|
||||
$this->assertFalse($service->shouldAutoRememberOnMicrosoftLogin(5));
|
||||
}
|
||||
|
||||
public function testSaveTenantMicrosoftAuthPersistsAutoRememberMode(): void
|
||||
{
|
||||
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
||||
$tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);
|
||||
|
||||
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
||||
$repository->method('findByTenantId')->willReturn([]);
|
||||
$repository->expects($this->once())
|
||||
->method('upsertByTenantId')
|
||||
->with(
|
||||
5,
|
||||
$this->callback(static function (array $data): bool {
|
||||
return $data['auto_remember_mode'] === 1;
|
||||
})
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
||||
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('client-id');
|
||||
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('client-secret');
|
||||
$settingsGateway->method('getMicrosoftAuthority')->willReturn('');
|
||||
|
||||
$service = $this->newService($repository, $tenantGateway, $settingsGateway);
|
||||
$result = $service->saveTenantMicrosoftAuth(5, [
|
||||
'microsoft_enabled' => '1',
|
||||
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
||||
'use_shared_app' => '1',
|
||||
'sync_profile_on_login' => '',
|
||||
'sync_profile_fields' => [],
|
||||
'allowed_domains' => '',
|
||||
'enforce_microsoft_login' => '',
|
||||
'client_id_override' => '',
|
||||
'client_secret_override' => '',
|
||||
'microsoft_auto_remember_mode' => '1',
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testSaveTenantMicrosoftAuthPersistsInheritAsNull(): void
|
||||
{
|
||||
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
||||
$tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);
|
||||
|
||||
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
||||
$repository->method('findByTenantId')->willReturn([]);
|
||||
$repository->expects($this->once())
|
||||
->method('upsertByTenantId')
|
||||
->with(
|
||||
5,
|
||||
$this->callback(static function (array $data): bool {
|
||||
return $data['auto_remember_mode'] === null;
|
||||
})
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
||||
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('client-id');
|
||||
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('client-secret');
|
||||
$settingsGateway->method('getMicrosoftAuthority')->willReturn('');
|
||||
|
||||
$service = $this->newService($repository, $tenantGateway, $settingsGateway);
|
||||
$result = $service->saveTenantMicrosoftAuth(5, [
|
||||
'microsoft_enabled' => '1',
|
||||
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
||||
'use_shared_app' => '1',
|
||||
'sync_profile_on_login' => '',
|
||||
'sync_profile_fields' => [],
|
||||
'allowed_domains' => '',
|
||||
'enforce_microsoft_login' => '',
|
||||
'client_id_override' => '',
|
||||
'client_secret_override' => '',
|
||||
'microsoft_auto_remember_mode' => 'inherit',
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
private function newService(
|
||||
?TenantMicrosoftAuthRepositoryInterface $repository = null,
|
||||
?AuthTenantGateway $tenantGateway = null,
|
||||
|
||||
@@ -13,6 +13,7 @@ use MintyPHP\Service\Settings\SettingsApiPolicyGateway;
|
||||
use MintyPHP\Service\Settings\SettingsAppGateway;
|
||||
use MintyPHP\Service\Settings\SettingsDefaultsGateway;
|
||||
use MintyPHP\Service\Settings\SettingsFrontendTelemetryGateway;
|
||||
use MintyPHP\Service\Settings\SettingsLoginPersistenceGateway;
|
||||
use MintyPHP\Service\Settings\SettingsMetadataGateway;
|
||||
use MintyPHP\Service\Settings\SettingsMicrosoftGateway;
|
||||
use MintyPHP\Service\Settings\SettingsSessionGateway;
|
||||
@@ -112,6 +113,74 @@ class AdminSettingsServiceSessionPolicyTest extends TestCase
|
||||
$this->assertContains('session_absolute_timeout_invalid', $errorKeys);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Login persistence settings
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testBuildPageDataIncludesLoginPersistenceValues(): void
|
||||
{
|
||||
$loginPersistenceGateway = $this->createConfiguredMock(SettingsLoginPersistenceGateway::class, [
|
||||
'isMicrosoftAutoRememberDefault' => true,
|
||||
'getRememberTokenLifetimeDays' => 14,
|
||||
]);
|
||||
|
||||
$service = $this->newService(loginPersistenceGateway: $loginPersistenceGateway);
|
||||
$data = $service->buildPageData();
|
||||
$values = is_array($data['values'] ?? null) ? $data['values'] : [];
|
||||
|
||||
$this->assertTrue((bool) ($values['microsoft_auto_remember_default'] ?? false));
|
||||
$this->assertSame(14, (int) ($values['remember_token_lifetime_days'] ?? 0));
|
||||
}
|
||||
|
||||
public function testUpdateFromAdminPersistsLoginPersistenceValues(): void
|
||||
{
|
||||
$loginPersistenceGateway = $this->createMock(SettingsLoginPersistenceGateway::class);
|
||||
$loginPersistenceGateway->method('isMicrosoftAutoRememberDefault')->willReturn(false);
|
||||
$loginPersistenceGateway->method('getRememberTokenLifetimeDays')->willReturn(30);
|
||||
$loginPersistenceGateway->expects($this->once())
|
||||
->method('setMicrosoftAutoRememberDefault')
|
||||
->with(true);
|
||||
$loginPersistenceGateway->expects($this->once())
|
||||
->method('setRememberTokenLifetimeDays')
|
||||
->with(14)
|
||||
->willReturn(true);
|
||||
|
||||
$service = $this->newService(loginPersistenceGateway: $loginPersistenceGateway);
|
||||
$result = $service->updateFromAdmin($this->validPostData([
|
||||
'microsoft_auto_remember_default' => '1',
|
||||
'remember_token_lifetime_days' => '14',
|
||||
]));
|
||||
|
||||
$this->assertSame([], $result['errors'] ?? []);
|
||||
}
|
||||
|
||||
public function testUpdateFromAdminReturnsErrorForInvalidRememberTtl(): void
|
||||
{
|
||||
$loginPersistenceGateway = $this->createMock(SettingsLoginPersistenceGateway::class);
|
||||
$loginPersistenceGateway->method('isMicrosoftAutoRememberDefault')->willReturn(false);
|
||||
$loginPersistenceGateway->method('getRememberTokenLifetimeDays')->willReturn(30);
|
||||
$loginPersistenceGateway->method('setMicrosoftAutoRememberDefault');
|
||||
$loginPersistenceGateway->expects($this->once())
|
||||
->method('setRememberTokenLifetimeDays')
|
||||
->with(500)
|
||||
->willReturn(false);
|
||||
|
||||
$service = $this->newService(loginPersistenceGateway: $loginPersistenceGateway);
|
||||
$result = $service->updateFromAdmin($this->validPostData([
|
||||
'remember_token_lifetime_days' => '500',
|
||||
]));
|
||||
|
||||
$errors = is_array($result['errors'] ?? null) ? $result['errors'] : [];
|
||||
$errorKeys = [];
|
||||
foreach ($errors as $error) {
|
||||
if (is_array($error) && isset($error['key'])) {
|
||||
$errorKeys[] = (string) $error['key'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertContains('remember_token_lifetime_days_invalid', $errorKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $overrides
|
||||
* @return array<string, mixed>
|
||||
@@ -157,7 +226,8 @@ class AdminSettingsServiceSessionPolicyTest extends TestCase
|
||||
|
||||
private function newService(
|
||||
?SettingsSessionGateway $settingsSessionGateway = null,
|
||||
?SettingsMetadataGateway $settingsMetadataGateway = null
|
||||
?SettingsMetadataGateway $settingsMetadataGateway = null,
|
||||
?SettingsLoginPersistenceGateway $loginPersistenceGateway = null
|
||||
): AdminSettingsService {
|
||||
$settingsDefaultsGateway = $this->createConfiguredMock(SettingsDefaultsGateway::class, [
|
||||
'getDefaultTenantId' => null,
|
||||
@@ -239,6 +309,13 @@ class AdminSettingsServiceSessionPolicyTest extends TestCase
|
||||
'setSmtpFromName' => true,
|
||||
]);
|
||||
|
||||
$loginPersistenceGateway = $loginPersistenceGateway ?? $this->createConfiguredMock(SettingsLoginPersistenceGateway::class, [
|
||||
'isMicrosoftAutoRememberDefault' => false,
|
||||
'getRememberTokenLifetimeDays' => 30,
|
||||
'setMicrosoftAutoRememberDefault' => true,
|
||||
'setRememberTokenLifetimeDays' => true,
|
||||
]);
|
||||
|
||||
if ($settingsMetadataGateway === null) {
|
||||
$settingsMetadataGateway = $this->createMock(SettingsMetadataGateway::class);
|
||||
$settingsMetadataGateway->method('getValue')->willReturn(null);
|
||||
@@ -281,6 +358,7 @@ class AdminSettingsServiceSessionPolicyTest extends TestCase
|
||||
$settingsFrontendTelemetryGateway,
|
||||
$settingsMicrosoftGateway,
|
||||
$settingsSmtpGateway,
|
||||
$loginPersistenceGateway,
|
||||
$settingsMetadataGateway,
|
||||
$settingCacheService,
|
||||
$tenantService,
|
||||
|
||||
178
tests/Service/Settings/SettingsLoginPersistenceGatewayTest.php
Normal file
178
tests/Service/Settings/SettingsLoginPersistenceGatewayTest.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Settings;
|
||||
|
||||
use MintyPHP\Service\Settings\SettingsLoginPersistenceGateway;
|
||||
use MintyPHP\Service\Settings\SettingsMetadataGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SettingsLoginPersistenceGatewayTest extends TestCase
|
||||
{
|
||||
// ---------------------------------------------------------------
|
||||
// isMicrosoftAutoRememberDefault
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testAutoRememberDefaultReturnsFalseWhenNotSet(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getValue')->willReturn(null);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertFalse($gateway->isMicrosoftAutoRememberDefault());
|
||||
}
|
||||
|
||||
public function testAutoRememberDefaultReturnsFalseForZero(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getValue')->willReturn('0');
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertFalse($gateway->isMicrosoftAutoRememberDefault());
|
||||
}
|
||||
|
||||
public function testAutoRememberDefaultReturnsTrueForOne(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getValue')->willReturn('1');
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertTrue($gateway->isMicrosoftAutoRememberDefault());
|
||||
}
|
||||
|
||||
public function testAutoRememberDefaultReturnsFalseForEmptyString(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getValue')->willReturn('');
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertFalse($gateway->isMicrosoftAutoRememberDefault());
|
||||
}
|
||||
|
||||
public function testSetAutoRememberDefaultPersistsValue(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->expects($this->once())
|
||||
->method('set')
|
||||
->with('microsoft_auto_remember_default', '1', $this->anything())
|
||||
->willReturn(true);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertTrue($gateway->setMicrosoftAutoRememberDefault(true));
|
||||
}
|
||||
|
||||
public function testSetAutoRememberDefaultPersistsFalseAsZero(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->expects($this->once())
|
||||
->method('set')
|
||||
->with('microsoft_auto_remember_default', '0', $this->anything())
|
||||
->willReturn(true);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertTrue($gateway->setMicrosoftAutoRememberDefault(false));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// getRememberTokenLifetimeDays
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testLifetimeDaysReturnsFallbackWhenNotSet(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getInt')->willReturn(null);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertSame(30, $gateway->getRememberTokenLifetimeDays());
|
||||
}
|
||||
|
||||
public function testLifetimeDaysReturnsConfiguredValue(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getInt')->willReturn(7);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertSame(7, $gateway->getRememberTokenLifetimeDays());
|
||||
}
|
||||
|
||||
public function testLifetimeDaysReturnsFallbackForZero(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getInt')->willReturn(0);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertSame(30, $gateway->getRememberTokenLifetimeDays());
|
||||
}
|
||||
|
||||
public function testLifetimeDaysReturnsFallbackForValueAboveMax(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getInt')->willReturn(500);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertSame(30, $gateway->getRememberTokenLifetimeDays());
|
||||
}
|
||||
|
||||
public function testSetLifetimeDaysAcceptsValidRange(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->expects($this->once())
|
||||
->method('set')
|
||||
->with('remember_token_lifetime_days', '14', $this->anything())
|
||||
->willReturn(true);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertTrue($gateway->setRememberTokenLifetimeDays(14));
|
||||
}
|
||||
|
||||
public function testSetLifetimeDaysRejectsZero(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->expects($this->never())->method('set');
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertFalse($gateway->setRememberTokenLifetimeDays(0));
|
||||
}
|
||||
|
||||
public function testSetLifetimeDaysRejectsAboveMax(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->expects($this->never())->method('set');
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertFalse($gateway->setRememberTokenLifetimeDays(400));
|
||||
}
|
||||
|
||||
public function testSetLifetimeDaysNullFallsBackToDefault(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->expects($this->once())
|
||||
->method('set')
|
||||
->with('remember_token_lifetime_days', '30', $this->anything())
|
||||
->willReturn(true);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertTrue($gateway->setRememberTokenLifetimeDays(null));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// getRememberTokenLifetimeSeconds
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testLifetimeSecondsComputesCorrectly(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getInt')->willReturn(7);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertSame(7 * 86400, $gateway->getRememberTokenLifetimeSeconds());
|
||||
}
|
||||
|
||||
public function testLifetimeSecondsFallbackIs30Days(): void
|
||||
{
|
||||
$metadata = $this->createMock(SettingsMetadataGateway::class);
|
||||
$metadata->method('getInt')->willReturn(null);
|
||||
|
||||
$gateway = new SettingsLoginPersistenceGateway($metadata);
|
||||
$this->assertSame(30 * 86400, $gateway->getRememberTokenLifetimeSeconds());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user