oidc: render HTML error pages for browser-facing failures

Add httpUserError() alongside httpError() for browser-facing error
paths. It renders a styled HTML page using the AuthError template
instead of returning plain text. Technical error details stay in
server logs; the HTML page shows actionable messages derived from
the HTTP status code:

  401/403 → "You are not authorized. Please contact your administrator."
  410     → "Your session has expired. Please try again."
  400-499 → "The request could not be processed. Please try again."
  500+    → "Something went wrong. Please try again later."

Convert all httpError calls in oidc.go (OIDC callback, SSH check,
registration confirm) to httpUserError. Machine-facing endpoints
(noise, verify, key, health, debug) are unchanged.

Fixes juanfont/headscale#3182
This commit is contained in:
Kristoffer Dalby 2026-04-13 08:46:00 +00:00
parent c15caff48c
commit 78990491da
5 changed files with 257 additions and 48 deletions

View file

@ -7,6 +7,71 @@ import (
"github.com/stretchr/testify/assert"
)
func TestAuthErrorTemplate(t *testing.T) {
tests := []struct {
name string
result templates.AuthErrorResult
}{
{
name: "bad_request",
result: templates.AuthErrorResult{
Title: "Headscale - Error",
Heading: "Bad Request",
Message: "The request could not be processed. Please try again.",
},
},
{
name: "forbidden",
result: templates.AuthErrorResult{
Title: "Headscale - Error",
Heading: "Forbidden",
Message: "You are not authorized. Please contact your administrator.",
},
},
{
name: "gone_expired",
result: templates.AuthErrorResult{
Title: "Headscale - Error",
Heading: "Gone",
Message: "Your session has expired. Please try again.",
},
},
{
name: "internal_server_error",
result: templates.AuthErrorResult{
Title: "Headscale - Error",
Heading: "Internal Server Error",
Message: "Something went wrong. Please try again later.",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
html := templates.AuthError(tt.result).Render()
// Verify the HTML contains expected structural elements
assert.Contains(t, html, "<!DOCTYPE html>")
assert.Contains(t, html, "<title>"+tt.result.Title+"</title>")
assert.Contains(t, html, tt.result.Heading)
assert.Contains(t, html, tt.result.Message)
// Verify Material for MkDocs design system CSS is present
assert.Contains(t, html, "Material for MkDocs")
assert.Contains(t, html, "Roboto")
assert.Contains(t, html, ".md-typeset")
// Verify SVG elements are present
assert.Contains(t, html, "<svg")
assert.Contains(t, html, "class=\"headscale-logo\"")
assert.Contains(t, html, "id=\"error-icon\"")
// Verify no success checkbox icon
assert.NotContains(t, html, "id=\"checkbox\"")
})
}
}
func TestAuthSuccessTemplate(t *testing.T) {
tests := []struct {
name string