Skip to content

Add shared-password auth for protected routes#787

Merged
evanphx merged 10 commits into
mainfrom
mir-886-add-shared-password-auth-for-protected-pages
May 19, 2026
Merged

Add shared-password auth for protected routes#787
evanphx merged 10 commits into
mainfrom
mir-886-add-shared-password-auth-for-protected-pages

Conversation

@evanphx
Copy link
Copy Markdown
Contributor

@evanphx evanphx commented May 7, 2026

Summary

  • Adds a password_provider entity type (parallel to oidc_provider) that protects HTTP routes with a shared password and a browser login form
  • After entering the correct password, the user receives a 24h encrypted session cookie (XChaCha20-Poly1305, reusing the existing SessionManager)
  • CLI commands: auth provider add-password, unified list/show/remove across both provider types, route protect --provider auto-detects type
  • Mutual exclusion enforced: attaching one provider type clears the other, using Replace instead of merge-based Update to correctly remove stale entity refs

Test plan

  • Unit tests for password handler, session roundtrip, login flow, cache invalidation, fail-closed behavior (servers/httpingress/password_auth_test.go)
  • Blackbox E2E test: deploy app → create password provider → protect route → verify login form → wrong password → correct password + cookie → authenticated access → unprotect → verify open access (blackbox/route_password_test.go)
  • Blackbox lifecycle test: create → list → show → update → remove password provider
  • make lint passes (no new issues)
  • Existing httpingress and ingress client tests pass

Closes MIR-886

@evanphx evanphx requested a review from a team as a code owner May 7, 2026 00:08
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

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

This PR implements password-based authentication for ingress route protection as an alternative to OIDC. The change introduces a PasswordProvider entity type that stores a bcrypt-hashed password, extends HttpRoute to reference a generic auth_provider, adds client APIs and CLI commands to manage password providers and attach/detach auth providers to routes, implements password-validated login flows with named encrypted session cookies in the HTTP ingress server, updates OIDC handling to use the generic auth provider, and adds unit and end-to-end tests plus documentation and CLI example updates.


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

Copy link
Copy Markdown

@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

🤖 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 `@api/ingress/client.go`:
- Around line 413-425: In replaceRoute, avoid clobbering concurrent updates by
not re-encoding the stale route parameter; instead fetch the latest entity (gr
:= c.eac.Get...), decode it to an HttpRoute, apply only the intended auth-field
mutation(s) from the incoming route onto that freshly fetched route, re-encode
that merged route (use route.Encode on the merged object) and call
c.eac.Replace(ctx, attrs, gr.Entity().Revision()) so Replace uses the current
revision; alternatively, if you prefer optimistic concurrency, carry the
caller's original revision through to Replace (pass the original revision
instead of gr.Entity().Revision()) so Replace fails on concurrent
modification—implement one of these two approaches in replaceRoute, referencing
replaceRoute, c.eac.Get, gr.Entity().Revision(), route.Encode and c.eac.Replace.

