Skip to content

Commit 769e4f7

Browse files
authored
Merge pull request #9 from linuxfoundation/html-error-page
HTML error pages
2 parents 79a33d1 + 4cd0093 commit 769e4f7

File tree

10 files changed

+436
-107
lines changed

10 files changed

+436
-107
lines changed

.cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
],
5454
"ignorePaths": [
5555
".cspell.json",
56+
".mega-linter.yml",
5657
"LICENSE",
5758
"LICENSE-docs",
5859
"megalinter-reports"

.mega-linter.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
# SPDX-License-Identifier: MIT
33
---
44
DISABLE_LINTERS:
5+
# Skipping for now.
6+
- HTML_DJLINT
7+
- HTML_HTMLHINT
58
# Revive covers this, plus golangci-lint has trouble with newer go toolchains
69
# in go.mod.
710
- GO_GOLANGCI_LINT

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ USER nonroot
3333
EXPOSE 8080
3434

3535
COPY --from=builder /go/bin/auth0-cas-server-go /auth0-cas-server-go
36+
COPY --from=builder /build/templates /templates
3637

3738
ENTRYPOINT ["/auth0-cas-server-go", "-p=8080"]

ERROR_PAGES.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Error Pages Documentation
2+
3+
## Overview
4+
5+
This document describes the error page system implemented in the auth0-cas-server-go service. The system provides user-friendly HTML error pages with embedded CSS styling, while maintaining fallback support for plain text errors. The system differentiates between user-facing routes and CAS protocol routes.
6+
7+
## Architecture
8+
9+
1. **HTML Template** (`templates/error.html`): Responsive error page template with embedded CSS styling and conditional "Go Back" button
10+
2. **Error Wrapper Functions** (`responses.go`): Template rendering with different behaviors for user vs callback routes
11+
3. **CAS Protocol Error Handler**: Uses existing `outputFailure` function for XML/JSON responses
12+
13+
## Route Classification
14+
15+
### User-Facing Routes (with "Go Back" button)
16+
- `/cas/login` - CAS login page
17+
- `/cas/logout` - CAS logout page
18+
19+
### User-Facing Callbacks (without "Go Back" button)
20+
- `/cas/oidc_callback` - OAuth2 callback handler
21+
22+
### CAS Protocol Routes (use `outputFailure`)
23+
- `/cas/serviceValidate` - CAS service validation
24+
- `/cas/p3/serviceValidate` - CAS protocol 3 service validation
25+
- `/cas/proxyValidate` - CAS proxy validation
26+
- `/cas/p3/proxyValidate` - CAS protocol 3 proxy validation
27+
- `/cas/proxy` - CAS proxy ticket granting
28+
29+
## Build Process
30+
31+
The error template uses embedded CSS and requires no build process. The template is self-contained with all styling included inline.
32+
33+
### Dependencies
34+
35+
- **Go html/template**: Standard library package for template rendering
36+
37+
## Usage
38+
39+
### Error Rendering Functions
40+
41+
Different functions are used based on route type:
42+
43+
```go
44+
// User-facing routes (shows "Go Back" button)
45+
renderUserErrorPage(r.Context(), w, http.StatusBadRequest, "Service parameter is required")
46+
47+
// Callback routes (no "Go Back" button)
48+
renderCallbackErrorPage(r.Context(), w, http.StatusBadRequest, "Invalid request")
49+
50+
// CAS protocol routes (use existing outputFailure for XML/JSON)
51+
outputFailure(r.Context(), w, err, "INVALID_REQUEST", "service parameter is required", useJSON)
52+
```
53+
54+
### Template Data Structure
55+
56+
```go
57+
type ErrorPageData struct {
58+
StatusCode int // HTTP status code (e.g., 400, 500)
59+
Message string // User-friendly error message
60+
ShowBackButton bool // Whether to show the "Go Back" button
61+
}
62+
```
63+
64+
### Conditional Back Button
65+
66+
The template includes conditional logic for the back button:
67+
68+
```html
69+
{{if .ShowBackButton}}
70+
<button onclick="history.back()" class="back-button">
71+
<svg class="back-icon" ...>...</svg>
72+
Go Back
73+
</button>
74+
{{end}}
75+
```
76+
77+
### Fallback Behavior
78+
79+
1. **Template Available**: Renders HTML error page with embedded styling
80+
2. **Template Missing**: Falls back to plain text error response
81+
3. **Template Error**: Logs error and continues with partial content
82+
4. **CAS Protocol Routes**: Always use XML/JSON responses via `outputFailure`
83+
84+
### Template Modifications
85+
86+
The error template supports standard Go template syntax with conditional logic:
87+
88+
```html
89+
<h1 class="error-title">Error {{.StatusCode}}</h1>
90+
<p class="error-message">{{.Message}}</p>
91+
{{if .ShowBackButton}}
92+
<button onclick="history.back()" class="back-button">
93+
<svg class="back-icon" ...>...</svg>
94+
Go Back
95+
</button>
96+
{{end}}
97+
```
98+
99+
### Adding New Routes
100+
101+
When adding new routes:
102+
103+
1. Determine if it's user-facing, callback, or CAS protocol
104+
2. Use the appropriate error function:
105+
- `renderUserErrorPage()` for user-facing with back button
106+
- `renderCallbackErrorPage()` for user-facing without back button
107+
- `outputFailure()` for CAS protocol routes

cas.go

Lines changed: 15 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -62,26 +62,26 @@ func casLogin(w http.ResponseWriter, r *http.Request) {
6262
service := params.Get("service")
6363
if service == "" {
6464
appLogger(r.Context()).Warn("service parameter is required")
65-
http.Error(w, "service parameter is required", http.StatusBadRequest)
65+
renderUserErrorPage(r.Context(), w, http.StatusBadRequest, "Service parameter is required")
6666
return
6767
}
6868

6969
if _, err := url.Parse(service); err != nil {
7070
// We don't use this now, but better to catch here than in oauth2Callback.
7171
appLogger(r.Context()).Warn("invalid service URL")
72-
http.Error(w, "invalid service URL", http.StatusBadRequest)
72+
renderUserErrorPage(r.Context(), w, http.StatusBadRequest, "Invalid service URL")
7373
return
7474
}
7575

7676
casClient, err := getAuth0ClientByService(r.Context(), service)
7777
if err != nil {
7878
appLogger(r.Context()).Error("error looking up service", "error", err)
79-
http.Error(w, "error looking up service", http.StatusInternalServerError)
79+
renderUserErrorPage(r.Context(), w, http.StatusInternalServerError, "Error looking up service")
8080
return
8181
}
8282
if casClient == nil {
8383
appLogger(r.Context()).Warn("unknown service")
84-
http.Error(w, "unknown service", http.StatusForbidden)
84+
renderUserErrorPage(r.Context(), w, http.StatusForbidden, "Unknown service")
8585
return
8686
}
8787

@@ -104,17 +104,17 @@ func casLogin(w http.ResponseWriter, r *http.Request) {
104104
session, _ := store.Get(r, "cas-shim")
105105
session.Values[state] = service
106106
err = session.Save(r, w)
107-
if err != nil && err.Error() == "securecookie: the value is too long" {
107+
if err != nil && strings.HasPrefix(err.Error(), "securecookie: the value is too long") {
108108
// The cookie can get too big if the user tries 10+ logins in the day
109109
// without returning from any of them.
110-
appLogger(r.Context()).Warn("cookie too large (bot or other bad client)")
110+
appLogger(r.Context()).Warn("cookie too large", "error", err)
111111
w.Header().Set("Retry-After", "86400")
112-
http.Error(w, "429 too many requests", http.StatusTooManyRequests)
112+
renderUserErrorPage(r.Context(), w, http.StatusTooManyRequests, "Session size limit reached. Either the URL you are logging into is too long, or you had too many unsuccessful logins in the last 24 hours.")
113113
return
114114
}
115115
if err != nil {
116116
appLogger(r.Context()).Error("error saving session", "error", err)
117-
http.Error(w, "500 internal server error", http.StatusInternalServerError)
117+
renderUserErrorPage(r.Context(), w, http.StatusInternalServerError, "Error saving session")
118118
return
119119
}
120120

@@ -320,9 +320,7 @@ func casServiceValidate(w http.ResponseWriter, r *http.Request) {
320320
}
321321
output, err := validationResponse(&success, nil, useJSON)
322322
if err != nil {
323-
appLogger(r.Context()).Error("error generating validation response", "error", err, "success", success)
324-
w.WriteHeader(http.StatusInternalServerError)
325-
http.Error(w, "error generating validation response", http.StatusInternalServerError)
323+
outputFailure(r.Context(), w, err, "INTERNAL_ERROR", "error generating validation response", useJSON)
326324
return
327325
}
328326

@@ -395,27 +393,27 @@ func oauth2Callback(w http.ResponseWriter, r *http.Request) {
395393
// Consider this a warning-level error for logging purposes.
396394
err := fmt.Errorf("%s: %s", errParam, errDescription)
397395
appLogger(r.Context()).Warn("login aborted", "error", err)
398-
http.Error(w, err.Error(), http.StatusBadRequest)
396+
renderCallbackErrorPage(r.Context(), w, http.StatusBadRequest, "Login was cancelled")
399397
return
400398
}
401399
if errParam != "" {
402400
err := fmt.Errorf("%s: %s", errParam, errDescription)
403401
appLogger(r.Context()).Error("login error", "error", err)
404-
http.Error(w, err.Error(), http.StatusBadRequest)
402+
renderCallbackErrorPage(r.Context(), w, http.StatusBadRequest, "Login error occurred")
405403
return
406404
}
407405

408406
code := params.Get("code")
409407
if code == "" {
410408
appLogger(r.Context()).Warn("invalid request")
411-
http.Error(w, "invalid request", http.StatusBadRequest)
409+
renderCallbackErrorPage(r.Context(), w, http.StatusBadRequest, "Invalid request")
412410
return
413411
}
414412

415413
state := params.Get("state")
416414
if state == "" {
417415
appLogger(r.Context()).Warn("missing state")
418-
http.Error(w, "missing state", http.StatusBadRequest)
416+
renderCallbackErrorPage(r.Context(), w, http.StatusBadRequest, "Missing state parameter")
419417
return
420418
}
421419

@@ -424,7 +422,7 @@ func oauth2Callback(w http.ResponseWriter, r *http.Request) {
424422
var ok bool
425423
if service, ok = session.Values[state].(string); !ok {
426424
appLogger(r.Context()).Warn("session missing or expired")
427-
http.Error(w, "session missing or expired", http.StatusBadRequest)
425+
renderCallbackErrorPage(r.Context(), w, http.StatusBadRequest, "Session missing or expired")
428426
return
429427
}
430428

@@ -434,7 +432,7 @@ func oauth2Callback(w http.ResponseWriter, r *http.Request) {
434432
serviceURL, err := url.Parse(service)
435433
if err != nil {
436434
appLogger(r.Context()).Warn("invalid service URL")
437-
http.Error(w, "invalid service URL", http.StatusBadRequest)
435+
renderCallbackErrorPage(r.Context(), w, http.StatusBadRequest, "Invalid service URL")
438436
return
439437
}
440438

@@ -524,33 +522,3 @@ func getLogoutParams(ctx context.Context, returnTo string) *url.Values {
524522

525523
return nil
526524
}
527-
528-
// outputFailure handles a common case of reporting a problem to the
529-
// /cas/serviceValidate URL, which is expected to return a properly-formatted
530-
// error. This logs the issue, and formats and outputs the response (default
531-
// 200 status code). If the response cannot be formatted, an additional error
532-
// is logged and a plain-text message and 500 response is output.
533-
func outputFailure(ctx context.Context, w http.ResponseWriter, err error, code, description string, useJSON bool) {
534-
switch {
535-
case err != nil:
536-
appLogger(ctx).Error(description, "error", err)
537-
default:
538-
appLogger(ctx).Warn(description)
539-
}
540-
541-
failure := casAuthenticationFailure{code, description}
542-
output, err := validationResponse(nil, &failure, useJSON)
543-
if err != nil {
544-
appLogger(ctx).Error("error generating validation response", "error", err, "failure", failure)
545-
w.WriteHeader(http.StatusInternalServerError)
546-
http.Error(w, "error generating validation response", http.StatusInternalServerError)
547-
return
548-
}
549-
switch useJSON {
550-
case true:
551-
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
552-
default:
553-
w.Header().Set("Content-Type", "application/xml;charset=UTF-8")
554-
}
555-
fmt.Fprintf(w, "%s\n", output)
556-
}

docker-compose.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Copyright The Linux Foundation and its contributors.
22
# SPDX-License-Identifier: MIT
33
---
4-
version: "2"
54
services:
65
# Auth0 CAS server.
76
auth0-cas-server-go:

go.mod

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33

44
module github.com/linuxfoundation/auth0-cas-server-go
55

6-
go 1.25.0
6+
go 1.25.3
77

88
require (
99
github.com/bmatcuk/doublestar/v4 v4.9.1
1010
github.com/gorilla/sessions v1.4.0
1111
github.com/joho/godotenv v1.5.1
1212
github.com/patrickmn/go-cache v2.1.0+incompatible
13-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0
14-
go.opentelemetry.io/otel v1.37.0
15-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
16-
go.opentelemetry.io/otel/sdk v1.37.0
17-
go.opentelemetry.io/otel/trace v1.37.0
18-
golang.org/x/oauth2 v0.30.0
19-
golang.org/x/text v0.28.0
13+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
14+
go.opentelemetry.io/otel v1.38.0
15+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
16+
go.opentelemetry.io/otel/sdk v1.38.0
17+
go.opentelemetry.io/otel/trace v1.38.0
18+
golang.org/x/oauth2 v0.32.0
19+
golang.org/x/text v0.30.0
2020
)
2121

2222
require (
@@ -26,15 +26,15 @@ require (
2626
github.com/go-logr/stdr v1.2.2 // indirect
2727
github.com/google/uuid v1.6.0 // indirect
2828
github.com/gorilla/securecookie v1.1.2 // indirect
29-
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
30-
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
31-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
32-
go.opentelemetry.io/otel/metric v1.37.0 // indirect
33-
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
34-
golang.org/x/net v0.43.0 // indirect
35-
golang.org/x/sys v0.35.0 // indirect
36-
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
37-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
38-
google.golang.org/grpc v1.75.0 // indirect
39-
google.golang.org/protobuf v1.36.7 // indirect
29+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
30+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
31+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
32+
go.opentelemetry.io/otel/metric v1.38.0 // indirect
33+
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
34+
golang.org/x/net v0.46.0 // indirect
35+
golang.org/x/sys v0.37.0 // indirect
36+
google.golang.org/genproto/googleapis/api v0.0.0-20251020155222-88f65dc88635 // indirect
37+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635 // indirect
38+
google.golang.org/grpc v1.76.0 // indirect
39+
google.golang.org/protobuf v1.36.10 // indirect
4040
)

0 commit comments

Comments
 (0)