providers/proxy: add support for X-Original-URI in nginx, better handle missing headers and report errors to authentik

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2022-01-27 18:14:02 +01:00
parent 79ec872232
commit ebb5711c32
7 changed files with 98 additions and 39 deletions

View File

@ -46,6 +46,7 @@ type Application struct {
log *log.Entry log *log.Entry
mux *mux.Router mux *mux.Router
ak *ak.APIController
errorTemplates *template.Template errorTemplates *template.Template
} }
@ -93,6 +94,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
httpClient: c, httpClient: c,
mux: mux, mux: mux,
errorTemplates: templates.GetTemplates(), errorTemplates: templates.GetTemplates(),
ak: ak,
} }
a.sessions = a.getStore(p) a.sessions = a.getStore(p)
mux.Use(web.NewLoggingHandler(muxLogger, func(l *log.Entry, r *http.Request) *log.Entry { mux.Use(web.NewLoggingHandler(muxLogger, func(l *log.Entry, r *http.Request) *log.Entry {

View File

@ -1,6 +1,7 @@
package application package application
import ( import (
"context"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@ -59,7 +60,7 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) {
} }
// getTraefikForwardUrl See https://doc.traefik.io/traefik/middlewares/forwardauth/ // getTraefikForwardUrl See https://doc.traefik.io/traefik/middlewares/forwardauth/
func (a *Application) getTraefikForwardUrl(r *http.Request) *url.URL { func (a *Application) getTraefikForwardUrl(r *http.Request) (*url.URL, error) {
u, err := url.Parse(fmt.Sprintf( u, err := url.Parse(fmt.Sprintf(
"%s://%s%s", "%s://%s%s",
r.Header.Get("X-Forwarded-Proto"), r.Header.Get("X-Forwarded-Proto"),
@ -67,27 +68,47 @@ func (a *Application) getTraefikForwardUrl(r *http.Request) *url.URL {
r.Header.Get("X-Forwarded-Uri"), r.Header.Get("X-Forwarded-Uri"),
)) ))
if err != nil { if err != nil {
a.log.WithError(err).Warning("Failed to parse URL from traefik") return nil, err
return r.URL
} }
a.log.WithField("url", u.String()).Trace("traefik forwarded url") a.log.WithField("url", u.String()).Trace("traefik forwarded url")
return u return u, nil
} }
// getNginxForwardUrl See https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/template/nginx.tmpl // getNginxForwardUrl See https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/template/nginx.tmpl
func (a *Application) getNginxForwardUrl(r *http.Request) *url.URL { func (a *Application) getNginxForwardUrl(r *http.Request) (*url.URL, error) {
ou := r.Header.Get("X-Original-URI")
if ou != "" {
u, _ := url.Parse(r.URL.String())
u.Path = ou
a.log.WithField("url", u.String()).Info("building forward URL from X-Original-URI")
return u, nil
}
h := r.Header.Get("X-Original-URL") h := r.Header.Get("X-Original-URL")
if len(h) < 1 { if len(h) < 1 {
a.log.WithError(errors.New("blank URL")).Warning("blank URL") return nil, errors.New("no forward URL found")
return r.URL
} }
u, err := url.Parse(h) u, err := url.Parse(h)
if err != nil { if err != nil {
a.log.WithError(err).Warning("failed to parse URL from nginx") a.log.WithError(err).Warning("failed to parse URL from nginx")
return r.URL return nil, err
} }
a.log.WithField("url", u.String()).Trace("nginx forwarded url") a.log.WithField("url", u.String()).Trace("nginx forwarded url")
return u return u, nil
}
func (a *Application) ReportMisconfiguration(r *http.Request, msg string, fields map[string]interface{}) {
fields["message"] = msg
a.log.WithFields(fields).Error("Reporting configuration error")
req := api.EventRequest{
Action: api.EVENTACTIONS_CONFIGURATION_ERROR,
App: "authentik.providers.proxy", // must match python apps.py name
ClientIp: *api.NewNullableString(api.PtrString(r.RemoteAddr)),
Context: &fields,
}
_, _, err := a.ak.Client.EventsApi.EventsEventsCreate(context.Background()).EventRequest(req).Execute()
if err != nil {
a.log.WithError(err).Warning("failed to report configuration error")
}
} }
func (a *Application) IsAllowlisted(u *url.URL) bool { func (a *Application) IsAllowlisted(u *url.URL) bool {

View File

@ -26,7 +26,19 @@ func (a *Application) configureForward() error {
func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Request) { func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Request) {
a.log.WithField("header", r.Header).Trace("tracing headers for debug") a.log.WithField("header", r.Header).Trace("tracing headers for debug")
fwd := a.getTraefikForwardUrl(r) // First check if we've got everything we need
fwd, err := a.getTraefikForwardUrl(r)
if err != nil {
a.ReportMisconfiguration(r, fmt.Sprintf("Outpost %s (Provider %s) failed to detect a forward URL from Traefik", a.outpostName, a.proxyConfig.Name), map[string]interface{}{
"provider": a.proxyConfig.Name,
"outpost": a.outpostName,
"url": r.URL.String(),
"headers": cleanseHeaders(r.Header),
})
http.Error(rw, "configuration error", http.StatusInternalServerError)
return
}
claims, err := a.getClaims(r) claims, err := a.getClaims(r)
if claims != nil && err == nil { if claims != nil && err == nil {
a.addHeaders(rw.Header(), claims) a.addHeaders(rw.Header(), claims)
@ -75,7 +87,18 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) { func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) {
a.log.WithField("header", r.Header).Trace("tracing headers for debug") a.log.WithField("header", r.Header).Trace("tracing headers for debug")
fwd := a.getNginxForwardUrl(r) fwd, err := a.getNginxForwardUrl(r)
if err != nil {
a.ReportMisconfiguration(r, fmt.Sprintf("Outpost %s (Provider %s) failed to detect a forward URL from nginx", a.outpostName, a.proxyConfig.Name), map[string]interface{}{
"provider": a.proxyConfig.Name,
"outpost": a.outpostName,
"url": r.URL.String(),
"headers": cleanseHeaders(r.Header),
})
http.Error(rw, "configuration error", http.StatusInternalServerError)
return
}
claims, err := a.getClaims(r) claims, err := a.getClaims(r)
if claims != nil && err == nil { if claims != nil && err == nil {
a.addHeaders(rw.Header(), claims) a.addHeaders(rw.Header(), claims)

View File

@ -17,7 +17,7 @@ func TestForwardHandleNginx_Single_Blank(t *testing.T) {
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req) a.forwardHandleNginx(rr, req)
assert.Equal(t, http.StatusUnauthorized, rr.Code) assert.Equal(t, http.StatusInternalServerError, rr.Code)
} }
func TestForwardHandleNginx_Single_Skip(t *testing.T) { func TestForwardHandleNginx_Single_Skip(t *testing.T) {
@ -45,9 +45,24 @@ func TestForwardHandleNginx_Single_Headers(t *testing.T) {
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
} }
func TestForwardHandleNginx_Single_URI(t *testing.T) {
a := newTestApplication()
req, _ := http.NewRequest("GET", "https://foo.bar/akprox/auth/nginx", nil)
req.Header.Set("X-Original-URI", "/app")
rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req)
assert.Equal(t, rr.Code, http.StatusUnauthorized)
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "https://foo.bar/app", s.Values[constants.SessionRedirect])
}
func TestForwardHandleNginx_Single_Claims(t *testing.T) { func TestForwardHandleNginx_Single_Claims(t *testing.T) {
a := newTestApplication() a := newTestApplication()
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
req.Header.Set("X-Original-URI", "/")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req) a.forwardHandleNginx(rr, req)
@ -98,10 +113,7 @@ func TestForwardHandleNginx_Domain_Blank(t *testing.T) {
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
a.forwardHandleNginx(rr, req) a.forwardHandleNginx(rr, req)
assert.Equal(t, http.StatusUnauthorized, rr.Code) assert.Equal(t, http.StatusInternalServerError, rr.Code)
s, _ := a.sessions.Get(req, constants.SeesionName)
assert.Equal(t, "/akprox/auth/nginx", s.Values[constants.SessionRedirect])
} }
func TestForwardHandleNginx_Domain_Header(t *testing.T) { func TestForwardHandleNginx_Domain_Header(t *testing.T) {

View File

@ -17,13 +17,7 @@ func TestForwardHandleTraefik_Single_Blank(t *testing.T) {
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
a.forwardHandleTraefik(rr, req) a.forwardHandleTraefik(rr, req)
assert.Equal(t, http.StatusTemporaryRedirect, rr.Code) assert.Equal(t, http.StatusInternalServerError, rr.Code)
loc, _ := rr.Result().Location()
assert.Equal(t, "/akprox/start", loc.String())
s, _ := a.sessions.Get(req, constants.SeesionName)
// Since we're not setting the traefik specific headers, expect it to redirect to the auth URL
assert.Equal(t, "/akprox/auth/traefik", s.Values[constants.SessionRedirect])
} }
func TestForwardHandleTraefik_Single_Skip(t *testing.T) { func TestForwardHandleTraefik_Single_Skip(t *testing.T) {
@ -60,6 +54,9 @@ func TestForwardHandleTraefik_Single_Headers(t *testing.T) {
func TestForwardHandleTraefik_Single_Claims(t *testing.T) { func TestForwardHandleTraefik_Single_Claims(t *testing.T) {
a := newTestApplication() a := newTestApplication()
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
req.Header.Set("X-Forwarded-Proto", "http")
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
req.Header.Set("X-Forwarded-Uri", "/app")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
a.forwardHandleTraefik(rr, req) a.forwardHandleTraefik(rr, req)
@ -110,13 +107,7 @@ func TestForwardHandleTraefik_Domain_Blank(t *testing.T) {
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
a.forwardHandleTraefik(rr, req) a.forwardHandleTraefik(rr, req)
assert.Equal(t, http.StatusTemporaryRedirect, rr.Code) assert.Equal(t, http.StatusInternalServerError, rr.Code)
loc, _ := rr.Result().Location()
assert.Equal(t, "/akprox/start", loc.String())
s, _ := a.sessions.Get(req, constants.SeesionName)
// Since we're not setting the traefik specific headers, expect it to redirect to the auth URL
assert.Equal(t, "/akprox/auth/traefik", s.Values[constants.SessionRedirect])
} }
func TestForwardHandleTraefik_Domain_Header(t *testing.T) { func TestForwardHandleTraefik_Domain_Header(t *testing.T) {

View File

@ -87,3 +87,13 @@ func contains(s []string, e string) bool {
} }
return false return false
} }
func cleanseHeaders(headers http.Header) map[string]string {
h := make(map[string]string)
for hk, hv := range headers {
if len(hv) > 0 {
h[hk] = hv[0]
}
}
return h
}

View File

@ -99,14 +99,14 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
h.handler.ServeHTTP(responseLogger, req) h.handler.ServeHTTP(responseLogger, req)
duration := float64(time.Since(t)) / float64(time.Millisecond) duration := float64(time.Since(t)) / float64(time.Millisecond)
h.afterHandler(h.logger.WithFields(log.Fields{ h.afterHandler(h.logger.WithFields(log.Fields{
"remote": req.RemoteAddr, "remote": req.RemoteAddr,
"host": GetHost(req), "host": GetHost(req),
"request_protocol": req.Proto, "runtime": fmt.Sprintf("%0.3f", duration),
"runtime": fmt.Sprintf("%0.3f", duration), "method": req.Method,
"method": req.Method, "scheme": req.URL.Scheme,
"size": responseLogger.Size(), "size": responseLogger.Size(),
"status": responseLogger.Status(), "status": responseLogger.Status(),
"upstream": responseLogger.upstream, "upstream": responseLogger.upstream,
"request_useragent": req.UserAgent(), "user_agent": req.UserAgent(),
}), req).Info(url.RequestURI()) }), req).Info(url.RequestURI())
} }