fix(coderd): render HTML error page for OIDC email validation failures (#23059)

## Summary

When the email address returned from an OIDC provider doesn't match the
configured allowed domain list (or isn't verified), users previously saw
raw JSON dumped directly in the browser — an ugly and confusing
experience during a browser-redirect flow.

This PR replaces those JSON responses with the same styled static HTML
error page already used for group allow-list errors, signups-disabled,
and wrong-login-type errors.

## Changes

### `coderd/userauth.go`
Replaced 3 `httpapi.Write` calls in `userOIDC` with
`site.RenderStaticErrorPage`:

| Error case | Title shown |
|---|---|
| Email domain not in allowed list | "Unauthorized email" |
| Malformed email (no `@`) with domain restrictions | "Unauthorized
email" |
| `email_verified` is `false` | "Email not verified" |

All render HTTP 403 with `HideStatus: true` and a "Back to login" action
button.

### `coderd/userauth_test.go`
- Updated `AssertResponse` callbacks on existing table-driven tests
(`EmailNotVerified`, `NotInRequiredEmailDomain`,
`EmailDomainForbiddenWithLeadingAt`) to verify HTML Content-Type and
page content.
- Extended `TestOIDCDomainErrorMessage` to additionally assert HTML
rendering.
- Added new `TestOIDCErrorPageRendering` with 3 subtests covering all
error scenarios, verifying: HTML doctype, expected title/description,
"Back to login" link, and absence of JSON markers.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Charlie Voiselle
2026-03-16 11:56:59 -04:00
committed by GitHub
parent fa8693605f
commit e94de0bdab
2 changed files with 84 additions and 11 deletions
+35 -8
View File
@@ -44,6 +44,7 @@ import (
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/site"
)
type MergedClaimsSource string
@@ -1343,12 +1344,21 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
verified, ok := verifiedRaw.(bool)
if ok && !verified {
if !api.OIDCConfig.IgnoreEmailVerified {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: fmt.Sprintf("Verify the %q email address on your OIDC provider to authenticate!", email),
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Status: http.StatusForbidden,
HideStatus: true,
Title: "Email not verified",
Description: fmt.Sprintf(
"Verify the %q email address on your OIDC provider to authenticate!",
email,
),
Actions: []site.Action{
{URL: "/login", Text: "Back to login"},
},
})
return
}
logger.Warn(ctx, "allowing unverified oidc email %q")
logger.Warn(ctx, "allowing unverified oidc email", slog.F("email", email))
}
}
@@ -1370,8 +1380,17 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
ok = false
emailSp := strings.Split(email, "@")
if len(emailSp) == 1 {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: fmt.Sprintf("Your email %q is not from an authorized domain! Please contact your administrator.", email),
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Status: http.StatusForbidden,
HideStatus: true,
Title: "Unauthorized email",
Description: fmt.Sprintf(
"Your email %q is not from an authorized domain! Please contact your administrator.",
email,
),
Actions: []site.Action{
{URL: "/login", Text: "Back to login"},
},
})
return
}
@@ -1385,8 +1404,17 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
}
}
if !ok {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: fmt.Sprintf("Your email %q is not from an authorized domain! Please contact your administrator.", email),
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Status: http.StatusForbidden,
HideStatus: true,
Title: "Unauthorized email",
Description: fmt.Sprintf(
"Your email %q is not from an authorized domain! Please contact your administrator.",
email,
),
Actions: []site.Action{
{URL: "/login", Text: "Back to login"},
},
})
return
}
@@ -1406,7 +1434,6 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
if ok {
picture, _ = pictureRaw.(string)
}
ctx = slog.With(ctx, slog.F("email", email), slog.F("username", username), slog.F("name", name))
user, link, err := findLinkedUser(ctx, api.Database, oidcLinkedID(idToken), email)