@marcel91200 / caddy Mirror · read-only / commits / 408d20a0
Fork 0

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 +}