Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 11:00 UTC
caddyauth: add candidate placeholders for rejected identities (#7698)
Zen Dodd committed
May 20, 2026 at 23:51 UTC
408d20a0e5b5311ffc7f0312e8829d281ff55ac1
2 files changed
+224
-4
modules/caddyhttp/caddyauth/caddyauth.go
+27
-4
@@ -37,6 +37,12 @@ func init() {
37
// `{http.auth.user.*}` placeholders may be set for any authentication
38
// modules that provide user metadata.
39
//
40
+// If authentication is rejected but a provider returns user information,
41
+// the placeholder `{http.auth.candidate.id}` will be set to the candidate
42
+// username, and also `{http.auth.candidate.*}` placeholders may be set
43
+// for candidate user metadata. Candidate placeholders do not represent a
44
+// successfully authenticated principal.
45
+//
46
// In case of an error, the placeholder `{http.auth.<provider>.error}`
47
// will be set to the error message returned by the authentication
48
// provider.
@@ -78,6 +84,8 @@ func (a *Authentication) Provision(ctx caddy.Context) error {
84
func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
85
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
86
var user User
87
+ var candidate User
88
+ var hasCandidate bool
89
var authed bool
90
var err error
91
for provName, prov := range a.Providers {
@@ -94,19 +102,34 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
102
if authed {
103
break
104
}
105
+ if userHasInfo(user) {
106
+ candidate = user
107
+ hasCandidate = true
108
+ }
109
}
110
if !authed {
111
+ if hasCandidate {
112
+ setAuthUserPlaceholders(repl, "http.auth.candidate", candidate)
113
+ }
114
return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated"))
115
}
116
102
- repl.Set("http.auth.user.id", user.ID)
103
- for k, v := range user.Metadata {
104
- repl.Set("http.auth.user."+k, v)
105
- }
117
+ setAuthUserPlaceholders(repl, "http.auth.user", user)
118
119
return next.ServeHTTP(w, r)
120
}
121
122
+func userHasInfo(user User) bool {
123
+ return user.ID != "" || len(user.Metadata) > 0
124
+}
125
+
126
+func setAuthUserPlaceholders(repl *caddy.Replacer, namespace string, user User) {
127
+ repl.Set(namespace+".id", user.ID)
128
+ for k, v := range user.Metadata {
129
+ repl.Set(namespace+"."+k, v)
130
+ }
131
+}
132
+
133
// Authenticator is a type which can authenticate a request.
134
// If a request was not authenticated, it returns false. An
135
// error is only returned if authenticating the request fails
modules/caddyhttp/caddyauth/caddyauth_test.go
new
+197
@@ -0,0 +1,197 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddyauth
16
+
17
+import (
18
+ "context"
19
+ "errors"
20
+ "net/http"
21
+ "net/http/httptest"
22
+ "testing"
23
+
24
+ "go.uber.org/zap"
25
+
26
+ "github.com/caddyserver/caddy/v2"
27
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
28
+)
29
+
30
+func TestAuthenticationRejectedUserSetsCandidatePlaceholders(t *testing.T) {
31
+ auth := Authentication{
32
+ Providers: map[string]Authenticator{
33
+ "test": staticAuthenticator{
34
+ user: User{
35
+ ID: "alice",
36
+ Metadata: map[string]string{
37
+ "role": "admin",
38
+ },
39
+ },
40
+ },
41
+ },
42
+ logger: zap.NewNop(),
43
+ }
44
+ req, repl := newRequestWithReplacer()
45
+ nextCalled := false
46
+
47
+ err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
48
+ nextCalled = true
49
+ return nil
50
+ }))
51
+ if err == nil {
52
+ t.Fatal("expected authentication error")
53
+ }
54
+ var handlerErr caddyhttp.HandlerError
55
+ if !errors.As(err, &handlerErr) {
56
+ t.Fatalf("expected HandlerError, got %T", err)
57
+ }
58
+ if handlerErr.StatusCode != http.StatusUnauthorized {
59
+ t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, handlerErr.StatusCode)
60
+ }
61
+ if nextCalled {
62
+ t.Fatal("next handler was called for rejected authentication")
63
+ }
64
+
65
+ assertPlaceholder(t, repl, "http.auth.candidate.id", "alice")
66
+ assertPlaceholder(t, repl, "http.auth.candidate.role", "admin")
67
+ assertPlaceholderAbsent(t, repl, "http.auth.user.id")
68
+ assertPlaceholderAbsent(t, repl, "http.auth.user.role")
69
+}
70
+
71
+func TestAuthenticationSuccessfulUserSetsUserPlaceholdersOnly(t *testing.T) {
72
+ auth := Authentication{
73
+ Providers: map[string]Authenticator{
74
+ "test": staticAuthenticator{
75
+ user: User{
76
+ ID: "alice",
77
+ Metadata: map[string]string{
78
+ "role": "admin",
79
+ },
80
+ },
81
+ authed: true,
82
+ },
83
+ },
84
+ logger: zap.NewNop(),
85
+ }
86
+ req, repl := newRequestWithReplacer()
87
+ nextCalled := false
88
+
89
+ err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
90
+ nextCalled = true
91
+ return nil
92
+ }))
93
+ if err != nil {
94
+ t.Fatalf("expected no authentication error, got %v", err)
95
+ }
96
+ if !nextCalled {
97
+ t.Fatal("next handler was not called for successful authentication")
98
+ }
99
+
100
+ assertPlaceholder(t, repl, "http.auth.user.id", "alice")
101
+ assertPlaceholder(t, repl, "http.auth.user.role", "admin")
102
+ assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
103
+ assertPlaceholderAbsent(t, repl, "http.auth.candidate.role")
104
+}
105
+
106
+func TestAuthenticationSuccessfulProviderDoesNotExposeEarlierCandidate(t *testing.T) {
107
+ auth := Authentication{
108
+ Providers: map[string]Authenticator{
109
+ "first": staticAuthenticator{
110
+ user: User{
111
+ ID: "rejected",
112
+ Metadata: map[string]string{
113
+ "role": "guest",
114
+ },
115
+ },
116
+ },
117
+ "second": staticAuthenticator{
118
+ user: User{
119
+ ID: "accepted",
120
+ Metadata: map[string]string{
121
+ "role": "admin",
122
+ },
123
+ },
124
+ authed: true,
125
+ },
126
+ },
127
+ logger: zap.NewNop(),
128
+ }
129
+ req, repl := newRequestWithReplacer()
130
+
131
+ err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
132
+ return nil
133
+ }))
134
+ if err != nil {
135
+ t.Fatalf("expected no authentication error, got %v", err)
136
+ }
137
+
138
+ assertPlaceholder(t, repl, "http.auth.user.id", "accepted")
139
+ assertPlaceholder(t, repl, "http.auth.user.role", "admin")
140
+ assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
141
+ assertPlaceholderAbsent(t, repl, "http.auth.candidate.role")
142
+}
143
+
144
+func TestAuthenticationRejectedEmptyUserDoesNotSetCandidatePlaceholders(t *testing.T) {
145
+ auth := Authentication{
146
+ Providers: map[string]Authenticator{
147
+ "test": staticAuthenticator{},
148
+ },
149
+ logger: zap.NewNop(),
150
+ }
151
+ req, repl := newRequestWithReplacer()
152
+
153
+ err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
154
+ t.Fatal("next handler was called for rejected authentication")
155
+ return nil
156
+ }))
157
+ if err == nil {
158
+ t.Fatal("expected authentication error")
159
+ }
160
+
161
+ assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
162
+}
163
+
164
+func newRequestWithReplacer() (*http.Request, *caddy.Replacer) {
165
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
166
+ repl := caddy.NewReplacer()
167
+ ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
168
+ return req.WithContext(ctx), repl
169
+}
170
+
171
+func assertPlaceholder(t *testing.T, repl *caddy.Replacer, key, expected string) {
172
+ t.Helper()
173
+ actual, ok := repl.GetString(key)
174
+ if !ok {
175
+ t.Fatalf("expected placeholder %q to be set", key)
176
+ }
177
+ if actual != expected {
178
+ t.Fatalf("expected placeholder %q to be %q, got %q", key, expected, actual)
179
+ }
180
+}
181
+
182
+func assertPlaceholderAbsent(t *testing.T, repl *caddy.Replacer, key string) {
183
+ t.Helper()
184
+ if actual, ok := repl.GetString(key); ok {
185
+ t.Fatalf("expected placeholder %q to be absent, got %q", key, actual)
186
+ }
187
+}
188
+
189
+type staticAuthenticator struct {
190
+ user User
191
+ authed bool
192
+ err error
193
+}
194
+
195
+func (a staticAuthenticator) Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) {
196
+ return a.user, a.authed, a.err
197
+}