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:
parent
79ec872232
commit
ebb5711c32
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue