Skip to content

[management] Enable MFA for local users#5804

Open
jnfrati wants to merge 21 commits intomainfrom
feat/local-user-totp
Open

[management] Enable MFA for local users#5804
jnfrati wants to merge 21 commits intomainfrom
feat/local-user-totp

Conversation

@jnfrati
Copy link
Copy Markdown
Contributor

@jnfrati jnfrati commented Apr 6, 2026

Describe your changes

Added support to enable MFA (TOTP only) for the embedded IDP.
Now the server can use the config server.auth.enableLocalMFA which will automatically configure TOTP for all local users.

Issue ticket number and link

#5088

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Dashboard

Related PR for dashboard:
netbirdio/dashboard#615

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

netbirdio/docs#704

Summary by CodeRabbit

  • New Features

    • Configurable local Multi‑Factor Authentication (TOTP & WebAuthn) with session behavior controls and an option to enable local MFA that is applied to the embedded identity provider.
    • New user-facing pages: MFA verification, WebAuthn redirect entry, logout confirmation, and a redirecting home entry.
  • Chores

    • Updated many third‑party dependencies to newer versions.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds local MFA and session config to the embedded IdP, introduces signer-based token signing, extends Dex YAML schema with sessions and MFA providers (TOTP/WebAuthn), injects post-logout URIs and MFA chains into static clients, adds UI templates, and updates module dependencies.

Changes

Cohort / File(s) Summary
Application config
combined/cmd/config.go, management/server/idp/embedded.go
Added EnableLocalMFA to AuthConfig and wired it to EmbeddedIdPConfig.EnableMFA; EmbeddedIdPConfig gains EnableMFA; ToYAMLConfig now sets logout/post-logout URIs and conditionally configures MFA, session settings, env var, and StaticClients[].MFAChain.
Dex YAML surface
idp/dex/config.go
Added sessions and mfa YAML fields plus exported types (Sessions, MFAConfig, MFAAuthenticator, TOTPConfig, WebAuthnConfig) and builders to produce server session config and MFA providers (parsing, validation, and provider construction).
Provider & signing
idp/dex/provider.go
Integrated Dex signer.Signer via new getSigner (configurable token lifetime and key rotation); removed RotateKeysAfter defaulting; ensure signer closed on errors; persist PostLogoutRedirectURIs and MFAChain when updating static clients; Handler() forwards to Dex server.
Templates / UI
idp/dex/web/templates/...
home.html, logout.html, totp_verify.html, webauthn_verify.html
Added four templates: home redirect, logout confirmation (optional back link), TOTP verification form (QR fallback/validation), and WebAuthn verify/redirect page with noscript fallback.
Dependencies / modules
go.mod
Bumped numerous direct and indirect dependencies (cobra, x/crypto, grpc, oidc, jose, jwt, otel and instrumentation, WebAuthn/CBOR/OTP libs, etc.) and adjusted replace directives for circl and dex modules.
Other
idp/dex/provider.go
Minor permission literal change for data-dir, removed prior RotateKeysAfter defaulting logic, and small error-handling cleanup (close storage on signer errors).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser
    participant Dex as "Embedded IdP (Dex)"
    participant Storage
    participant Signer

    User->>Browser: Start login
    Browser->>Dex: Submit primary credentials
    Dex->>Storage: Verify credentials / fetch user record
    Storage-->>Dex: User record (may require MFA)
    Dex->>Browser: Serve MFA challenge (TOTP or WebAuthn)
    alt TOTP flow
        Browser->>Dex: POST TOTP code
        Dex->>Storage: Validate TOTP secret
        Storage-->>Dex: Validation result
    else WebAuthn flow
        Browser->>Dex: Request challenge
        Dex->>Signer: Prepare/sign challenge
        Signer-->>Dex: Challenge payload
        Dex->>Browser: Return challenge
        Browser->>Dex: POST assertion
        Dex->>Storage: Validate assertion/credential
        Storage-->>Dex: Validation result
    end
    Dex->>Signer: Sign ID/access tokens
    Signer-->>Dex: Signed tokens
    Dex->>Browser: Redirect to application with tokens
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"I nibble code and hop with cheer,
Keys and tokens clutched so near,
TOTP beeps and WebAuthn bright,
Sessions warm through day and night,
Hop—secure logins, hold me dear! 🐇"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature: enabling MFA for local users in the management system, which is the core objective of the changeset.
Description check ✅ Passed The PR description follows the required template with all key sections completed: change description, issue reference, checklist with feature enhancement marked, and documentation link provided.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/local-user-totp

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jnfrati jnfrati marked this pull request as ready for review April 8, 2026 14:55
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
idp/dex/web/templates/totp_verify.html (1)