In `@cli/commands/auth_provider.go`:
- Around line 189-190: The lookup by name is ambiguous because the code (e.g.,
calls to ic.GetOIDCProvider and ic.GetPasswordProvider in auth_provider.go)
assumes names are globally unique across OIDC and password kinds; fix by adding
an explicit provider kind discriminator or enforcing cross-kind uniqueness at
creation: either (A) add/require a --kind/Type field on commands that resolve
providers (update show/remove/route protect and parsing of opts.Name to use a
new lookup like ic.GetProviderByNameAndKind or call
ic.GetOIDCProvider/ic.GetPasswordProvider based on the discriminator), or (B)
when adding providers, validate uniqueness across both kinds so existing
name-based lookups remain safe (update the add commands' duplicate checks). Also
update all affected call sites referenced (lines ~189, 226-254, 277-279,
311-349) to use the chosen approach so password providers are addressable when
an OIDC provider has the same name.

In `@cli/commands/route_protect.go`:
- Around line 162-164: The current ctx.Printf warning about opts.ClaimHeader
prints to stdout (ctx.Printf) which breaks JSON output; change this so the
warning goes to stderr or becomes a validation error: replace the
ctx.Printf("Warning: --claim-header is ignored for password providers\n") call
in the route_protect command handling (the opts.ClaimHeader check) with either a
stderr print (e.g., use the CLI context's stderr-printing helper such as
ctx.Errorf/ctx.PrintErrf or write to os.Stderr) or return an error/validation
failure before emitting the JSON payload so consumers of the JSON output are not
polluted.

In `@servers/httpingress/password_auth.go`:
- Around line 83-92: The returnPath is user-controlled and is currently echoed
into HTML and used for post-login redirects; sanitize and validate it before
use. In serveLoginForm (and the POST login/redirect logic referenced around
lines 95-98 and 107-128), normalize the incoming returnPath with path.Clean,
ensure it is an app-local path that starts with "/" and does not contain a
scheme/host (reject or strip values containing "://" or leading "//"), then
HTML-escape the final value (e.g., html.EscapeString) before inserting into
loginFormHTML; for redirects use the validated local path only (do not redirect
to external URLs). Ensure the same validation/escaping is applied wherever the
return target is parsed and used after login.
- Around line 147-156: The code currently falls back to a cached handler on
s.eac.Get(ctx, string(route.PasswordProvider)) errors, allowing stale
credentials to remain valid; change this to fail-closed by removing the
fallback: when Get returns an error, acquire s.passwordMu (write lock), delete
any entry for passwordHandlers[key] to invalidate the cache, release the lock,
log the error (include route.Host and the error) and return nil,
fmt.Errorf("failed to get password provider: %w", err) instead of returning the
cached handler; update the s.passwordMu usage to use Lock/Unlock when deleting
and remove the RLock/RUnlock fallback path that returns h.
- Around line 202-206: When getOrCreatePasswordHandler returns an error, the
current code logs and calls next(w,r) which bypasses protection; change the
error path in the request handling (the block where handler, err :=
s.getOrCreatePasswordHandler(...)) to stop forwarding the request and instead
return an error response (e.g., write a 5xx or a denial HTTP response) after
logging. Specifically, in the handler creation error branch (referencing
getOrCreatePasswordHandler, the handler variable, s.Log.Error and next), remove
the call to next and write an appropriate http error status/body (e.g.,
http.StatusServiceUnavailable or http.StatusInternalServerError) then return so
the protected app is not served unauthenticated.
🪄 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: 485dd9be-e0c3-4415-a46d-72cfb74f834c

📥 Commits

Reviewing files that changed from the base of the PR and between af4f87c and 48fbb2f.

📒 Files selected for processing (17)
  • api/ingress/client.go
  • api/ingress/ingress_v1alpha/schema.gen.go
  • api/ingress/schema.yml
  • blackbox/harness/miren.go
  • blackbox/route_password_test.go
  • cli/commands/auth_provider.go
  • cli/commands/auth_provider_password.go
  • cli/commands/commands.go
  • cli/commands/route_protect.go
  • cli/commands/route_show.go
  • cli/commands/route_unprotect.go
  • pkg/labs/features.yaml
  • pkg/labs/labs.gen.go
  • pkg/oidc/session.go
  • servers/httpingress/httpingress.go
  • servers/httpingress/password_auth.go
  • servers/httpingress/password_auth_test.go

Comment thread api/ingress/client.go Outdated
Comment thread cli/commands/auth_provider.go
Comment thread cli/commands/route_protect.go
Comment thread servers/httpingress/password_auth.go Outdated
Comment thread servers/httpingress/password_auth.go Outdated
Comment thread servers/httpingress/password_auth.go Outdated
@evanphx evanphx changed the base branch from main to mir-1079-remove-globalrouter-labs-flag May 7, 2026 16:05
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
cli/commands/auth_provider_password.go (1)

14-77: 💤 Low value

Consider adding --format json support for consistency.

Unlike other auth provider commands (list, show), this command only outputs human-readable format. For IaC/scripting scenarios, JSON output would be useful.

💡 Suggested addition
 func AuthProviderAddPassword(ctx *Context, opts struct {
 	Name     string `position:"0" usage:"Name for this password provider" required:"true"`
 	Password string `long:"password" description:"Password to protect routes with" required:"true"`
 	Update   bool   `long:"update" description:"Overwrite an existing provider with the same name (rotates password)"`
+	FormatOptions
 	ConfigCentric
 }) error {

Then add JSON output handling similar to other commands.

🤖 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 `@cli/commands/auth_provider_password.go` around lines 14 - 77,
AuthProviderAddPassword currently only prints a human-readable NamedValueList;
add a --format option (e.g., Format string `long:"format" choice:"human"
choice:"json" default:"human"`) to the opts struct and branch on opts.Format
after creating the provider: when format=="json" marshal the created provider
(or a small result object with Name and Type) to JSON and write via ctx.Printf,
otherwise keep the existing ui.NewNamedValueList rendering; ensure the JSON
branch returns proper errors on marshal failure and that output is identical to
other auth provider commands (use the same JSON shape those commands produce).
🤖 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.

Nitpick comments:
In `@cli/commands/auth_provider_password.go`:
- Around line 14-77: AuthProviderAddPassword currently only prints a
human-readable NamedValueList; add a --format option (e.g., Format string
`long:"format" choice:"human" choice:"json" default:"human"`) to the opts struct
and branch on opts.Format after creating the provider: when format=="json"
marshal the created provider (or a small result object with Name and Type) to
JSON and write via ctx.Printf, otherwise keep the existing ui.NewNamedValueList
rendering; ensure the JSON branch returns proper errors on marshal failure and
that output is identical to other auth provider commands (use the same JSON
shape those commands produce).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e37e8aed-eb40-4f39-8bcf-978adbcb9f23

📥 Commits

Reviewing files that changed from the base of the PR and between dfb2bf1 and 8a1878f.

📒 Files selected for processing (6)
  • api/ingress/client.go
  • cli/commands/auth_provider.go
  • cli/commands/auth_provider_password.go
  • cli/commands/route_protect.go
  • servers/httpingress/password_auth.go
  • servers/httpingress/password_auth_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • servers/httpingress/password_auth.go

Copy link
Copy Markdown
Contributor

@phinze phinze left a comment

Choose a reason for hiding this comment

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

Claude and Paul here. We did a paired read across this PR and 786 (the WAF one), with notes from yesterday's product standup where the composable-middleware direction came up. That conversation is what's framing how we're thinking about shape.

What we want to put on the table is some shape asymmetry that's showing up between the three protections. After both PRs land, OIDC and password sit on the route as entity-backed refs (with mutual exclusion enforced manually via Replace), and WAF sits as a plain int. Each of these really wants the same shape: a named, reusable entity the route refs.

Here's the cleanup we're picturing on the auth side. Collapse oidc_provider and password_provider into one auth_provider kind with a type: oidc | password discriminator. The route gets a single auth_provider ref. Mutual exclusion is automatic (one slot, can't double-attach). The Replace-vs-Update dance disappears. Middleware composition becomes one authMiddleware that dispatches on type. Type-specific nullable fields would normally be a smell, but the sparse-attribute entity model handles it fine; you just don't set what doesn't apply.

We're expecting the internal representation might trend toward generic middleware later, but for now the focus is just getting the UX and config shape clean and extensible. The schema reshape sets that up without committing us to anything bigger yet.

CodeRabbit's pass caught real items including an XSS + open redirect on returnPath and a fail-open path in the password middleware (same fail-open shape exists in the OIDC middleware, parallel fix worth doing). Agreed on all of them. Those need fixing regardless of which way the architectural conversation goes.

Marking REQUEST_CHANGES on the auth_provider consolidation specifically. Once that pivot lands we'll come back through for the rest of the review properly.

@evanphx
Copy link
Copy Markdown
Contributor Author

evanphx commented May 8, 2026

@phinze I'm glad you pointed this out, as I worked through the problem I had the same instinct but opt'd to push through. I'll go back and rework it to have a single auth_provider ref.

@evanphx evanphx force-pushed the mir-886-add-shared-password-auth-for-protected-pages branch from 8a1878f to 2fa189c Compare May 8, 2026 23:26
@evanphx evanphx changed the base branch from mir-1079-remove-globalrouter-labs-flag to mir-1081-remove-routeoidc-labs-flag May 8, 2026 23:26
Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
cli/commands/route_unprotect.go (1)

54-63: 💤 Low value

Method naming is misleading but behavior is correct.

The condition correctly checks both provider fields. However, DetachOIDCProviderFromRoute clearing both OIDC and password provider fields makes its name misleading—it actually acts as a "clear all protection" operation. Consider renaming to ClearRouteProtection or DetachAllProvidersFromRoute for clarity.

🤖 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 `@cli/commands/route_unprotect.go` around lines 54 - 63, The method
DetachOIDCProviderFromRoute is misnamed because it clears both OIDC and password
providers; rename it to a clear-all name (e.g., ClearRouteProtection or
DetachAllProvidersFromRoute) and update this call site
(ic.DetachOIDCProviderFromRoute(ctx, route)) to the new name; also rename the
implementation and update all other references (interfaces, callers, tests and
docs) to the new identifier so the name accurately reflects that it clears all
protection fields on the route.
🤖 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 `@blackbox/route_password_test.go`:
- Around line 162-166: The status parsing currently ignores fmt.Sscanf's return
values so a failed parse leaves code==0; update the parsing in the helper that
builds statusStr/status code (and mirror the same change in httpGetWithCookie)
to check Sscanf's (n, err) results and handle failures explicitly: if err != nil
or n != 1, return an error (or call t.Fatalf from the test helper) instead of
silently using 0; alternatively replace fmt.Sscanf(statusStr, "%d", &code) with
strconv.Atoi(statusStr) and handle the error by failing/returning so malformed
status lines don't masquerade as HTTP 0.

---

Nitpick comments:
In `@cli/commands/route_unprotect.go`:
- Around line 54-63: The method DetachOIDCProviderFromRoute is misnamed because
it clears both OIDC and password providers; rename it to a clear-all name (e.g.,
ClearRouteProtection or DetachAllProvidersFromRoute) and update this call site
(ic.DetachOIDCProviderFromRoute(ctx, route)) to the new name; also rename the
implementation and update all other references (interfaces, callers, tests and
docs) to the new identifier so the name accurately reflects that it clears all
protection fields on the route.
🪄 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: e2483ed6-6338-4241-95c4-4d99c12061e2

📥 Commits

Reviewing files that changed from the base of the PR and between 8a1878f and 2fa189c.

📒 Files selected for processing (20)
  • api/ingress/client.go
  • api/ingress/ingress_v1alpha/schema.gen.go
  • api/ingress/schema.yml
  • blackbox/harness/miren.go
  • blackbox/route_password_test.go
  • cli/commands/auth_provider.go
  • cli/commands/auth_provider_password.go
  • cli/commands/commands.go
  • cli/commands/route_protect.go
  • cli/commands/route_show.go
  • cli/commands/route_unprotect.go
  • docs/command-sidebar.json
  • docs/docs/command/auth-provider-add-password.md
  • docs/docs/command/auth-provider.md
  • docs/docs/command/route-protect.md
  • docs/docs/commands.md
  • pkg/oidc/session.go
  • servers/httpingress/httpingress.go
  • servers/httpingress/password_auth.go
  • servers/httpingress/password_auth_test.go
✅ Files skipped from review due to trivial changes (6)
  • docs/docs/command/route-protect.md
  • docs/docs/command/auth-provider-add-password.md
  • docs/command-sidebar.json
  • docs/docs/commands.md
  • docs/docs/command/auth-provider.md
  • api/ingress/ingress_v1alpha/schema.gen.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • cli/commands/route_protect.go
  • servers/httpingress/httpingress.go
  • api/ingress/schema.yml
  • cli/commands/route_show.go
  • servers/httpingress/password_auth_test.go
  • cli/commands/auth_provider.go
  • servers/httpingress/password_auth.go
  • pkg/oidc/session.go

Comment on lines +162 to +166
statusStr := lines[len(lines)-1]
var code int
fmt.Sscanf(statusStr, "%d", &code)
body := strings.Join(lines[:len(lines)-1], "\n")
return code, body
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Check fmt.Sscanf return value for robustness.

fmt.Sscanf returns the number of items successfully scanned. If parsing fails, code remains 0, which could mask real failures as "HTTP 0" rather than failing the test explicitly.

🛠️ Proposed fix
 	statusStr := lines[len(lines)-1]
 	var code int
-	fmt.Sscanf(statusStr, "%d", &code)
+	if _, err := fmt.Sscanf(statusStr, "%d", &code); err != nil {
+		t.Fatalf("failed to parse HTTP status code from %q: %v", statusStr, err)
+	}
 	body := strings.Join(lines[:len(lines)-1], "\n")
 	return code, body

Apply the same fix to httpGetWithCookie at line 223.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
statusStr := lines[len(lines)-1]
var code int
fmt.Sscanf(statusStr, "%d", &code)
body := strings.Join(lines[:len(lines)-1], "\n")
return code, body
statusStr := lines[len(lines)-1]
var code int
if _, err := fmt.Sscanf(statusStr, "%d", &code); err != nil {
t.Fatalf("failed to parse HTTP status code from %q: %v", statusStr, err)
}
body := strings.Join(lines[:len(lines)-1], "\n")
return code, body
🤖 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 `@blackbox/route_password_test.go` around lines 162 - 166, The status parsing
currently ignores fmt.Sscanf's return values so a failed parse leaves code==0;
update the parsing in the helper that builds statusStr/status code (and mirror
the same change in httpGetWithCookie) to check Sscanf's (n, err) results and
handle failures explicitly: if err != nil or n != 1, return an error (or call
t.Fatalf from the test helper) instead of silently using 0; alternatively
replace fmt.Sscanf(statusStr, "%d", &code) with strconv.Atoi(statusStr) and
handle the error by failing/returning so malformed status lines don't masquerade
as HTTP 0.

Copy link
Copy Markdown

@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

🤖 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 `@cli/commands/route_show.go`:
- Around line 64-87: Replace the current blind Decode flow (which calls
ic.GetEntityStore().GetById into an OidcProvider and falls back to
PasswordProvider) with a GetByIdWithEntity lookup and explicit kind checks: call
ic.GetEntityStore().GetByIdWithEntity(ctx, route.AuthProvider, nil), handle
cond.ErrNotFound exactly as before, then inspect ent.Entity() with
entity.Is(..., ingress_v1alpha.KindOidcProvider) and
ingress_v1alpha.KindPasswordProvider; only then allocate oidcProvider or
pwProvider and call Decode on the correct struct and set protectionType to
"oidc" or "password" respectively. Ensure error wrapping uses the same message
("failed to get auth provider: %w") when non-NotFound errors occur.
🪄 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: 1dca799f-4b2b-42dd-b588-6cd16b876d63

📥 Commits

Reviewing files that changed from the base of the PR and between 2fa189c and f89a104.

📒 Files selected for processing (12)
  • api/ingress/client.go
  • api/ingress/ingress_v1alpha/schema.gen.go
  • api/ingress/schema.yml
  • cli/commands/auth_provider.go
  • cli/commands/route_protect.go
  • cli/commands/route_show.go
  • cli/commands/route_unprotect.go
  • servers/httpingress/httpingress.go
  • servers/httpingress/oidc.go
  • servers/httpingress/oidc_test.go
  • servers/httpingress/password_auth.go
  • servers/httpingress/password_auth_test.go
✅ Files skipped from review due to trivial changes (1)
  • servers/httpingress/oidc_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • api/ingress/schema.yml
  • servers/httpingress/password_auth_test.go
  • cli/commands/auth_provider.go
  • servers/httpingress/password_auth.go

Comment thread cli/commands/route_show.go
@evanphx evanphx requested a review from phinze May 11, 2026 00:19
Copy link
Copy Markdown
Contributor

@phinze phinze left a comment

Choose a reason for hiding this comment

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

Claude and Paul here, follow-up to last round. Took it for a spin in dev too.

Consolidation landed cleanly, sanitizer holds up under real probes, fail-closed change tested. Nice reshape.

One loose thread from round 1: OIDC side still falls back to the cached handler on store errors while the password side now evicts. Worth aligning so the two don't disagree about revocation behavior. Happy to punt to a follow-up ticket if you'd rather keep this PR scoped.

Comment thread servers/httpingress/oidc.go Outdated
Comment thread servers/httpingress/httpingress.go
Comment thread cli/commands/auth_provider_password.go
@evanphx evanphx force-pushed the mir-1081-remove-routeoidc-labs-flag branch from 07ad137 to 2a890dc Compare May 18, 2026 22:43
Base automatically changed from mir-1081-remove-routeoidc-labs-flag to main May 18, 2026 23:04
@evanphx evanphx force-pushed the mir-886-add-shared-password-auth-for-protected-pages branch from 9177dc8 to 56b44af Compare May 19, 2026 00:01
return "/"
}
cleaned := path.Clean("/" + raw)
if !strings.HasPrefix(cleaned, "/") {
}

h.logger.Info("password authentication successful", "host", h.route.Host)
http.Redirect(w, r, returnPath, http.StatusFound)
evanphx added 10 commits May 18, 2026 17:18
Add a password_provider entity type parallel to oidc_provider that
protects routes with a shared password and browser login form. After
entering the correct password, the user gets a 24h encrypted session
cookie.

Key changes:
- New password_provider entity kind with name and bcrypt password_hash
- Password provider CRUD in ingress client with route attachment
- HTML login form served by ingress middleware at well-known paths
- CLI: auth provider add-password, list/show/remove support both types
- route protect auto-detects provider type (OIDC vs password)
- Mutual exclusion: attaching one provider type clears the other
- Use Replace instead of Update when clearing ref fields to avoid
  stale refs from Encode() skipping empty fields
- Blackbox harness: SetEnv support for lab-gated CLI commands
… route replace

- Sanitize user-controlled return path to prevent XSS and open redirects
- Return 503 instead of serving unprotected content when handler creation fails
- Fail closed on provider lookup errors instead of using stale cached credentials
- Refactor replaceRoute into read-modify-write to avoid overwriting concurrent changes
- Enforce cross-kind uniqueness for auth provider names at creation time
- Return error instead of stdout warning for --claim-header in JSON mode
…_provider on HttpRoute

Replace the two separate ref fields on http_route with a single polymorphic
auth_provider ref that can point to either an OIDC or password provider entity.
The provider entity kinds themselves are unchanged.

- Schema: single auth_provider ref replaces oidc_provider + password_provider
- Client: unified AttachAuthProviderToRoute/DetachAuthProviderFromRoute methods
- httpingress: new authMiddleware dispatches to OIDC or password handler based
  on entity kind, all auth check points and tunnel rejection use AuthProvider
- CLI: route protect/show/unprotect and auth provider remove updated
GetById decodes without checking entity kind, so a password provider
was always decoded as an empty OIDC provider. Use GetByIdWithEntity
and check the entity kind before decoding.
Match the password handler behavior: evict the cached handler and
return an error instead of falling back to a stale cached handler.
This ensures consistent revocation behavior after provider rotation.
authMiddleware already fetches the entity to determine its kind.
Pass that entity directly to getOrCreate*Handler and *Middleware
instead of having them re-fetch from the entity store, eliminating
a redundant eac.Get per request and a TOCTOU window.
The --password flag is no longer required. When omitted, the CLI
prompts interactively with masked input. The @file syntax reads
the password from a file. Both avoid exposing the password in
shell history and process listings.
@evanphx evanphx force-pushed the mir-886-add-shared-password-auth-for-protected-pages branch from 56b44af to b4d4f54 Compare May 19, 2026 00:18
@evanphx evanphx merged commit 24bd762 into main May 19, 2026
19 checks passed
@evanphx evanphx deleted the mir-886-add-shared-password-auth-for-protected-pages branch May 19, 2026 17:18
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.

3 participants