mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-05-09 08:25:54 +02:00
[PR #1118] [MERGED] feat(oidc): Azure Entra ID support — configurable email claim & verification + Remember Me persistent login #1166
Labels
No labels
A1
automated
automated
bug
bug
Closed due to inactivity
contrib
dependencies
dependencies
duplicate
enhancement
feedback
hold
invalid
Notes
P1S
pull-request
security
ThumbsUp
user-report
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/bambuddy-maziggy-1#1166
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/maziggy/bambuddy/pull/1118
Author: @netscout2001
Created: 4/24/2026
Status: ✅ Merged
Merged: 4/25/2026
Merged by: @maziggy
Base:
dev← Head:feature/2fa-oidc-authentication📝 Commits (10+)
a3e4b96feat(oidc): add Azure Entra ID support with configurable email claim resolution50e9cd3fix(oidc): harden email claim resolution, guards, and test coverage460818bfix(oidc): apply maintainer review fixes for Azure Entra ID supportfad989dfeat(auth): add Remember Me option to login page0640101fix(db): swallow 'no such column' in _safe_execute for idempotent RENAME COLUMN migrations190c268fix(oidc): harden _safe_execute, extract email/autolink helpers, add test coverage9c18b7ffix(db): restore 'no such column' and 'duplicate column name' to _safe_execute swallow listd7fb0e6fix(db): swallow PostgreSQL 'does not exist' error for idempotent RENAME COLUMN migrations216d16efix(db,auth): move DML backfills out of _safe_execute; split setAuthToken storage writesc990775fix(oidc,db): address PR #1118 maintainer review — migration safety, email shape checks, dialect branching📊 Changes
22 files changed (+2237 additions, -74 deletions)
View changed files
📝
backend/app/api/routes/mfa.py(+136 -18)📝
backend/app/core/database.py(+132 -19)📝
backend/app/models/oidc_provider.py(+24 -1)📝
backend/app/schemas/auth.py(+56 -3)📝
backend/tests/integration/test_mfa_api.py(+777 -0)📝
backend/tests/unit/test_db_dialect.py(+313 -4)➕
docs/authentication/entra-id.md(+82 -0)📝
frontend/src/__tests__/api/client.test.ts(+56 -0)➕
frontend/src/__tests__/components/OIDCProviderSettings.test.tsx(+125 -0)📝
frontend/src/__tests__/pages/LoginPage.test.tsx(+328 -1)📝
frontend/src/api/client.ts(+25 -11)📝
frontend/src/components/OIDCProviderSettings.tsx(+50 -1)📝
frontend/src/contexts/AuthContext.tsx(+9 -9)📝
frontend/src/i18n/locales/de.ts(+8 -0)📝
frontend/src/i18n/locales/en.ts(+8 -0)📝
frontend/src/i18n/locales/fr.ts(+8 -0)📝
frontend/src/i18n/locales/it.ts(+8 -0)📝
frontend/src/i18n/locales/ja.ts(+8 -0)📝
frontend/src/i18n/locales/pt-BR.ts(+8 -0)📝
frontend/src/i18n/locales/zh-CN.ts(+8 -0)...and 2 more files
📄 Description
Description
This PR combines two improvements to the OIDC / authentication flow:
1 — Azure Entra ID support + hardening (maintainer review fixes)
Adds Azure Entra ID support to the OIDC provider configuration by introducing two new fields:
email_claimandrequire_email_verified.Azure Entra ID never includes the
email_verifiedclaim in its ID tokens. The previous code dropped the email wheneveremail_verifiedwas absent (fail-closed), breaking Azure logins. This PR introduces three configurable email resolution paths:email_claim="email"+require_email_verified=true— standard behaviour, unchangedemail_claim="email"+require_email_verified=false— permissive mode, accepts email whenemail_verifiedis absentemail_claim != "email"— custom claim (preferred_username,upn), noemail_verifiedcheck; recommended for Azure Entra IDauto_link_existing_accountsis blocked at schema level, route level, and DB level when either new field would make auto-linking unsafe (SEC-1/SEC-6).This PR also addresses the subsequent maintainer review feedback:
_EMAIL_SHAPE_REis now precompiled at module level and usesfullmatch()with a stricter pattern that requires a dot in the domain (local@domain.tld) — rejectsx@nodot,@domain,x@, etc.provider_id,email_claimname, andsubare emitted.= 0/= 1to= FALSE/= TRUEso the constraint compiles correctly on PostgreSQL._safe_executere-raises on real errors: Non-idempotency errors are now logged withlogger.errorand re-raised so a failed migration aborts startup loudly instead of silently continuing._safe_executeswallows PostgreSQL RENAME COLUMN idempotency errors:column "xyz" does not exist(raised by PostgreSQL when re-running an already-appliedRENAME COLUMNmigration) is now swallowed correctly, fixing a startup failure on existing PostgreSQL installations._safe_execute: The three data-backfill statements (UPDATE api_keys,UPDATE users SET password_changed_at,DELETE FROM settings) now use directconn.execute()inside a savepoint instead of going through_safe_execute. This ensures any column-reference failure is always fatal and never silently swallowed — most importantly for the security-criticalpassword_changed_atbackfill.provider_emaillowercasing migration: Normalises existing mixed-case UPN values (common with Azure Entra ID) to lowercase for consistent matching.docs/authentication/entra-id.mdcovering app registration, client secret, issuer URL, BamBuddy provider config,preferred_usernamevsemailclaim, and a troubleshooting table._safe_executereraise/swallow behaviour,provider_emaillowercasing migration, PostgreSQL RENAME COLUMN idempotency, andFALSE/TRUECheckConstraint enforcement.2 — Remember Me persistent login
sessionStorageandlocalStorage, so the session survives a tab close / browser restart.TokenPersistence = 'session' | 'persistent'type, replacing the previouspersist?: booleanparameter throughoutclient.ts,AuthContext, andLoginPage.sessionStorageasauth_remember_me=1before the provider redirect and consumed (read + removed atomically) in the OIDC callbackuseEffect, so the setting carries through the external redirect where React state is lost.sessionStorageandlocalStoragewrites/removals now happen in independenttry/catchblocks so a failure on one storage (e.g.localStorageblocked in private mode) cannot prevent the other from being cleared — a previously possible scenario where a user could believe they were logged out while theirlocalStoragetoken remained active.try-catchwithconsole.warnto handle private browsing / quota errors gracefully (in-memory token still works for the current tab).rememberMei18n key added to all 8 locale files (en, de, fr, it, pt-BR, zh-CN, ja, zh-TW).client.test.ts; Remember Me checkbox, session/persistent storage, OIDC carry-through, OIDC+2FA carry-through, error cleanup, and negative cases inLoginPage.test.tsx.Related Issues
Fixes #902 #1088
Documentation
https://github.com/maziggy/bambuddy-wiki/pull/19
Type of Change
Changes Made
Backend
backend/app/api/routes/mfa.py— precompiled_EMAIL_SHAPE_RE, PII-free log, three-case email resolution (Case A/B/C),_is_valid_email_shapedhelper, Combined-State-Guard in PUT handlerbackend/app/models/oidc_provider.py—email_claimandrequire_email_verifiedORM fields,CheckConstraintusesFALSE/TRUEbackend/app/core/database.py— column migrations for new fields, PostgreSQLADD CONSTRAINTmigration,_safe_executelogs + re-raises non-idempotency errors + swallows PostgreSQL RENAME COLUMN idempotency errors, DML backfills moved to directconn.execute(),provider_emaillowercasing migrationbackend/app/schemas/auth.py— new fields onOIDCProviderCreate/OIDCProviderUpdate/OIDCProviderResponse,model_validatorfor SEC-1/SEC-6 on create and updatebackend/tests/integration/test_mfa_api.py— 19 new tests: 13-case parametrized matrix (Case A/B/C), SEC-1/SEC-6 guard tests, Combined-State-Guard tests, response field coverage,no-dot-in-domainmatrix casebackend/tests/unit/test_db_dialect.py— real SQLite integration tests for_safe_executeand migrations, PostgreSQL RENAME COLUMN idempotency mock testdocs/authentication/entra-id.md— new Azure Entra ID setup guideFrontend
frontend/src/api/client.ts—TokenPersistencetype, updatedsetAuthTokenwith split try/catch for independent storage failure handling, new fields onOIDCProviderandOIDCProviderCreateinterfacesfrontend/src/contexts/AuthContext.tsx—login/loginWithTokenacceptTokenPersistencefrontend/src/pages/LoginPage.tsx— Remember Me checkbox,consumeSavedRememberMe(),toPersistence()helper, OIDC sessionStorage flagfrontend/src/components/OIDCProviderSettings.tsx— toggle forrequire_email_verified, input foremail_claim, both shown in provider info viewfrontend/src/i18n/locales/*.ts—rememberMekey + OIDC settings keys in all 8 locale filesfrontend/src/__tests__/api/client.test.ts— 5 new persistence mode tests (including independent storage-failure paths on logout)frontend/src/__tests__/pages/LoginPage.test.tsx— 9 new Remember Me / OIDC carry-through testsScreenshots
Testing
149 backend tests pass (
pytest tests/integration/test_mfa_api.py tests/unit/test_db_dialect.py). 1479 frontend tests pass (vitest run). No ruff or TypeScript warnings.Checklist
Additional Notes
Existing OIDC providers and login flows are fully backwards-compatible. Both new features are opt-in by default:
require_email_verified=true/email_claim='email'preserves the previous behaviour; the Remember Me checkbox defaults to unchecked.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.