feat(auth): add Microsoft ADFS auth support#292
Conversation
## Overview
Adds a Microsoft ADFS (Active Directory Federation Services) authenticator plugin supporting OIDC single sign-on (SSO), token refresh, and integrated health checking.
## Key Changes
- **Authenticator Plugin**: Implements `MicrosoftADFSAuthenticator` for code flow authentication, secure claims resolution from ID tokens, and token refresh.
- **Factory & Helper Integration**: Registers ADFS as an OAuth provider in the factory and helper utilities.
- **Callback API**: Exposes the `/v1/oauth/adfs/callback` endpoint to handle the ADFS auth redirect.
- **Validation**: Adds `validate_microsoft_adfs_config` schema validation for critical configurations.
## Configuration Template
```json
{
"client_id": "YOUR_ADFS_CLIENT_ID",
"client_secret": "YOUR_ADFS_CLIENT_SECRET",
"authority": "https://fs.your-domain.com",
"redirect_uri": "https://your-domain.com/auth/adfs/callback",
"scopes": ["openid", "profile", "email"],
"response_type": "code",
"response_mode": "query"
}
📝 WalkthroughWalkthroughAdds Microsoft ADFS OAuth/OIDC support: new authenticator type and config, ADFS authenticator implementation (auth-code, token exchange, id_token JWKS verification, user mapping, refresh/health), factory and helper wiring, server-side single-use flow state with nonce, and ADFS callback endpoint with middleware bypass. ChangesMicrosoft ADFS Authentication Support
Sequence Diagram(s)sequenceDiagram
participant Client
participant require_auth
participant auth_plugin_controller
participant CacheManager
participant MicrosoftADFSAuthenticator
participant ADFS
Client->>require_auth: GET /v1/oauth/adfs/callback?code&state
require_auth->>auth_plugin_controller: allow optional auth path
auth_plugin_controller->>CacheManager: consume state -> auth_id + nonce
CacheManager-->>auth_plugin_controller: flow record (or miss)
auth_plugin_controller->>MicrosoftADFSAuthenticator: handle_callback(callback_data, expected_nonce)
MicrosoftADFSAuthenticator->>ADFS: POST token endpoint (code exchange)
ADFS-->>MicrosoftADFSAuthenticator: tokens (access_token, id_token)
MicrosoftADFSAuthenticator->>MicrosoftADFSAuthenticator: decode id_token (JWKS)
MicrosoftADFSAuthenticator-->>auth_plugin_controller: AuthResult
auth_plugin_controller-->>Client: redirect / response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@wavefront/server/modules/plugins_module/plugins_module/utils/authenticator_helper.py`:
- Around line 64-87: The validator function validate_microsoft_adfs_config
currently omits the dataclass-mandated redirect fields; update it to require and
validate client_redirect_success_url and client_redirect_failure_url (ensure
they exist and are valid HTTP/HTTPS URLs similarly to redirect_uri) and add
appropriate error messages to the returned errors list so the
MicrosoftADFSConfig dataclass will not fail on instantiation.
- Line 163: The template's redirect_uri value is pointing at the placeholder
'/auth/adfs/callback' which does not match this PR's actual callback endpoint;
update the 'redirect_uri' entry in authenticator_helper.py (the dictionary key
'redirect_uri') to use '/v1/oauth/adfs/callback' (including the proper domain,
e.g. 'https://your-domain.com/v1/oauth/adfs/callback') so OAuth auth-code
responses are routed to the correct callback handler.
In
`@wavefront/server/modules/user_management_module/user_management_module/controllers/auth_plugin_controller.py`:
- Around line 341-343: The ADFS callback still treats `code` as required because
`Query(...)` enforces presence even when the type is `Optional[str]`, so FastAPI
returns 422 before `_handle_oauth_callback()` can process error-only redirects.
Update the callback parameter declaration in `auth_plugin_controller` so `code`
is truly optional by using a non-required query default, and keep the existing
`state` and `error` handling unchanged so requests with only `error` can reach
the redirect failure path.
In
`@wavefront/server/plugins/authenticator/authenticator/microsoft_adfs/authenticator.py`:
- Around line 139-157: The Microsoft ADFS auth flow in get_authorization_url is
trusting caller-provided state JSON instead of using a server-generated
correlation value. Update this flow to generate a random opaque state, persist
it server-side together with the expected auth_id and a nonce, and send only
that opaque state in the authorization URL. Then, in the callback handling path,
verify the returned state matches the stored one and validate the nonce in the
id_token before continuing.
- Around line 311-323: The current _decode_id_token_claims simply base64-decodes
the JWT and skips verification; update the flow to fully validate the ID Token
as an OIDC RP: replace naive decoding in _decode_id_token_claims with signature
verification against the IdP JWKS (e.g., PyJWT’s PyJWKClient), then enforce
standard claims (iss matches IdP issuer, aud includes our client_id, exp/nbf
with a configurable clock skew) and verify nonce before mapping claims; also
bind and validate state/nonce between get_authorization_url and handle_callback
(store nonce/state per auth_id when building the authorization URL and check
them in handle_callback before calling authenticate) so authenticate only
receives claims after signature and claim checks pass.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3b4efabd-ac04-42b8-a67a-f708cfb10790
📒 Files selected for processing (10)
wavefront/server/modules/plugins_module/plugins_module/utils/authenticator_helper.pywavefront/server/modules/user_management_module/user_management_module/authorization/require_auth.pywavefront/server/modules/user_management_module/user_management_module/controllers/auth_plugin_controller.pywavefront/server/plugins/authenticator/authenticator/__init__.pywavefront/server/plugins/authenticator/authenticator/factory.pywavefront/server/plugins/authenticator/authenticator/helper.pywavefront/server/plugins/authenticator/authenticator/microsoft_adfs/__init__.pywavefront/server/plugins/authenticator/authenticator/microsoft_adfs/authenticator.pywavefront/server/plugins/authenticator/authenticator/microsoft_adfs/config.pywavefront/server/plugins/authenticator/authenticator/types.py
| def get_authorization_url(self, state: Optional[str] = None) -> Optional[str]: | ||
| if not state: | ||
| raise ValueError("State doesn't exist Microsoft ADFS") | ||
|
|
||
| state_obj = json.loads(state) | ||
| if state_obj.get('auth_id') is None: | ||
| raise ValueError("Auth Id doesn't exist in Microsoft ADFS state") | ||
|
|
||
| params = { | ||
| 'response_type': self.config.response_type, | ||
| 'client_id': self.config.client_id, | ||
| 'redirect_uri': self.config.redirect_uri, | ||
| 'scope': ' '.join(self.config.scopes), | ||
| 'state': state, | ||
| 'response_mode': self.config.response_mode, | ||
| 'prompt': 'select_account', | ||
| } | ||
|
|
||
| return f'{self.auth_url}?{urlencode(params)}' |
There was a problem hiding this comment.
Bind state to server-side state instead of echoing raw auth_id.
This flow only checks that the caller-supplied JSON contains auth_id, then sends that same blob to ADFS and later trusts it on callback. There is no nonce or server-side correlation, so the new ADFS login can be replayed or used for login CSRF, and there is no value available to validate a nonce claim in the id_token.
Generate a random state value, persist it with the expected auth_id/nonce, and reject callbacks whose state does not match.
In OAuth 2.0 / OpenID Connect authorization-code flow, how should the `state` parameter and `nonce` be generated and validated to prevent login CSRF and replay?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@wavefront/server/plugins/authenticator/authenticator/microsoft_adfs/authenticator.py`
around lines 139 - 157, The Microsoft ADFS auth flow in get_authorization_url is
trusting caller-provided state JSON instead of using a server-generated
correlation value. Update this flow to generate a random opaque state, persist
it server-side together with the expected auth_id and a nonce, and send only
that opaque state in the authorization URL. Then, in the callback handling path,
verify the returned state matches the stored one and validate the nonce in the
id_token before continuing.
## Overview Implements server-side, Redis-backed OAuth state verification to prevent CSRF / session-fixation attacks across all OAuth providers (Google, Microsoft Entra, and Microsoft ADFS), and adds cryptographic OIDC `nonce` claim validation to the ADFS provider's ID token decoder. ## Key Changes - **State Hardening (CSRF Protection)**: Replaced legacy predictable client-side JSON states with opaque 32-byte tokens (`state` & `nonce`) generated at flow initialization. - **Redis Integration**: Flow details are cached with a 10-minute (600s) TTL. The callback handler consumes and deletes the state on first use to enforce one-shot token lifespan and prevent replays. - **OIDC Nonce Validation**: Extended the `AuthenticatorABC` interface with optional `nonce` and `expected_nonce` params. The `MicrosoftADFSAuthenticator` now extracts and verifies the `nonce` claim from verified `id_token` signatures. - **Backward Compatibility**: Updated Google and Microsoft Entra providers to treat `state` as an opaque string without breaking existing callback routes.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
wavefront/server/modules/user_management_module/user_management_module/controllers/auth_plugin_controller.py (1)
352-354:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
codeis still required despiteOptionaltype annotation.The comment indicates
codeshould be optional for error scenarios, butQuery(...)makes it a required parameter regardless of theOptional[str]type hint. FastAPI will return 422 before_handle_oauth_callback()can handle error-only redirects.Proposed fix
code: Optional[str] = Query( - ... + None ), # keeping it optional as in error scenarios we dont get code🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@wavefront/server/modules/user_management_module/user_management_module/controllers/auth_plugin_controller.py` around lines 352 - 354, The parameter `code` in `_handle_oauth_callback` is annotated Optional[str] but uses `Query(...)`, which makes it required; change the Query call to provide an explicit default of None (e.g., `Query(None, ...)` or `Query(default=None, ...)`) so FastAPI treats `code` as optional and allows the handler to run on error-only redirects; update the `code` parameter in the `_handle_oauth_callback` function signature accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@wavefront/server/modules/user_management_module/user_management_module/controllers/auth_plugin_controller.py`:
- Around line 352-354: The parameter `code` in `_handle_oauth_callback` is
annotated Optional[str] but uses `Query(...)`, which makes it required; change
the Query call to provide an explicit default of None (e.g., `Query(None, ...)`
or `Query(default=None, ...)`) so FastAPI treats `code` as optional and allows
the handler to run on error-only redirects; update the `code` parameter in the
`_handle_oauth_callback` function signature accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3daabd38-5d87-47ea-8ece-fc7842533cbc
📒 Files selected for processing (6)
wavefront/server/modules/user_management_module/user_management_module/controllers/auth_plugin_controller.pywavefront/server/plugins/authenticator/authenticator/email_password/authenticator.pywavefront/server/plugins/authenticator/authenticator/google_oauth/authenticator.pywavefront/server/plugins/authenticator/authenticator/microsoft_adfs/authenticator.pywavefront/server/plugins/authenticator/authenticator/microsoft_oauth/authenticator.pywavefront/server/plugins/authenticator/authenticator/types.py
Overview
Adds a Microsoft ADFS (Active Directory Federation Services) authenticator plugin supporting OIDC single sign-on (SSO), token refresh, and integrated health checking.
Key Changes
MicrosoftADFSAuthenticatorfor code flow authentication, secure claims resolution from ID tokens, and token refresh./v1/oauth/adfs/callbackendpoint to handle the ADFS auth redirect.validate_microsoft_adfs_configschema validation for critical configurations.Summary by CodeRabbit