summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/reverseproxy/forwardauth
diff options
context:
space:
mode:
authorFrancis Lavoie <lavofr@gmail.com>2022-05-06 10:50:26 -0400
committerGitHub <noreply@github.com>2022-05-06 10:50:26 -0400
commitf6900fcf530e80c921dac8e4f09996cffce7f436 (patch)
tree6914d9a1038ff230f9047cae8d219838b6636d81 /modules/caddyhttp/reverseproxy/forwardauth
parentec86a2f7a3905c888b8c953255a98a50be70d5db (diff)
reverseproxy: Support performing pre-check requests (#4739)
Diffstat (limited to 'modules/caddyhttp/reverseproxy/forwardauth')
-rw-r--r--modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go269
1 files changed, 269 insertions, 0 deletions
diff --git a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
new file mode 100644
index 0000000..1571f09
--- /dev/null
+++ b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
@@ -0,0 +1,269 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package forwardauth
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig"
+ "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
+)
+
+func init() {
+ httpcaddyfile.RegisterDirective("forward_auth", parseCaddyfile)
+}
+
+// parseCaddyfile parses the forward_auth directive, which has the same syntax
+// as the reverse_proxy directive (in fact, the reverse_proxy's directive
+// Unmarshaler is invoked by this function) but the resulting proxy is specially
+// configured for most™️ auth gateways that support forward auth. The typical
+// config which looks something like this:
+//
+// forward_auth auth-gateway:9091 {
+// uri /authenticate?redirect=https://auth.example.com
+// copy_headers Remote-User Remote-Email
+// }
+//
+// is equivalent to a reverse_proxy directive like this:
+//
+// reverse_proxy auth-gateway:9091 {
+// method GET
+// rewrite /authenticate?redirect=https://auth.example.com
+//
+// header_up X-Forwarded-Method {method}
+// header_up X-Forwarded-Uri {uri}
+//
+// @good status 2xx
+// handle_response @good {
+// request_header {
+// Remote-User {http.reverse_proxy.header.Remote-User}
+// Remote-Email {http.reverse_proxy.header.Remote-Email}
+// }
+// }
+//
+// handle_response {
+// copy_response_headers {
+// exclude Connection Keep-Alive Te Trailers Transfer-Encoding Upgrade
+// }
+// copy_response
+// }
+// }
+//
+func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
+ if !h.Next() {
+ return nil, h.ArgErr()
+ }
+
+ // if the user specified a matcher token, use that
+ // matcher in a route that wraps both of our routes;
+ // either way, strip the matcher token and pass
+ // the remaining tokens to the unmarshaler so that
+ // we can gain the rest of the reverse_proxy syntax
+ userMatcherSet, err := h.ExtractMatcherSet()
+ if err != nil {
+ return nil, err
+ }
+
+ // make a new dispenser from the remaining tokens so that we
+ // can reset the dispenser back to this point for the
+ // reverse_proxy unmarshaler to read from it as well
+ dispenser := h.NewFromNextSegment()
+
+ // create the reverse proxy handler
+ rpHandler := &reverseproxy.Handler{
+ // set up defaults for header_up; reverse_proxy already deals with
+ // adding the other three X-Forwarded-* headers, but for this flow,
+ // we want to also send along the incoming method and URI since this
+ // request will have a rewritten URI and method.
+ Headers: &headers.Handler{
+ Request: &headers.HeaderOps{
+ Set: http.Header{
+ "X-Forwarded-Method": []string{"{http.request.method}"},
+ "X-Forwarded-Uri": []string{"{http.request.uri}"},
+ },
+ },
+ },
+
+ // we always rewrite the method to GET, which implicitly
+ // turns off sending the incoming request's body, which
+ // allows later middleware handlers to consume it
+ Rewrite: &rewrite.Rewrite{
+ Method: "GET",
+ },
+
+ HandleResponse: []caddyhttp.ResponseHandler{},
+ }
+
+ // collect the headers to copy from the auth response
+ // onto the original request, so they can get passed
+ // through to a backend app
+ headersToCopy := []string{}
+
+ // read the subdirectives for configuring the forward_auth shortcut
+ // NOTE: we delete the tokens as we go so that the reverse_proxy
+ // unmarshal doesn't see these subdirectives which it cannot handle
+ for dispenser.Next() {
+ for dispenser.NextBlock(0) {
+ // ignore any sub-subdirectives that might
+ // have the same name somewhere within
+ // the reverse_proxy passthrough tokens
+ if dispenser.Nesting() != 1 {
+ continue
+ }
+
+ // parse the forward_auth subdirectives
+ switch dispenser.Val() {
+ case "uri":
+ if !dispenser.NextArg() {
+ return nil, dispenser.ArgErr()
+ }
+ rpHandler.Rewrite.URI = dispenser.Val()
+ dispenser.Delete()
+ dispenser.Delete()
+
+ case "copy_headers":
+ args := dispenser.RemainingArgs()
+ dispenser.Delete()
+ for _, headerField := range args {
+ dispenser.Delete()
+ headersToCopy = append(headersToCopy, headerField)
+ }
+ if len(headersToCopy) == 0 {
+ return nil, dispenser.ArgErr()
+ }
+ }
+ }
+ }
+
+ // reset the dispenser after we're done so that the reverse_proxy
+ // unmarshaler can read it from the start
+ dispenser.Reset()
+
+ // the auth target URI must not be empty
+ if rpHandler.Rewrite.URI == "" {
+ return nil, dispenser.Errf("the 'uri' subdirective is required")
+ }
+
+ // set up handler for good responses; when a response
+ // has 2xx status, then we will copy some headers from
+ // the response onto the original request, and allow
+ // handling to continue down the middleware chain,
+ // by _not_ executing a terminal handler.
+ goodResponseHandler := caddyhttp.ResponseHandler{
+ Match: &caddyhttp.ResponseMatcher{
+ StatusCode: []int{2},
+ },
+ Routes: []caddyhttp.Route{},
+ }
+ if len(headersToCopy) > 0 {
+ handler := &headers.Handler{
+ Request: &headers.HeaderOps{
+ Set: http.Header{},
+ },
+ }
+
+ for _, headerField := range headersToCopy {
+ handler.Request.Set[headerField] = []string{
+ "{http.reverse_proxy.header." + headerField + "}",
+ }
+ }
+
+ goodResponseHandler.Routes = append(
+ goodResponseHandler.Routes,
+ caddyhttp.Route{
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
+ handler,
+ "handler",
+ "headers",
+ nil,
+ )},
+ },
+ )
+ }
+ rpHandler.HandleResponse = append(rpHandler.HandleResponse, goodResponseHandler)
+
+ // set up handler for denial responses; when a response
+ // has any other status than 2xx, then we copy the response
+ // back to the client, and terminate handling.
+ denialResponseHandler := caddyhttp.ResponseHandler{
+ Routes: []caddyhttp.Route{
+ {
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
+ &reverseproxy.CopyResponseHeadersHandler{
+ Exclude: []string{
+ "Connection",
+ "Keep-Alive",
+ "Te",
+ "Trailers",
+ "Transfer-Encoding",
+ "Upgrade",
+ },
+ },
+ "handler",
+ "copy_response_headers",
+ nil,
+ )},
+ },
+ {
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
+ &reverseproxy.CopyResponseHandler{},
+ "handler",
+ "copy_response",
+ nil,
+ )},
+ },
+ },
+ }
+ rpHandler.HandleResponse = append(rpHandler.HandleResponse, denialResponseHandler)
+
+ // the rest of the config is specified by the user
+ // using the reverse_proxy directive syntax
+ err = rpHandler.UnmarshalCaddyfile(dispenser)
+ if err != nil {
+ return nil, err
+ }
+ err = rpHandler.FinalizeUnmarshalCaddyfile(h)
+ if err != nil {
+ return nil, err
+ }
+
+ // create the final reverse proxy route
+ rpRoute := caddyhttp.Route{
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
+ rpHandler,
+ "handler",
+ "reverse_proxy",
+ nil,
+ )},
+ }
+
+ // apply the user's matcher if any
+ if userMatcherSet != nil {
+ rpRoute.MatcherSetsRaw = []caddy.ModuleMap{userMatcherSet}
+ }
+
+ return []httpcaddyfile.ConfigValue{
+ {
+ Class: "route",
+ Value: rpRoute,
+ },
+ }, nil
+}