23-35: Consider using a stricter pattern for TOTP input validation.

The current pattern="[0-9]*" allows empty strings and any number of digits. Since TOTP codes are exactly 6 digits, a stricter pattern would provide better client-side validation feedback.

🔧 Suggested pattern improvement
             <input
                 type="text"
                 id="totp"
                 name="totp"
                 class="nb-input"
                 inputmode="numeric"
-                pattern="[0-9]*"
+                pattern="[0-9]{6}"
                 maxlength="6"
                 autocomplete="one-time-code"
                 placeholder="000000"
                 autofocus
                 required
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@idp/dex/web/templates/totp_verify.html` around lines 23 - 35, The TOTP input
(id="totp", name="totp", class="nb-input") uses pattern="[0-9]*" which allows
empty or variable-length digits; update the pattern to enforce exactly 6 digits
(e.g. use a regex like ^[0-9]{6}$) while keeping inputmode="numeric",
maxlength="6", and required so browsers provide proper client-side validation
for a 6-digit TOTP code.
idp/dex/provider.go (1)

105-112: Note: Different default key rotation periods between NewProvider and NewProviderFromYAML.

NewProvider uses KeysRotationPeriod: "6h" while getSigner (used by NewProviderFromYAML) defaults to "720h" (30 days). This may be intentional for different use cases (standalone vs embedded mode), but it's worth documenting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@idp/dex/provider.go` around lines 105 - 112, NewProvider sets
signer.LocalConfig.KeysRotationPeriod to "6h" while getSigner (used by
NewProviderFromYAML) defaults to "720h", causing inconsistent key rotation
behavior; update either NewProvider or getSigner to use the same default value
or add a clear comment/docstring near NewProvider, NewProviderFromYAML and
getSigner explaining the intentional difference and rationale. Locate the
LocalConfig usage in NewProvider (localSignerConfig) and the getSigner function,
then either unify the KeysRotationPeriod string in both places or add explicit
documentation in both NewProvider and getSigner describing why one uses "6h" and
the other "720h" so maintainers understand the divergence.
management/server/idp/embedded.go (1)

222-228: Clarify variable naming for remember-me default.

The variable rememberMeEnabled := false is then assigned to RememberMeCheckedByDefault, which can be confusing. Consider naming it to reflect its actual purpose.

