Direct Password and MFA Authentication with MS Entra ID - #1520
Merged
Conversation
nooreldeenmansour
force-pushed
the
002-entra-password-mfa-auth
branch
4 times, most recently
from
May 18, 2026 13:22
71977ed to
d44496f
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1520 +/- ##
==========================================
+ Coverage 83.47% 87.33% +3.85%
==========================================
Files 21 124 +103
Lines 1398 8907 +7509
Branches 0 111 +111
==========================================
+ Hits 1167 7779 +6612
- Misses 231 1072 +841
- Partials 0 56 +56 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
nooreldeenmansour
force-pushed
the
002-entra-password-mfa-auth
branch
8 times, most recently
from
May 20, 2026 12:18
95281f3 to
c593968
Compare
nooreldeenmansour
force-pushed
the
002-entra-password-mfa-auth
branch
12 times, most recently
from
June 5, 2026 12:18
552221e to
4b2a458
Compare
nooreldeenmansour
marked this pull request as ready for review
June 5, 2026 12:19
nooreldeenmansour
force-pushed
the
002-entra-password-mfa-auth
branch
2 times, most recently
from
June 8, 2026 17:51
7f4a985 to
e3a0ec1
Compare
adombeck
reviewed
Jul 2, 2026
adombeck
reviewed
Jul 2, 2026
adombeck
reviewed
Jul 2, 2026
adombeck
approved these changes
Jul 2, 2026
Move `DeviceRegistrationData` and its validation helper into an untagged file so cached device-registration JSON can be validated without the `libhimmelblau` cgo build.
Add the Go-side types and Cgo bindings needed to start and continue the `libhimmelblau` MFA flow from the broker. Also add helpers to extract user identity fields from access token claims, since the Entra password flow returns an access token rather than a standard OIDC ID token. Bump `libhimmelblau` to the revision that exposes the MFA C API.
The previous `sync.Once` singleton permanently reused the first client configuration that initialized the broker app. Replace it with a keyed cache so device registration and Entra password login can reuse separate broker apps without interfering with each other.
When the delegated token cannot call Microsoft Graph directly, let `GetGroups` fall back to an app-only client-credentials token derived from the configured OIDC client secret. Also keep the Graph-token requirement in cached auth state instead of provider-global mutable state.
Implement the provider methods that start the Entra password flow and complete its MFA challenge. Also advertise `entra_password` in the supported auth modes.
Split `HashAndStorePassword` into separate hashing and persistence steps so callers can hash the plaintext immediately and write the result to disk only after MFA succeeds.
Add the broker-side login flow for direct Entra password authentication and its MFA follow-up modes, including challenge routing, `AADSTS` error handling, and finalization of cached tokens plus the local password hash. Also add `[flows]` configuration for bootstrap auth mode selection and validate the Entra password prerequisites, disabling the flow when neither device registration nor a configured client secret can support group lookup after login.
The current e2e environment relies on device auth flow, so keep the new flow disabled there for now to avoid breaking the current expected flow, until e2e tests for `entra_password` are added
The Entra password + MFA path cannot use the standard OIDC ID token trust path: the MFA result is for Microsoft first-party resources, and libhimmelblau also exposes id_token-derived helpers without signature verification. Verifying an access token while binding identity from a different, unverified source would leave first-login identity outside the control. Verify the MFA access token's RS256 signature against the tenant JWKS, including Microsoft's nonce rewrite, and reject expired or wrong-tenant tokens. Derive first-login and refresh identity from the verified access-token claims via UserInfoFromAccessToken, keep JWKS-backed calls under request timeouts, and drop the now-dead id_token-derived helpers. When a refresh succeeds but later local validation fails, persist the rotated refresh token before denying the login. Otherwise a local clock skew or claim-mapping problem could strand the cache with a refresh token Entra already invalidated server-side. The cgo-free tokenverify package covers signatures, nonce rewriting, expiry, tenant binding, and malformed JWKS cases directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When MFA setup fails (authenticator not registered, interactive auth required), libhimmelblau silently fell back to its own Device Authorization Grant flow, showing a plain browser-URL prompt that conflicts with authd's QR-code Device Authentication screen and bypasses the auth state machine entirely. AADSTS 50203 and 16000 surface the same MFA-not-configured condition from a different server path and are routed to Device Authentication for the same reason. Based on https://gitlab.com/nooreldeensalah/libhimmelblau/-/tree/capi-mfa-auth-options
libhimmelblau now returns dedicated error variants for the three MFA outcomes that authd previously classified by matching on error message text. Remove the string-matching blocks from newMFAError and map the new C enum codes directly in mfaErrorCategory.
The Entra password+MFA login failed with "invalid character 'Y' looking
for beginning of value" after a successful grant. The broker emitted the
success message as a bare string while the consumer (dataToMsg) expects
a {"message": ...} envelope, so the PAM client's parse aborted an
already-granted login.
Forward a consistent {userinfo, message} envelope from IsAuthenticated
so consumers always parse the same shape regardless of whether the
broker attached a notice. Encode IAResponse.Msg as {"message": ...}
matching the format used for non-granted replies. Non-string values in
the broker's message field are treated as absent rather than rejected,
so a malformed cosmetic field never blocks an already-granted login at
the broker boundary.
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
A cosmetic notice attached to a granted IAResponse must never abort an already-granted login. Treat a parse error on the Msg field as a warning and fall back to showing no notice. Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
After the Entra password+MFA flow caches the user's password for offline login, the user had no indication that their local password was set to their Entra password Attach a broker-owned notice to the granted response so it surfaces through the PAM conversation. The notice lives in the broker — the component that knows when caching occurred — rather than being hardcoded in authd. Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Native (SSH, non-TTY) clients receive the granted message twice: once through nativeModel.sendInfo and again through the PAM TextInfo conversation echo in sendReturnMessageToPam. GDM and interactive- terminal clients do not go through the native sendInfo path, so they must keep receiving the echo. Suppress the redundant PAM-conversation echo only for Native clients by returning false from shouldSendAuthMessage when clientType == Native and the response is a success. Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Bump the libhimmelblau submodule to the latest version `v0.8.24` which contains the necessary patches for this PR The upstream GitLab MR is https://gitlab.com/samba-team/libhimmelblau/-/merge_requests/163
The new entra_password flow has no automated coverage. Add an end-to-end test that exercises the register_device=true configuration: the broker authenticates via the Microsoft Broker App, registers the device on first login, and caches the password locally for subsequent offline use. Disabling device_auth ensures the broker auto-selects the single available mode, keeping the test focused on the password+MFA flow without an interactive provider-selection step.
Cover the register_device=false configuration, where the broker uses a configured client_secret to obtain an app-only Graph token for group lookup instead of registering a device. The secret is injected into broker.conf at test setup time rather than baked into the provisioning snapshot, keeping the base image clean for public-client flows. Without the secret the test fails immediately at setup, surfacing a misconfigured CI run as a clear error instead of a silent missing-auth-mode failure.
When client_secret is configured alongside register_device, refresh token requests against the Microsoft Broker App fail with AADSTS700025 — Entra ID rejects secrets on public clients unconditionally. The Graph API credential (cfg.clientSecret) goes to the client-credentials endpoint, not the OIDC token endpoint, so it is separate and unaffected.
`entra_password` is an Entra ID-specific broker option, but these tests ran against every broker, including google, where the config keys don't apply and the password+MFA UI never appears. CI runs against the google broker were failing instead of skipping.
When the Entra password flow is enabled without any way to read group memberships, users cannot actually use that flow. The previous startup check only rejected the configuration when it was the sole enabled auth flow; if device_auth was also enabled, the broker silently hid the bad configuration behind a fallback to device auth. Reject that configuration whenever the Entra password flow is enabled but has no group source available. Without a group source, any login through it would fail at the group-fetch step — an undiagnosable error at login time; failing startup surfaces it immediately.
refreshToken and refreshEntraPasswordToken both need to force a refresh even when the cached token has not actually expired (it's the liveness/revocation check, not an expiry check), but did it two different ways: refreshToken backdated the cached token's Expiry field in place, while RefreshEntraPasswordToken (which only ever received the refresh token string) built a bare token with just the refresh token, relying on oauth2.Token.Valid requiring a non-empty AccessToken. Standardize on the latter in refreshToken too. It has the same effect without mutating the caller's cached oldToken, which the backdating approach did as a side effect (harmless today since nothing reads Expiry afterwards, but not guaranteed to stay that way).
16 pre-existing tests fail under -tags withmsentraid with "invalid configuration: the entra_password flow is enabled" because the minimal broker configs generated for tests omit [flows], leaving entra_password at its default (enabled). Under the withmsentraid tag the provider implements EntraPasswordProvider, so the startup validation added in 452cd00 rejects the config before any test logic runs. Disable the flow explicitly in both test config generators, matching the pattern already used by provision-authd.sh for E2E tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a direct password + MFA authentication flow to the MS Entra ID broker as an additional bootstrap authentication method alongside device authentication. Users can authenticate with
entra_password, complete MFA via push notification (entra_mfa_wait) or OTP/SMS code (entra_mfa_code), then authd caches the OAuth2 token and stores an Argon2id password hash for offline re-authentication.Changes Summary
GroupMember.Read.Alland aclient_secretis configured.[flows]toggles forentra_passwordanddevice_auth, and validate the prerequisites forentra_passwordat startup. When neitherregister_device = truenor a configuredclient_secretcan support group lookup, the flow is disabled or rejected if it would leave no bootstrap login method.Related Issues and Discussions (WIP)
UDENG-6756
Closes #857
Closes #886
Closes #561