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, 01:00 UTC
| 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 | "bufio" |
| 19 | "bytes" |
| 20 | "fmt" |
| 21 | "io" |
| 22 | "maps" |
| 23 | "net" |
| 24 | "net/http" |
| 25 | |
| 26 | "go.uber.org/zap" |
| 27 | "go.uber.org/zap/zapcore" |
| 28 | |
| 29 | "github.com/caddyserver/caddy/v2" |
| 30 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" |
| 31 | ) |
| 32 | |
| 33 | func init() { |
| 34 | caddy.RegisterModule(Authentication{}) |
| 35 | } |
| 36 | |
| 37 | // Authentication is a middleware which provides user authentication. |
| 38 | // Rejects requests with HTTP 401 if the request is not authenticated. |
| 39 | // |
| 40 | // After a successful authentication, the placeholder |
| 41 | // `{http.auth.user.id}` will be set to the username, and also |
| 42 | // `{http.auth.user.*}` placeholders may be set for any authentication |
| 43 | // modules that provide user metadata. |
| 44 | // |
| 45 | // If authentication is rejected but a provider returns user information, |
| 46 | // the placeholder `{http.auth.candidate.id}` will be set to the candidate |
| 47 | // username, and also `{http.auth.candidate.*}` placeholders may be set |
| 48 | // for candidate user metadata. Candidate placeholders do not represent a |
| 49 | // successfully authenticated principal. |
| 50 | // |
| 51 | // In case of an error, the placeholder `{http.auth.<provider>.error}` |
| 52 | // will be set to the error message returned by the authentication |
| 53 | // provider. |
| 54 | // |
| 55 | // Its API is still experimental and may be subject to change. |
| 56 | type Authentication struct { |
| 57 | // A set of authentication providers. If none are specified, |
| 58 | // all requests will always be unauthenticated. |
| 59 | ProvidersRaw caddy.ModuleMap `json:"providers,omitempty" caddy:"namespace=http.authentication.providers"` |
| 60 | |
| 61 | Providers map[string]Authenticator `json:"-"` |
| 62 | |
| 63 | logger *zap.Logger |
| 64 | } |
| 65 | |
| 66 | // CaddyModule returns the Caddy module information. |
| 67 | func (Authentication) CaddyModule() caddy.ModuleInfo { |
| 68 | return caddy.ModuleInfo{ |
| 69 | ID: "http.handlers.authentication", |
| 70 | New: func() caddy.Module { return new(Authentication) }, |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | // Provision sets up an Authentication module by initializing its logger, |
| 75 | // loading and registering all configured authentication providers. |
| 76 | func (a *Authentication) Provision(ctx caddy.Context) error { |
| 77 | a.logger = ctx.Logger() |
| 78 | a.Providers = make(map[string]Authenticator) |
| 79 | mods, err := ctx.LoadModule(a, "ProvidersRaw") |
| 80 | if err != nil { |
| 81 | return fmt.Errorf("loading authentication providers: %v", err) |
| 82 | } |
| 83 | for modName, modIface := range mods.(map[string]any) { |
| 84 | a.Providers[modName] = modIface.(Authenticator) |
| 85 | } |
| 86 | return nil |
| 87 | } |
| 88 | |
| 89 | func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { |
| 90 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 91 | var user User |
| 92 | var candidate User |
| 93 | var hasCandidate bool |
| 94 | var authed bool |
| 95 | var err error |
| 96 | |
| 97 | // With a single provider there is nothing to isolate it from, so it is |
| 98 | // given the real ResponseWriter directly — no buffering, and every writer |
| 99 | // capability (Flusher, Hijacker, Pusher, ReaderFrom, ...) is preserved |
| 100 | // exactly as before. Only when MULTIPLE providers are configured can a |
| 101 | // failing provider's response clobber another provider's or the successful |
| 102 | // handler chain's, so there each provider gets its own bounded buffered |
| 103 | // writer. See https://github.com/caddyserver/caddy/issues/5190. |
| 104 | isolate := len(a.Providers) > 1 |
| 105 | |
| 106 | var winner *bufferedResponseWriter |
| 107 | var failed []*bufferedResponseWriter |
| 108 | for provName, prov := range a.Providers { |
| 109 | pw := w |
| 110 | var bw *bufferedResponseWriter |
| 111 | if isolate { |
| 112 | bw = newBufferedResponseWriter(w) |
| 113 | pw = bw |
| 114 | } |
| 115 | user, authed, err = prov.Authenticate(pw, r) |
| 116 | if err != nil { |
| 117 | if c := a.logger.Check(zapcore.ErrorLevel, "auth provider returned error"); c != nil { |
| 118 | c.Write(zap.String("provider", provName), zap.Error(err)) |
| 119 | } |
| 120 | // Set the error from the authentication provider in a placeholder, |
| 121 | // so it can be used in the handle_errors directive. |
| 122 | repl.Set("http.auth."+provName+".error", err.Error()) |
| 123 | continue |
| 124 | } |
| 125 | if authed { |
| 126 | winner = bw |
| 127 | break |
| 128 | } |
| 129 | if isolate { |
| 130 | failed = append(failed, bw) |
| 131 | } |
| 132 | if userHasInfo(user) { |
| 133 | candidate = user |
| 134 | hasCandidate = true |
| 135 | } |
| 136 | } |
| 137 | if !authed { |
| 138 | if hasCandidate { |
| 139 | setAuthUserPlaceholders(repl, "http.auth.candidate", candidate) |
| 140 | } |
| 141 | // When isolating, no failed provider's response reached the real |
| 142 | // writer, so apply one provider's challenge headers (e.g. a |
| 143 | // WWW-Authenticate, or a Location); a redirecting provider takes |
| 144 | // precedence and is sent as a full response. A single provider already |
| 145 | // wrote its challenge directly to the real writer. Either way, fall |
| 146 | // through to the auth error so handle_errors runs and a challenge that |
| 147 | // set only headers (like basic auth) still returns 401, not 200. |
| 148 | if isolate { |
| 149 | if replay := pickReplay(failed); replay != nil { |
| 150 | maps.Copy(w.Header(), replay.header) |
| 151 | if replay.statusCode >= 300 && replay.statusCode < 400 { |
| 152 | w.WriteHeader(replay.statusCode) |
| 153 | _, _ = w.Write(replay.buf.Bytes()) |
| 154 | return nil |
| 155 | } |
| 156 | } |
| 157 | } |
| 158 | return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")) |
| 159 | } |
| 160 | |
| 161 | // When isolating, the winning provider wrote to its buffer; copy the |
| 162 | // headers it set (e.g. a Set-Cookie establishing a new session) onto the |
| 163 | // real writer. Its status/body are NOT replayed: the request is |
| 164 | // authenticated and continues down the handler chain, which produces the |
| 165 | // actual response. (A single provider already wrote its headers directly.) |
| 166 | if winner != nil { |
| 167 | maps.Copy(w.Header(), winner.header) |
| 168 | } |
| 169 | |
| 170 | setAuthUserPlaceholders(repl, "http.auth.user", user) |
| 171 | |
| 172 | return next.ServeHTTP(w, r) |
| 173 | } |
| 174 | |
| 175 | // pickReplay chooses which failed provider's buffered response to send when |
| 176 | // no provider authenticated: a redirect (3xx) wins, otherwise the first |
| 177 | // provider that wrote anything (status, body, or headers). |
| 178 | func pickReplay(failed []*bufferedResponseWriter) *bufferedResponseWriter { |
| 179 | var replay *bufferedResponseWriter |
| 180 | for _, bw := range failed { |
| 181 | if bw.statusCode >= 300 && bw.statusCode < 400 { |
| 182 | return bw |
| 183 | } |
| 184 | if replay == nil && (bw.statusCode != 0 || bw.buf.Len() > 0 || len(bw.header) > 0) { |
| 185 | replay = bw |
| 186 | } |
| 187 | } |
| 188 | return replay |
| 189 | } |
| 190 | |
| 191 | func userHasInfo(user User) bool { |
| 192 | return user.ID != "" || len(user.Metadata) > 0 |
| 193 | } |
| 194 | |
| 195 | // maxBufferedAuthResponse caps how much of a single provider's response body |
| 196 | // is buffered during multi-provider authentication, so a misbehaving provider |
| 197 | // cannot cause unbounded memory use. Authentication challenges are tiny; this |
| 198 | // limit is far larger than any legitimate challenge. |
| 199 | const maxBufferedAuthResponse = 1 << 20 // 1 MiB |
| 200 | |
| 201 | // bufferedResponseWriter captures a single provider's response — headers, |
| 202 | // status, and a size-capped body — so it can be discarded or replayed once |
| 203 | // the outcome of the whole provider set is known (only used when more than |
| 204 | // one provider is configured). |
| 205 | // |
| 206 | // It implements every capability interface a provider may hold on the real |
| 207 | // writer, so both plain type assertions (w.(http.Flusher)) and |
| 208 | // http.ResponseController keep working exactly as they did before isolation: |
| 209 | // Pusher and ReaderFrom come from the embedded caddyhttp.ResponseWriterWrapper, |
| 210 | // Hijacker and Flusher are declared below. Body writes are captured rather |
| 211 | // than streamed, and flushing is suppressed while buffering. A provider that |
| 212 | // hijacks the connection mid-authentication escapes to the real writer |
| 213 | // (isolation cannot apply once hijacked); auth providers are not expected to |
| 214 | // stream or hijack during Authenticate. |
| 215 | type bufferedResponseWriter struct { |
| 216 | *caddyhttp.ResponseWriterWrapper |
| 217 | header http.Header |
| 218 | statusCode int |
| 219 | buf bytes.Buffer |
| 220 | overflowed bool |
| 221 | } |
| 222 | |
| 223 | func newBufferedResponseWriter(w http.ResponseWriter) *bufferedResponseWriter { |
| 224 | return &bufferedResponseWriter{ |
| 225 | ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w}, |
| 226 | header: make(http.Header), |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | func (bw *bufferedResponseWriter) Header() http.Header { return bw.header } |
| 231 | |
| 232 | func (bw *bufferedResponseWriter) WriteHeader(statusCode int) { |
| 233 | if bw.statusCode == 0 { |
| 234 | bw.statusCode = statusCode |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | func (bw *bufferedResponseWriter) Write(data []byte) (int, error) { |
| 239 | bw.WriteHeader(http.StatusOK) |
| 240 | if room := maxBufferedAuthResponse - bw.buf.Len(); room < len(data) { |
| 241 | bw.overflowed = true |
| 242 | if room > 0 { |
| 243 | bw.buf.Write(data[:room]) |
| 244 | } |
| 245 | // Report a full write so the provider does not error; the body is a |
| 246 | // discardable challenge and only its capped prefix is retained. |
| 247 | return len(data), nil |
| 248 | } |
| 249 | return bw.buf.Write(data) |
| 250 | } |
| 251 | |
| 252 | // ReadFrom captures the body (respecting the size cap) instead of the |
| 253 | // embedded wrapper's pass-through to the real writer, which would leak the |
| 254 | // provider's body past isolation. It returns the total number of bytes |
| 255 | // consumed from r — retained and drained alike — and any read error, |
| 256 | // per the io.ReaderFrom contract. |
| 257 | func (bw *bufferedResponseWriter) ReadFrom(r io.Reader) (int64, error) { |
| 258 | bw.WriteHeader(http.StatusOK) |
| 259 | room := int64(maxBufferedAuthResponse - bw.buf.Len()) |
| 260 | n, err := bw.buf.ReadFrom(io.LimitReader(r, room)) |
| 261 | if err != nil { |
| 262 | return n, err |
| 263 | } |
| 264 | extra, err := io.Copy(io.Discard, r) |
| 265 | if extra > 0 { |
| 266 | bw.overflowed = true |
| 267 | } |
| 268 | return n + extra, err |
| 269 | } |
| 270 | |
| 271 | // Flush implements http.Flusher. Flushing is suppressed while a provider's |
| 272 | // response is buffered, so a provider that flushes during Authenticate cannot |
| 273 | // prematurely commit the response to the client. It is declared explicitly |
| 274 | // (rather than left to FlushError) so that providers doing a plain |
| 275 | // w.(http.Flusher) assertion still find one, as they do on the real writer. |
| 276 | func (bw *bufferedResponseWriter) Flush() {} |
| 277 | |
| 278 | // FlushError is the same suppression for http.ResponseController.Flush, which |
| 279 | // prefers FlushError over Flush. See https://github.com/caddyserver/caddy/issues/6144. |
| 280 | func (bw *bufferedResponseWriter) FlushError() error { return nil } |
| 281 | |
| 282 | // Hijack implements http.Hijacker by delegating to the real writer: once a |
| 283 | // provider takes over the connection there is no response left to isolate. |
| 284 | // This mirrors responseRecorder.Hijack in the caddyhttp package. The |
| 285 | // controller is built on the embedded wrapper, whose Unwrap reaches the real |
| 286 | // writer, and returns http.ErrNotSupported if that writer cannot hijack. |
| 287 | func (bw *bufferedResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { |
| 288 | //nolint:bodyclose |
| 289 | return http.NewResponseController(bw.ResponseWriterWrapper).Hijack() |
| 290 | } |
| 291 | |
| 292 | func setAuthUserPlaceholders(repl *caddy.Replacer, namespace string, user User) { |
| 293 | repl.Set(namespace+".id", user.ID) |
| 294 | for k, v := range user.Metadata { |
| 295 | repl.Set(namespace+"."+k, v) |
| 296 | } |
| 297 | } |
| 298 | |
| 299 | // Authenticator is a type which can authenticate a request. |
| 300 | // If a request was not authenticated, it returns false. An |
| 301 | // error is only returned if authenticating the request fails |
| 302 | // for a technical reason (not for bad/missing credentials). |
| 303 | type Authenticator interface { |
| 304 | Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) |
| 305 | } |
| 306 | |
| 307 | // User represents an authenticated user. |
| 308 | type User struct { |
| 309 | // The ID of the authenticated user. |
| 310 | ID string |
| 311 | |
| 312 | // Any other relevant data about this |
| 313 | // user. Keys should be adhere to Caddy |
| 314 | // conventions (snake_casing), as all |
| 315 | // keys will be made available as |
| 316 | // placeholders. |
| 317 | Metadata map[string]string |
| 318 | } |
| 319 | |
| 320 | // Interface guards |
| 321 | var ( |
| 322 | _ caddy.Provisioner = (*Authentication)(nil) |
| 323 | _ caddyhttp.MiddlewareHandler = (*Authentication)(nil) |
| 324 | ) |