♻️ Clearer variable naming
-	rememberMeEnabled := false
+	rememberMeCheckedByDefault := false

 	cfg.Sessions = &dex.Sessions{
 		CookieName:                 "netbird-session",
 		AbsoluteLifetime:           "24h",
 		ValidIfNotUsedFor:          "1h",
-		RememberMeCheckedByDefault: &rememberMeEnabled,
+		RememberMeCheckedByDefault: &rememberMeCheckedByDefault,
 		SSOSharedWithDefault:       "",
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/idp/embedded.go` around lines 222 - 228, The local variable
rememberMeEnabled used to set cfg.Sessions.RememberMeCheckedByDefault is
misleading; rename it to reflect its purpose (e.g., rememberMeCheckedByDefault
or rememberMeDefaultChecked) and update its declaration and the assignment to
cfg.Sessions.RememberMeCheckedByDefault in embedded.go so the name clearly
indicates it's the default checked state for the "remember me" checkbox when
constructing cfg.Sessions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@idp/dex/config.go`:
- Around line 57-61: The MFA field in the struct is missing a yaml tag which
causes inconsistency with Sessions; update the MFA field declaration (MFA
MFAConfig `json:"mfa"`) to include a matching yaml tag (e.g., `yaml:"mfa"`) so
it reads similarly to Sessions and will be populated when loading YAML configs.
- Around line 561-577: In buildSessionsConfig, avoid dereferencing
sessions.RememberMeCheckedByDefault without a nil check and stop silently
discarding parseDuration errors: first verify
sessions.RememberMeCheckedByDefault != nil before using it (e.g., use a safe
default when nil), and capture the errors returned by
parseDuration(sessions.AbsoluteLifetime) and
parseDuration(sessions.ValidIfNotUsedFor) instead of ignoring them—handle them
by returning an error up the call chain or logging and applying sensible default
durations; update buildSessionsConfig and any callers to propagate/handle the
error if you choose to return one.

In `@idp/dex/web/templates/webauthn_verify.html`:
- Around line 33-37: The apiParams() helper and related fetch calls currently
append req/hmac/authenticator to the URL query string; change them to send those
values in the POST request body instead (e.g., build a URLSearchParams or JSON
payload from reqID, hmacVal, authenticator and pass it as fetch body with
appropriate Content-Type) and call the endpoints without those query params;
update all similar places (register/finish, login/begin, login/finish and the
occurrences around lines referenced) and ensure server handlers expect the
values in the POST body rather than the query string.
- Around line 33-37: The apiParams function (and the surrounding initialization
that reads reqID, hmacVal, authenticator via URLSearchParams.get) must fail fast
when any of reqID, hmacVal, or authenticator are null/undefined: add a preflight
check after extracting those values (before any network calls or calling
apiParams) that tests for null/empty and, if present, stops further processing,
displays a deterministic user-facing error message (e.g., "Missing required URL
parameters") and prevents calling apiParams or initiating requests; update any
code paths that assume apiParams returns a valid string to handle the early
exit. Ensure you reference the variables reqID, hmacVal, authenticator and the
apiParams function to locate where to add the check.
- Line 25: The injected template value for .Mode is inserted as a bare
identifier causing a runtime ReferenceError; fix the assignment to the mode
variable by injecting .Mode as a quoted string (i.e., change the const mode = {{
.Mode }} assignment so the template emits a JavaScript string literal for .Mode)
and ensure the template escaping/quoting used by the HTML template engine is
applied so values like "register" or "login" are safely emitted; update the line
that defines mode to use the quoted .Mode injection.

In `@management/server/idp/embedded.go`:
- Around line 231-232: The comment points out a typo and a problematic global
side effect: fix the spelling in the comment ("otherwsise" → "otherwise") and
remove the call to os.Setenv("DEX_SESSIONS_ENABLED", "true") from the
ToYAMLConfig/config conversion helper; instead document the requirement in the
helper's comment (referencing DEX_SESSIONS_ENABLED) and ensure the environment
variable is set explicitly at application startup (e.g., main or server
initialization) so ToYAMLConfig remains pure and safe for repeated calls or
tests.

---

Nitpick comments:
In `@idp/dex/provider.go`:
- Around line 105-112: NewProvider sets signer.LocalConfig.KeysRotationPeriod to
"6h" while getSigner (used by NewProviderFromYAML) defaults to "720h", causing
inconsistent key rotation behavior; update either NewProvider or getSigner to
use the same default value or add a clear comment/docstring near NewProvider,
NewProviderFromYAML and getSigner explaining the intentional difference and
rationale. Locate the LocalConfig usage in NewProvider (localSignerConfig) and
the getSigner function, then either unify the KeysRotationPeriod string in both
places or add explicit documentation in both NewProvider and getSigner
describing why one uses "6h" and the other "720h" so maintainers understand the
divergence.

In `@idp/dex/web/templates/totp_verify.html`:
- Around line 23-35: The TOTP input (id="totp", name="totp", class="nb-input")
uses pattern="[0-9]*" which allows empty or variable-length digits; update the
pattern to enforce exactly 6 digits (e.g. use a regex like ^[0-9]{6}$) while
keeping inputmode="numeric", maxlength="6", and required so browsers provide
proper client-side validation for a 6-digit TOTP code.

In `@management/server/idp/embedded.go`:
- Around line 222-228: The local variable rememberMeEnabled used to set
cfg.Sessions.RememberMeCheckedByDefault is misleading; rename it to reflect its
purpose (e.g., rememberMeCheckedByDefault or rememberMeDefaultChecked) and
update its declaration and the assignment to
cfg.Sessions.RememberMeCheckedByDefault in embedded.go so the name clearly
indicates it's the default checked state for the "remember me" checkbox when
constructing cfg.Sessions.
🪄 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: caaf5b60-4cb8-4b1f-8c1c-32592e9d1939

📥 Commits

Reviewing files that changed from the base of the PR and between decb5dd and 9da0a6d.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (10)
  • combined/cmd/config.go
  • go.mod
  • idp/dex/config.go
  • idp/dex/provider.go
  • idp/dex/web/templates/home.html
  • idp/dex/web/templates/logout.html
  • idp/dex/web/templates/totp_verify.html
  • idp/dex/web/templates/webauthn_verify.html
  • management/server/idp/embedded.go
  • netbird-server

Comment thread idp/dex/config.go Outdated
Comment thread idp/dex/config.go
Comment thread idp/dex/web/templates/webauthn_verify.html Outdated
Comment thread idp/dex/web/templates/webauthn_verify.html Outdated
Comment thread management/server/idp/embedded.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@idp/dex/config.go`:
- Around line 516-528: The WebAuthnConfig's UserVerification and
AuthenticatorAttachment fields are defined but unused; either remove them from
the WebAuthnConfig struct and its docs (and any references) or, if you plan to
support upstream changes, wire them into the provider by updating
buildWebAuthnConfig to pass those values into server.NewWebAuthnProvider and
update the NewWebAuthnProvider signature to accept UserVerification and
AuthenticatorAttachment parameters; locate the struct WebAuthnConfig, the fields
UserVerification and AuthenticatorAttachment, the function buildWebAuthnConfig,
and the call to server.NewWebAuthnProvider to make the corresponding removal or
signature/argument changes and update any callers or tests accordingly.

In `@management/server/idp/embedded.go`:
- Around line 204-238: The TOTP issuer string in configureMFA
(totpConfig.Issuer) uses "Netbird" but other places use "NetBird"; update the
Issuer field in the totpConfig inside configureMFA to "NetBird" to match
Frontend.Issuer and any other usages so authenticator apps display a consistent
name (also scan the file for other "Netbird" occurrences and normalize their
casing to "NetBird" if present).
🪄 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: 8969eafb-5cb4-4030-b37a-fb4bf43c28f8

📥 Commits

Reviewing files that changed from the base of the PR and between 9da0a6d and 1ca2a84.

📒 Files selected for processing (3)
  • idp/dex/config.go
  • idp/dex/web/templates/webauthn_verify.html
  • management/server/idp/embedded.go

Comment thread idp/dex/config.go
Comment thread management/server/idp/embedded.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
management/server/idp/embedded.go (2)

205-207: ⚠️ Potential issue | 🟡 Minor

Keep the TOTP issuer name consistent with the rest of the UI.

Frontend.Issuer is "NetBird", but Line 206 still uses "Netbird". That inconsistency is visible in authenticator apps during TOTP enrollment.

🔧 Proposed fix
 	totpConfig := dex.TOTPConfig{
-		Issuer: "Netbird",
+		Issuer: "NetBird",
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/idp/embedded.go` around lines 205 - 207, TOTP issuer string
is inconsistent with the frontend; in the dex.TOTPConfig initialization
(totpConfig variable in embedded.go) change Issuer from "Netbird" to match
Frontend.Issuer "NetBird" so authenticator apps show the same name during TOTP
enrollment.

231-232: ⚠️ Potential issue | 🟡 Minor

Move DEX_SESSIONS_ENABLED out of the config builder.

Setting a process-wide env var here makes ToYAMLConfig() impure: one MFA-enabled call leaves sessions enabled for later callers/tests, even when EnableMFA is false. This should be set once during server/Dex startup, not from a YAML conversion helper.

#!/bin/bash
set -euo pipefail

printf 'DEX_SESSIONS_ENABLED references:\n'
rg -n --type=go 'DEX_SESSIONS_ENABLED'

printf '\nToYAMLConfig call sites:\n'
rg -n --type=go '\.ToYAMLConfig\s*\('

printf '\nEmbedded IdP initialization paths:\n'
rg -n --type=go 'NewEmbeddedIdPManager\s*\('
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/idp/embedded.go` around lines 231 - 232, The call to
os.Setenv("DEX_SESSIONS_ENABLED","true") inside ToYAMLConfig() makes the helper
impure; remove that side-effect from ToYAMLConfig() and any other YAML
conversion helpers so they only produce config text. Instead set the
DEX_SESSIONS_ENABLED env var once during server/Dex startup or during Embedded
IdP initialization (e.g. in NewEmbeddedIdPManager or the main server bootstrap
path that constructs the embedded IdP), so tests and later callers aren’t
affected by prior ToYAMLConfig() calls; update tests that relied on the env
being set to explicitly set it in their setup if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@management/server/idp/embedded.go`:
- Around line 160-173: PostLogoutRedirectURIs currently only contains c.Issuer
and logoutURL which breaks post-logout redirects for dashboards/CLIs hosted on
different origins; update the client construction for both dashboard and
staticClientCLI to populate PostLogoutRedirectURIs from each client's configured
redirect URIs (use DashboardRedirectURIs for the dashboard client and
cliRedirectURIs for staticClientCLI) and, where necessary, add derived base
origins of those redirect URIs (strip path/query to form origin) so Dex will
accept post_logout_redirect_uri for each configured app origin instead of
hard-coding c.Issuer/logoutURL.

---

Duplicate comments:
In `@management/server/idp/embedded.go`:
- Around line 205-207: TOTP issuer string is inconsistent with the frontend; in
the dex.TOTPConfig initialization (totpConfig variable in embedded.go) change
Issuer from "Netbird" to match Frontend.Issuer "NetBird" so authenticator apps
show the same name during TOTP enrollment.
- Around line 231-232: The call to os.Setenv("DEX_SESSIONS_ENABLED","true")
inside ToYAMLConfig() makes the helper impure; remove that side-effect from
ToYAMLConfig() and any other YAML conversion helpers so they only produce config
text. Instead set the DEX_SESSIONS_ENABLED env var once during server/Dex
startup or during Embedded IdP initialization (e.g. in NewEmbeddedIdPManager or
the main server bootstrap path that constructs the embedded IdP), so tests and
later callers aren’t affected by prior ToYAMLConfig() calls; update tests that
relied on the env being set to explicitly set it in their setup if needed.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0f3776a2-2c4b-4640-bf07-1d492c0371eb

📥 Commits

Reviewing files that changed from the base of the PR and between 1ca2a84 and 6d4dcca.

📒 Files selected for processing (2)
  • idp/dex/provider.go
  • management/server/idp/embedded.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • idp/dex/provider.go

Comment thread management/server/idp/embedded.go Outdated
Comment on lines +160 to +173
PostLogoutRedirectURIs: []string{
c.Issuer,
logoutURL,
},
},
{
ID: staticClientCLI,
Name: "NetBird CLI",
Public: true,
RedirectURIs: cliRedirectURIs,
PostLogoutRedirectURIs: []string{
c.Issuer,
logoutURL,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve configured app URLs in the post-logout allowlist.

These PostLogoutRedirectURIs ignore DashboardRedirectURIs/CLIRedirectURIs and only allow the issuer root. In deployments where the dashboard or CLI callback lives on a different origin, Dex will reject post_logout_redirect_uri back to that client and logout will break. Build the post-logout list from each client’s configured redirect URLs (or their derived app base URLs) instead of hard-coding c.Issuer/logoutURL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/idp/embedded.go` around lines 160 - 173,
PostLogoutRedirectURIs currently only contains c.Issuer and logoutURL which
breaks post-logout redirects for dashboards/CLIs hosted on different origins;
update the client construction for both dashboard and staticClientCLI to
populate PostLogoutRedirectURIs from each client's configured redirect URIs (use
DashboardRedirectURIs for the dashboard client and cliRedirectURIs for
staticClientCLI) and, where necessary, add derived base origins of those
redirect URIs (strip path/query to form origin) so Dex will accept
post_logout_redirect_uri for each configured app origin instead of hard-coding
c.Issuer/logoutURL.

jnfrati added 10 commits April 15, 2026 11:43
Signed-off-by: jnfrati <nicofrati@gmail.com>
Signed-off-by: jnfrati <nicofrati@gmail.com>
Signed-off-by: jnfrati <nicofrati@gmail.com>
Signed-off-by: jnfrati <nicofrati@gmail.com>
Signed-off-by: jnfrati <nicofrati@gmail.com>
Signed-off-by: jnfrati <nicofrati@gmail.com>
@jnfrati jnfrati force-pushed the feat/local-user-totp branch from 6d4dcca to 4501f0f Compare April 15, 2026 09:44
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
idp/dex/config.go (1)

571-572: ⚠️ Potential issue | 🟡 Minor

Duration parsing errors are silently discarded.

Lines 571-572 ignore parsing errors for AbsoluteLifetime and ValidIfNotUsedFor. Invalid duration strings will result in zero-value durations (0s), potentially causing unexpected session behavior (e.g., sessions expiring immediately).

🛠️ Proposed fix with sensible defaults
-	absoluteLifetime, _ := parseDuration(sessions.AbsoluteLifetime)
-	validIfNotUsedFor, _ := parseDuration(sessions.ValidIfNotUsedFor)
+	absoluteLifetime, err := parseDuration(sessions.AbsoluteLifetime)
+	if err != nil && sessions.AbsoluteLifetime != "" {
+		// Log warning and use sensible default
+		absoluteLifetime = 24 * time.Hour
+	}
+	validIfNotUsedFor, err := parseDuration(sessions.ValidIfNotUsedFor)
+	if err != nil && sessions.ValidIfNotUsedFor != "" {
+		// Log warning and use sensible default
+		validIfNotUsedFor = time.Hour
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@idp/dex/config.go` around lines 571 - 572, The code silently discards
parseDuration errors for sessions.AbsoluteLifetime and
sessions.ValidIfNotUsedFor; update the handling in the block that calls
parseDuration so you check the returned error for both calls
(parseDuration(sessions.AbsoluteLifetime) and
parseDuration(sessions.ValidIfNotUsedFor)), and on error either return/wrap the
error up to the caller or set a sensible default timeout (e.g., preserve current
behavior with a non-zero default or a configured fallback) while logging the
parse failure; ensure you update the variables absoluteLifetime and
validIfNotUsedFor only after successful parsing or assignment of the fallback so
invalid strings cannot become zero-valued durations.
management/server/idp/embedded.go (1)

204-207: ⚠️ Potential issue | 🟡 Minor

Inconsistent casing in TOTP issuer name.

Line 206 uses "Netbird" while line 148 uses "NetBird" for Frontend.Issuer. This inconsistency will appear in users' authenticator apps when they set up TOTP.

🔧 Proposed fix
 func configureMFA(cfg *dex.YAMLConfig) error {
 	totpConfig := dex.TOTPConfig{
-		Issuer: "Netbird",
+		Issuer: "NetBird",
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/idp/embedded.go` around lines 204 - 207, The TOTP issuer
string is cased differently ("Netbird") than the frontend issuer ("NetBird");
update configureMFA so TOTPConfig.Issuer uses the same value as the frontend to
ensure consistent casing — e.g., set totpConfig.Issuer = cfg.Frontend.Issuer (or
reference the same constant/variable used for Frontend.Issuer) in the
configureMFA function where dex.TOTPConfig is constructed.
🧹 Nitpick comments (1)
idp/dex/config.go (1)

566-569: Input parameter mutation is unexpected.

buildSessionsConfig mutates the input sessions parameter by setting RememberMeCheckedByDefault (line 568). This side effect can cause unexpected behavior if the caller reuses the Sessions object.

♻️ Avoid mutating input
 func buildSessionsConfig(sessions *Sessions) *server.SessionConfig {
 	if sessions == nil {
 		return nil
 	}

-	if sessions.RememberMeCheckedByDefault == nil {
-		defaultRememberMeCheckedByDefault := false
-		sessions.RememberMeCheckedByDefault = &defaultRememberMeCheckedByDefault
-	}
+	rememberMeDefault := false
+	if sessions.RememberMeCheckedByDefault != nil {
+		rememberMeDefault = *sessions.RememberMeCheckedByDefault
+	}

 	absoluteLifetime, _ := parseDuration(sessions.AbsoluteLifetime)
 	validIfNotUsedFor, _ := parseDuration(sessions.ValidIfNotUsedFor)

 	return &server.SessionConfig{
 		CookieEncryptionKey:        []byte(sessions.CookieEncryptionKey),
 		CookieName:                 sessions.CookieName,
 		AbsoluteLifetime:           absoluteLifetime,
 		ValidIfNotUsedFor:          validIfNotUsedFor,
-		RememberMeCheckedByDefault: *sessions.RememberMeCheckedByDefault,
+		RememberMeCheckedByDefault: rememberMeDefault,
 		SSOSharedWithDefault:       sessions.SSOSharedWithDefault,
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@idp/dex/config.go` around lines 566 - 569, buildSessionsConfig currently
mutates the input Sessions parameter by setting RememberMeCheckedByDefault on
the passed object; instead make a local copy and modify that copy before
returning (e.g., copy := *sessions; if copy.RememberMeCheckedByDefault == nil {
defaultVal := false; copy.RememberMeCheckedByDefault = &defaultVal } ) or
construct and return a new Sessions value so the original input is not mutated;
update buildSessionsConfig to operate on and return the copied/constructed
Sessions instance rather than altering the input parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@idp/dex/web/templates/webauthn_verify.html`:
- Around line 1-12: The webauthn_verify.html template currently only contains a
redirect (globalThis.location.replace("/")) and mirrors home.html, indicating a
leftover debug placeholder; either delete this orphan template or replace its
body with a clear TODO comment and brief spec of the intended WebAuthn UI (e.g.,
page for credential creation and assertion flows, expected endpoints, and any
client-side WebAuthn JS interactions) so future developers know its
purpose—refer to the template name webauthn_verify.html and the existing
redirect/script block (globalThis.location.replace("/")) when locating and
updating the file, and keep header.html/footer.html includes intact.

---

Duplicate comments:
In `@idp/dex/config.go`:
- Around line 571-572: The code silently discards parseDuration errors for
sessions.AbsoluteLifetime and sessions.ValidIfNotUsedFor; update the handling in
the block that calls parseDuration so you check the returned error for both
calls (parseDuration(sessions.AbsoluteLifetime) and
parseDuration(sessions.ValidIfNotUsedFor)), and on error either return/wrap the
error up to the caller or set a sensible default timeout (e.g., preserve current
behavior with a non-zero default or a configured fallback) while logging the
parse failure; ensure you update the variables absoluteLifetime and
validIfNotUsedFor only after successful parsing or assignment of the fallback so
invalid strings cannot become zero-valued durations.

In `@management/server/idp/embedded.go`:
- Around line 204-207: The TOTP issuer string is cased differently ("Netbird")
than the frontend issuer ("NetBird"); update configureMFA so TOTPConfig.Issuer
uses the same value as the frontend to ensure consistent casing — e.g., set
totpConfig.Issuer = cfg.Frontend.Issuer (or reference the same constant/variable
used for Frontend.Issuer) in the configureMFA function where dex.TOTPConfig is
constructed.

---

Nitpick comments:
In `@idp/dex/config.go`:
- Around line 566-569: buildSessionsConfig currently mutates the input Sessions
parameter by setting RememberMeCheckedByDefault on the passed object; instead
make a local copy and modify that copy before returning (e.g., copy :=
*sessions; if copy.RememberMeCheckedByDefault == nil { defaultVal := false;
copy.RememberMeCheckedByDefault = &defaultVal } ) or construct and return a new
Sessions value so the original input is not mutated; update buildSessionsConfig
to operate on and return the copied/constructed Sessions instance rather than
altering the input parameter.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d643c7f-ff40-4e2f-bcc0-c9d78f9811cb

📥 Commits

Reviewing files that changed from the base of the PR and between 6d4dcca and 4501f0f.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • combined/cmd/config.go
  • go.mod
  • idp/dex/config.go
  • idp/dex/provider.go
  • idp/dex/web/templates/home.html
  • idp/dex/web/templates/logout.html
  • idp/dex/web/templates/totp_verify.html
  • idp/dex/web/templates/webauthn_verify.html
  • management/server/idp/embedded.go
✅ Files skipped from review due to trivial changes (1)
  • combined/cmd/config.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • idp/dex/provider.go
  • go.mod

Comment thread idp/dex/web/templates/webauthn_verify.html
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
idp/dex/config.go (1)

576-593: ⚠️ Potential issue | 🟠 Major

expiry.signingKeys became a silent no-op.

Expiry.SigningKeys is still part of the public config model in this file, but ToServerConfig() no longer copies it into the Dex server config while it still maps the other expiry settings. That makes expiry.signingKeys a silent no-op for existing deployments and weakens signing-key rotation. Please wire it back when populating cfg (for example via cfg.RotateKeysAfter).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@idp/dex/config.go` around lines 576 - 593, The expiry.signingKeys setting in
YAMLConfig is not being propagated in ToServerConfig, making signing key
rotation a silent no-op; update YAMLConfig.ToServerConfig to copy the expiry
signing-key setting (Expiry.SigningKeys) into the server.Config (e.g., set
cfg.RotateKeysAfter or the appropriate rotation field on server.Config) when
building cfg so key rotation behavior is preserved; locate the Expiry struct
usage in ToServerConfig and assign its SigningKeys/rotation value into cfg
before returning.
♻️ Duplicate comments (1)
idp/dex/config.go (1)

562-563: ⚠️ Potential issue | 🟠 Major

Reject invalid session durations instead of silently zeroing them.

These parse errors are still discarded, so a typo in absoluteLifetime or validIfNotUsedFor quietly becomes 0 and changes session behavior without failing startup. Please surface an error for non-empty invalid values here or validate them earlier in Validate().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@idp/dex/config.go` around lines 562 - 563, The current code silently ignores
parseDuration errors for sessions.AbsoluteLifetime and
sessions.ValidIfNotUsedFor causing invalid values to become zero; update the
code in the function that computes absoluteLifetime/validIfNotUsedFor to check
the error returned by parseDuration and return or propagate an error when the
input string is non-empty and parseDuration fails (alternatively perform this
validation in Validate()). Specifically, when calling parseDuration for
sessions.AbsoluteLifetime and sessions.ValidIfNotUsedFor, if err != nil and the
original string is not empty, surface that error (with context mentioning the
field name) instead of discarding it so startup fails on invalid durations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@idp/dex/config.go`:
- Around line 85-88: The MFAAuthenticator.Config field currently defined as
json.RawMessage causes YAML unmarshalling to leave YAML bytes in the field,
which later breaks json.Unmarshal in buildTotpConfig and buildWebAuthnConfig;
change MFAAuthenticator.Config to map[string]interface{} (matching Connector) so
yaml.Unmarshal produces a native map, then where buildTotpConfig and
buildWebAuthnConfig expect JSON bytes, json.Marshal the map before calling
json.Unmarshal (or implement a custom UnmarshalYAML on MFAAuthenticator to
decode directly into the target structs); update references to
MFAAuthenticator.Config in LoadConfig, buildTotpConfig, and buildWebAuthnConfig
to handle the map-to-JSON conversion.

---

Outside diff comments:
In `@idp/dex/config.go`:
- Around line 576-593: The expiry.signingKeys setting in YAMLConfig is not being
propagated in ToServerConfig, making signing key rotation a silent no-op; update
YAMLConfig.ToServerConfig to copy the expiry signing-key setting
(Expiry.SigningKeys) into the server.Config (e.g., set cfg.RotateKeysAfter or
the appropriate rotation field on server.Config) when building cfg so key
rotation behavior is preserved; locate the Expiry struct usage in ToServerConfig
and assign its SigningKeys/rotation value into cfg before returning.

---

Duplicate comments:
In `@idp/dex/config.go`:
- Around line 562-563: The current code silently ignores parseDuration errors
for sessions.AbsoluteLifetime and sessions.ValidIfNotUsedFor causing invalid
values to become zero; update the code in the function that computes
absoluteLifetime/validIfNotUsedFor to check the error returned by parseDuration
and return or propagate an error when the input string is non-empty and
parseDuration fails (alternatively perform this validation in Validate()).
Specifically, when calling parseDuration for sessions.AbsoluteLifetime and
sessions.ValidIfNotUsedFor, if err != nil and the original string is not empty,
surface that error (with context mentioning the field name) instead of
discarding it so startup fails on invalid durations.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db1a14df-eee5-4630-a002-a91e81f43604

📥 Commits

Reviewing files that changed from the base of the PR and between 4501f0f and 4325495.

📒 Files selected for processing (2)
  • idp/dex/config.go
  • management/server/idp/embedded.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • management/server/idp/embedded.go

Comment thread idp/dex/config.go Outdated
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants