From 393d7ec486dd1fe01da3b1ecf45bd6748c8fe969 Mon Sep 17 00:00:00 2001
From: Jens L <jens.langhammer@beryju.org>
Date: Sat, 30 Jul 2022 17:51:01 +0200
Subject: [PATCH] providers/proxy: no exposed urls (#3151)

* test any callback

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* cleanup

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* dont detect callback in per-server handler

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* use full redirect uri with both path and query param

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* update tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* correctly route to embedded outpost for callback signature

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix allowed redirects

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
---
 authentik/providers/proxy/models.py           |  9 +-
 .../proxyv2/application/application.go        | 34 +++++---
 .../proxyv2/application/mode_common.go        |  9 +-
 .../proxyv2/application/mode_forward.go       | 87 +++----------------
 .../application/mode_forward_caddy_test.go    | 24 +++--
 .../application/mode_forward_envoy_test.go    | 25 ++++--
 .../application/mode_forward_nginx_test.go    |  4 +-
 .../application/mode_forward_traefik_test.go  | 24 +++--
 internal/outpost/proxyv2/application/oauth.go |  7 +-
 internal/outpost/proxyv2/application/test.go  |  5 ++
 internal/web/proxy.go                         |  4 +-
 internal/web/web.go                           |  6 +-
 12 files changed, 117 insertions(+), 121 deletions(-)

diff --git a/authentik/providers/proxy/models.py b/authentik/providers/proxy/models.py
index 8d1aa8b45..38fe5c171 100644
--- a/authentik/providers/proxy/models.py
+++ b/authentik/providers/proxy/models.py
@@ -14,6 +14,7 @@ from authentik.outposts.models import OutpostModel
 from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
 
 SCOPE_AK_PROXY = "ak_proxy"
+OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback"
 
 
 def get_cookie_secret():
@@ -22,7 +23,13 @@ def get_cookie_secret():
 
 
 def _get_callback_url(uri: str) -> str:
-    return urljoin(uri, "outpost.goauthentik.io/callback")
+    return "\n".join(
+        [
+            urljoin(uri, "outpost.goauthentik.io/callback")
+            + f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
+            uri + f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
+        ]
+    )
 
 
 class ProxyMode(models.TextChoices):
diff --git a/internal/outpost/proxyv2/application/application.go b/internal/outpost/proxyv2/application/application.go
index 6b0052d6b..f3af62616 100644
--- a/internal/outpost/proxyv2/application/application.go
+++ b/internal/outpost/proxyv2/application/application.go
@@ -8,6 +8,7 @@ import (
 	"html/template"
 	"net/http"
 	"net/url"
+	"path"
 	"regexp"
 	"strings"
 	"time"
@@ -34,7 +35,7 @@ type Application struct {
 	Cert                 *tls.Certificate
 	UnauthenticatedRegex []*regexp.Regexp
 
-	endpint       OIDCEndpoint
+	endpoint      OIDCEndpoint
 	oauthConfig   oauth2.Config
 	tokenVerifier *oidc.IDTokenVerifier
 	outpostName   string
@@ -72,12 +73,18 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
 		SupportedSigningAlgs: []string{"RS256", "HS256"},
 	})
 
+	redirectUri, _ := url.Parse(p.ExternalHost)
+	redirectUri.Path = path.Join(redirectUri.Path, "/outpost.goauthentik.io/callback")
+	redirectUri.RawQuery = url.Values{
+		CallbackSignature: []string{"true"},
+	}.Encode()
+
 	// Configure an OpenID Connect aware OAuth2 client.
 	endpoint := GetOIDCEndpoint(p, ak.Outpost.Config["authentik_host"].(string))
 	oauth2Config := oauth2.Config{
 		ClientID:     *p.ClientId,
 		ClientSecret: *p.ClientSecret,
-		RedirectURL:  urlJoin(p.ExternalHost, "/outpost.goauthentik.io/callback"),
+		RedirectURL:  redirectUri.String(),
 		Endpoint:     endpoint.Endpoint,
 		Scopes:       p.ScopesToRequest,
 	}
@@ -86,7 +93,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
 		Host:           externalHost.Host,
 		log:            muxLogger,
 		outpostName:    ak.Outpost.Name,
-		endpint:        endpoint,
+		endpoint:       endpoint,
 		oauthConfig:    oauth2Config,
 		tokenVerifier:  verifier,
 		proxyConfig:    p,
@@ -139,11 +146,18 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
 		})
 	})
 	mux.Use(sentryhttp.New(sentryhttp.Options{}).Handle)
+	mux.Use(func(inner http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			if _, set := r.URL.Query()[CallbackSignature]; set {
+				a.handleAuthCallback(w, r)
+			} else {
+				inner.ServeHTTP(w, r)
+			}
+		})
+	})
 
-	// Support /start and /sign_in for backwards compatibility
-	mux.HandleFunc("/outpost.goauthentik.io/start", a.handleRedirect)
-	mux.HandleFunc("/outpost.goauthentik.io/sign_in", a.handleRedirect)
-	mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleCallback)
+	mux.HandleFunc("/outpost.goauthentik.io/start", a.handleAuthStart)
+	mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleAuthCallback)
 	mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut)
 	switch *p.Mode.Get() {
 	case api.PROXYMODE_PROXY:
@@ -197,14 +211,14 @@ func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) {
 	//TODO: Token revocation
 	s, err := a.sessions.Get(r, constants.SessionName)
 	if err != nil {
-		http.Redirect(rw, r, a.endpint.EndSessionEndpoint, http.StatusFound)
+		http.Redirect(rw, r, a.endpoint.EndSessionEndpoint, http.StatusFound)
 		return
 	}
 	s.Options.MaxAge = -1
 	err = s.Save(r, rw)
 	if err != nil {
-		http.Redirect(rw, r, a.endpint.EndSessionEndpoint, http.StatusFound)
+		http.Redirect(rw, r, a.endpoint.EndSessionEndpoint, http.StatusFound)
 		return
 	}
-	http.Redirect(rw, r, a.endpint.EndSessionEndpoint, http.StatusFound)
+	http.Redirect(rw, r, a.endpoint.EndSessionEndpoint, http.StatusFound)
 }
diff --git a/internal/outpost/proxyv2/application/mode_common.go b/internal/outpost/proxyv2/application/mode_common.go
index c5a95c524..8a0a61d21 100644
--- a/internal/outpost/proxyv2/application/mode_common.go
+++ b/internal/outpost/proxyv2/application/mode_common.go
@@ -13,8 +13,6 @@ import (
 	"goauthentik.io/internal/constants"
 )
 
-var hasReportedMisconfiguration = false
-
 func (a *Application) addHeaders(headers http.Header, c *Claims) {
 	// https://goauthentik.io/docs/providers/proxy/proxy
 
@@ -26,7 +24,7 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) {
 	headers.Set("X-authentik-jwt", c.RawToken)
 
 	// System headers
-	headers.Set("X-authentik-meta-jwks", a.endpint.JwksUri)
+	headers.Set("X-authentik-meta-jwks", a.endpoint.JwksUri)
 	headers.Set("X-authentik-meta-outpost", a.outpostName)
 	headers.Set("X-authentik-meta-provider", a.proxyConfig.Name)
 	headers.Set("X-authentik-meta-app", a.proxyConfig.AssignedApplicationSlug)
@@ -105,9 +103,6 @@ func (a *Application) getNginxForwardUrl(r *http.Request) (*url.URL, error) {
 func (a *Application) ReportMisconfiguration(r *http.Request, msg string, fields map[string]interface{}) {
 	fields["message"] = msg
 	a.log.WithFields(fields).Error("Reporting configuration error")
-	if hasReportedMisconfiguration {
-		return
-	}
 	req := api.EventRequest{
 		Action:   api.EVENTACTIONS_CONFIGURATION_ERROR,
 		App:      "authentik.providers.proxy", // must match python apps.py name
@@ -117,8 +112,6 @@ func (a *Application) ReportMisconfiguration(r *http.Request, msg string, 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")
-	} else {
-		hasReportedMisconfiguration = true
 	}
 }
 
diff --git a/internal/outpost/proxyv2/application/mode_forward.go b/internal/outpost/proxyv2/application/mode_forward.go
index 3f4d7dbc2..6a1761738 100644
--- a/internal/outpost/proxyv2/application/mode_forward.go
+++ b/internal/outpost/proxyv2/application/mode_forward.go
@@ -3,12 +3,9 @@ package application
 import (
 	"fmt"
 	"net/http"
-	"net/url"
 	"strings"
 
-	"goauthentik.io/api/v3"
 	"goauthentik.io/internal/outpost/proxyv2/constants"
-	"goauthentik.io/internal/utils/web"
 )
 
 const (
@@ -40,7 +37,7 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
 		http.Error(rw, "configuration error", http.StatusInternalServerError)
 		return
 	}
-
+	// Check if we're authenticated, or the request path is on the allowlist
 	claims, err := a.getClaims(r)
 	if claims != nil && err == nil {
 		a.addHeaders(rw.Header(), claims)
@@ -51,22 +48,9 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
 		a.log.Trace("path can be accessed without authentication")
 		return
 	}
-	if strings.HasPrefix(r.Header.Get("X-Forwarded-Uri"), "/outpost.goauthentik.io") {
-		a.log.WithField("url", r.URL.String()).Trace("path begins with /outpost.goauthentik.io, allowing access")
-		return
-	}
-	host := ""
-	// Optional suffix, which is appended to the URL
-	if *a.proxyConfig.Mode.Get() == api.PROXYMODE_FORWARD_SINGLE {
-		host = web.GetHost(r)
-	} else if *a.proxyConfig.Mode.Get() == api.PROXYMODE_FORWARD_DOMAIN {
-		eh, err := url.Parse(a.proxyConfig.ExternalHost)
-		if err != nil {
-			a.log.WithField("host", a.proxyConfig.ExternalHost).WithError(err).Warning("invalid external_host")
-		} else {
-			host = eh.Host
-		}
-	}
+	tr := r.Clone(r.Context())
+	tr.URL = fwd
+	a.handleAuthStart(rw, r)
 	// set the redirect flag to the current URL we have, since we redirect
 	// to a (possibly) different domain, but we want to be redirected back
 	// to the application
@@ -76,17 +60,9 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
 		s.Values[constants.SessionRedirect] = fwd.String()
 		err = s.Save(r, rw)
 		if err != nil {
-			a.log.WithError(err).Warning("failed to save session before redirect")
+			a.log.WithError(err).Warning("failed to save session")
 		}
 	}
-
-	proto := r.Header.Get("X-Forwarded-Proto")
-	if proto != "" {
-		proto = proto + ":"
-	}
-	rdFinal := fmt.Sprintf("%s//%s%s", proto, host, "/outpost.goauthentik.io/start")
-	a.log.WithField("url", rdFinal).Debug("Redirecting to login")
-	http.Redirect(rw, r, rdFinal, http.StatusTemporaryRedirect)
 }
 
 func (a *Application) forwardHandleCaddy(rw http.ResponseWriter, r *http.Request) {
@@ -103,7 +79,7 @@ func (a *Application) forwardHandleCaddy(rw http.ResponseWriter, r *http.Request
 		http.Error(rw, "configuration error", http.StatusInternalServerError)
 		return
 	}
-
+	// Check if we're authenticated, or the request path is on the allowlist
 	claims, err := a.getClaims(r)
 	if claims != nil && err == nil {
 		a.addHeaders(rw.Header(), claims)
@@ -114,22 +90,9 @@ func (a *Application) forwardHandleCaddy(rw http.ResponseWriter, r *http.Request
 		a.log.Trace("path can be accessed without authentication")
 		return
 	}
-	if strings.HasPrefix(r.Header.Get("X-Forwarded-Uri"), "/outpost.goauthentik.io") {
-		a.log.WithField("url", r.URL.String()).Trace("path begins with /outpost.goauthentik.io, allowing access")
-		return
-	}
-	host := ""
-	// Optional suffix, which is appended to the URL
-	if *a.proxyConfig.Mode.Get() == api.PROXYMODE_FORWARD_SINGLE {
-		host = web.GetHost(r)
-	} else if *a.proxyConfig.Mode.Get() == api.PROXYMODE_FORWARD_DOMAIN {
-		eh, err := url.Parse(a.proxyConfig.ExternalHost)
-		if err != nil {
-			a.log.WithField("host", a.proxyConfig.ExternalHost).WithError(err).Warning("invalid external_host")
-		} else {
-			host = eh.Host
-		}
-	}
+	tr := r.Clone(r.Context())
+	tr.URL = fwd
+	a.handleAuthStart(rw, r)
 	// set the redirect flag to the current URL we have, since we redirect
 	// to a (possibly) different domain, but we want to be redirected back
 	// to the application
@@ -139,17 +102,9 @@ func (a *Application) forwardHandleCaddy(rw http.ResponseWriter, r *http.Request
 		s.Values[constants.SessionRedirect] = fwd.String()
 		err = s.Save(r, rw)
 		if err != nil {
-			a.log.WithError(err).Warning("failed to save session before redirect")
+			a.log.WithError(err).Warning("failed to save session")
 		}
 	}
-
-	proto := r.Header.Get("X-Forwarded-Proto")
-	if proto != "" {
-		proto = proto + ":"
-	}
-	rdFinal := fmt.Sprintf("%s//%s%s", proto, host, "/outpost.goauthentik.io/start")
-	a.log.WithField("url", rdFinal).Debug("Redirecting to login")
-	http.Redirect(rw, r, rdFinal, http.StatusTemporaryRedirect)
 }
 
 func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) {
@@ -200,7 +155,7 @@ func (a *Application) forwardHandleEnvoy(rw http.ResponseWriter, r *http.Request
 	a.log.WithField("header", r.Header).Trace("tracing headers for debug")
 	r.URL.Path = strings.TrimPrefix(r.URL.Path, envoyPrefix)
 	fwd := r.URL
-
+	// Check if we're authenticated, or the request path is on the allowlist
 	claims, err := a.getClaims(r)
 	if claims != nil && err == nil {
 		a.addHeaders(rw.Header(), claims)
@@ -211,22 +166,7 @@ func (a *Application) forwardHandleEnvoy(rw http.ResponseWriter, r *http.Request
 		a.log.Trace("path can be accessed without authentication")
 		return
 	}
-	if strings.HasPrefix(r.URL.Path, "/outpost.goauthentik.io") {
-		a.log.WithField("url", r.URL.String()).Trace("path begins with /outpost.goauthentik.io, allowing access")
-		return
-	}
-	host := ""
-	// Optional suffix, which is appended to the URL
-	if *a.proxyConfig.Mode.Get() == api.PROXYMODE_FORWARD_SINGLE {
-		host = web.GetHost(r)
-	} else if *a.proxyConfig.Mode.Get() == api.PROXYMODE_FORWARD_DOMAIN {
-		eh, err := url.Parse(a.proxyConfig.ExternalHost)
-		if err != nil {
-			a.log.WithField("host", a.proxyConfig.ExternalHost).WithError(err).Warning("invalid external_host")
-		} else {
-			host = eh.Host
-		}
-	}
+	a.handleAuthStart(rw, r)
 	// set the redirect flag to the current URL we have, since we redirect
 	// to a (possibly) different domain, but we want to be redirected back
 	// to the application
@@ -239,7 +179,4 @@ func (a *Application) forwardHandleEnvoy(rw http.ResponseWriter, r *http.Request
 			a.log.WithError(err).Warning("failed to save session before redirect")
 		}
 	}
-	rdFinal := fmt.Sprintf("//%s%s", host, "/outpost.goauthentik.io/start")
-	a.log.WithField("url", rdFinal).Debug("Redirecting to login")
-	http.Redirect(rw, r, rdFinal, http.StatusTemporaryRedirect)
 }
diff --git a/internal/outpost/proxyv2/application/mode_forward_caddy_test.go b/internal/outpost/proxyv2/application/mode_forward_caddy_test.go
index f8c3778af..9942eec0b 100644
--- a/internal/outpost/proxyv2/application/mode_forward_caddy_test.go
+++ b/internal/outpost/proxyv2/application/mode_forward_caddy_test.go
@@ -1,8 +1,10 @@
 package application
 
 import (
+	"fmt"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -43,11 +45,16 @@ func TestForwardHandleCaddy_Single_Headers(t *testing.T) {
 	rr := httptest.NewRecorder()
 	a.forwardHandleCaddy(rr, req)
 
-	assert.Equal(t, rr.Code, http.StatusTemporaryRedirect)
+	assert.Equal(t, http.StatusFound, rr.Code)
 	loc, _ := rr.Result().Location()
-	assert.Equal(t, loc.String(), "http://test.goauthentik.io/outpost.goauthentik.io/start")
-
 	s, _ := a.sessions.Get(req, constants.SessionName)
+	shouldUrl := url.Values{
+		"client_id":     []string{*a.proxyConfig.ClientId},
+		"redirect_uri":  []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
+		"response_type": []string{"code"},
+		"state":         []string{s.Values[constants.SessionOAuthState].([]string)[0]},
+	}
+	assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
 }
 
@@ -123,10 +130,15 @@ func TestForwardHandleCaddy_Domain_Header(t *testing.T) {
 	rr := httptest.NewRecorder()
 	a.forwardHandleCaddy(rr, req)
 
-	assert.Equal(t, http.StatusTemporaryRedirect, rr.Code)
+	assert.Equal(t, http.StatusFound, rr.Code)
 	loc, _ := rr.Result().Location()
-	assert.Equal(t, "http://auth.test.goauthentik.io/outpost.goauthentik.io/start", loc.String())
-
 	s, _ := a.sessions.Get(req, constants.SessionName)
+	shouldUrl := url.Values{
+		"client_id":     []string{*a.proxyConfig.ClientId},
+		"redirect_uri":  []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
+		"response_type": []string{"code"},
+		"state":         []string{s.Values[constants.SessionOAuthState].([]string)[0]},
+	}
+	assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
 }
diff --git a/internal/outpost/proxyv2/application/mode_forward_envoy_test.go b/internal/outpost/proxyv2/application/mode_forward_envoy_test.go
index 3f5c3a488..30d75f3da 100644
--- a/internal/outpost/proxyv2/application/mode_forward_envoy_test.go
+++ b/internal/outpost/proxyv2/application/mode_forward_envoy_test.go
@@ -1,8 +1,10 @@
 package application
 
 import (
+	"fmt"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -27,11 +29,16 @@ func TestForwardHandleEnvoy_Single_Headers(t *testing.T) {
 	rr := httptest.NewRecorder()
 	a.forwardHandleEnvoy(rr, req)
 
-	assert.Equal(t, rr.Code, http.StatusTemporaryRedirect)
+	assert.Equal(t, http.StatusFound, rr.Code)
 	loc, _ := rr.Result().Location()
-	assert.Equal(t, loc.String(), "//test.goauthentik.io/outpost.goauthentik.io/start")
-
 	s, _ := a.sessions.Get(req, constants.SessionName)
+	shouldUrl := url.Values{
+		"client_id":     []string{*a.proxyConfig.ClientId},
+		"redirect_uri":  []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
+		"response_type": []string{"code"},
+		"state":         []string{s.Values[constants.SessionOAuthState].([]string)[0]},
+	}
+	assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
 }
 
@@ -89,10 +96,16 @@ func TestForwardHandleEnvoy_Domain_Header(t *testing.T) {
 	rr := httptest.NewRecorder()
 	a.forwardHandleEnvoy(rr, req)
 
-	assert.Equal(t, http.StatusTemporaryRedirect, rr.Code)
+	assert.Equal(t, http.StatusFound, rr.Code)
 	loc, _ := rr.Result().Location()
-	assert.Equal(t, "//auth.test.goauthentik.io/outpost.goauthentik.io/start", loc.String())
-
 	s, _ := a.sessions.Get(req, constants.SessionName)
+
+	shouldUrl := url.Values{
+		"client_id":     []string{*a.proxyConfig.ClientId},
+		"redirect_uri":  []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
+		"response_type": []string{"code"},
+		"state":         []string{s.Values[constants.SessionOAuthState].([]string)[0]},
+	}
+	assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
 }
diff --git a/internal/outpost/proxyv2/application/mode_forward_nginx_test.go b/internal/outpost/proxyv2/application/mode_forward_nginx_test.go
index a23a0be71..434a18ee0 100644
--- a/internal/outpost/proxyv2/application/mode_forward_nginx_test.go
+++ b/internal/outpost/proxyv2/application/mode_forward_nginx_test.go
@@ -39,7 +39,7 @@ func TestForwardHandleNginx_Single_Headers(t *testing.T) {
 	rr := httptest.NewRecorder()
 	a.forwardHandleNginx(rr, req)
 
-	assert.Equal(t, rr.Code, http.StatusUnauthorized)
+	assert.Equal(t, http.StatusUnauthorized, rr.Code)
 
 	s, _ := a.sessions.Get(req, constants.SessionName)
 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
@@ -53,7 +53,7 @@ func TestForwardHandleNginx_Single_URI(t *testing.T) {
 	rr := httptest.NewRecorder()
 	a.forwardHandleNginx(rr, req)
 
-	assert.Equal(t, rr.Code, http.StatusUnauthorized)
+	assert.Equal(t, http.StatusUnauthorized, rr.Code)
 
 	s, _ := a.sessions.Get(req, constants.SessionName)
 	assert.Equal(t, "/app", s.Values[constants.SessionRedirect])
diff --git a/internal/outpost/proxyv2/application/mode_forward_traefik_test.go b/internal/outpost/proxyv2/application/mode_forward_traefik_test.go
index 8d22b12ed..87598b6c4 100644
--- a/internal/outpost/proxyv2/application/mode_forward_traefik_test.go
+++ b/internal/outpost/proxyv2/application/mode_forward_traefik_test.go
@@ -1,8 +1,10 @@
 package application
 
 import (
+	"fmt"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -43,11 +45,16 @@ func TestForwardHandleTraefik_Single_Headers(t *testing.T) {
 	rr := httptest.NewRecorder()
 	a.forwardHandleTraefik(rr, req)
 
-	assert.Equal(t, rr.Code, http.StatusTemporaryRedirect)
+	assert.Equal(t, http.StatusFound, rr.Code)
 	loc, _ := rr.Result().Location()
-	assert.Equal(t, loc.String(), "http://test.goauthentik.io/outpost.goauthentik.io/start")
-
 	s, _ := a.sessions.Get(req, constants.SessionName)
+	shouldUrl := url.Values{
+		"client_id":     []string{*a.proxyConfig.ClientId},
+		"redirect_uri":  []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
+		"response_type": []string{"code"},
+		"state":         []string{s.Values[constants.SessionOAuthState].([]string)[0]},
+	}
+	assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
 }
 
@@ -123,10 +130,15 @@ func TestForwardHandleTraefik_Domain_Header(t *testing.T) {
 	rr := httptest.NewRecorder()
 	a.forwardHandleTraefik(rr, req)
 
-	assert.Equal(t, http.StatusTemporaryRedirect, rr.Code)
+	assert.Equal(t, http.StatusFound, rr.Code)
 	loc, _ := rr.Result().Location()
-	assert.Equal(t, "http://auth.test.goauthentik.io/outpost.goauthentik.io/start", loc.String())
-
 	s, _ := a.sessions.Get(req, constants.SessionName)
+	shouldUrl := url.Values{
+		"client_id":     []string{*a.proxyConfig.ClientId},
+		"redirect_uri":  []string{"https://ext.t.goauthentik.io/outpost.goauthentik.io/callback?X-authentik-auth-callback=true"},
+		"response_type": []string{"code"},
+		"state":         []string{s.Values[constants.SessionOAuthState].([]string)[0]},
+	}
+	assert.Equal(t, fmt.Sprintf("http://fake-auth.t.goauthentik.io/auth?%s", shouldUrl.Encode()), loc.String())
 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
 }
diff --git a/internal/outpost/proxyv2/application/oauth.go b/internal/outpost/proxyv2/application/oauth.go
index cf87826de..81928781d 100644
--- a/internal/outpost/proxyv2/application/oauth.go
+++ b/internal/outpost/proxyv2/application/oauth.go
@@ -13,7 +13,8 @@ import (
 )
 
 const (
-	redirectParam = "rd"
+	redirectParam     = "rd"
+	CallbackSignature = "X-authentik-auth-callback"
 )
 
 func (a *Application) checkRedirectParam(r *http.Request) (string, bool) {
@@ -41,7 +42,7 @@ func (a *Application) checkRedirectParam(r *http.Request) (string, bool) {
 	return u.String(), true
 }
 
-func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) {
+func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request) {
 	newState := base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
 	s, err := a.sessions.Get(r, constants.SessionName)
 	if err != nil {
@@ -65,7 +66,7 @@ func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) {
 	http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(newState), http.StatusFound)
 }
 
-func (a *Application) handleCallback(rw http.ResponseWriter, r *http.Request) {
+func (a *Application) handleAuthCallback(rw http.ResponseWriter, r *http.Request) {
 	s, err := a.sessions.Get(r, constants.SessionName)
 	if err != nil {
 		a.log.WithError(err).Trace("failed to get session")
diff --git a/internal/outpost/proxyv2/application/test.go b/internal/outpost/proxyv2/application/test.go
index 74787114a..3bb93591c 100644
--- a/internal/outpost/proxyv2/application/test.go
+++ b/internal/outpost/proxyv2/application/test.go
@@ -22,6 +22,11 @@ func newTestApplication() *Application {
 			BasicAuthEnabled:           api.PtrBool(true),
 			BasicAuthUserAttribute:     api.PtrString("username"),
 			BasicAuthPasswordAttribute: api.PtrString("password"),
+			OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{
+				AuthorizationEndpoint: "http://fake-auth.t.goauthentik.io/auth",
+				TokenEndpoint:         "http://fake-auth.t.goauthentik.io/token",
+				UserinfoEndpoint:      "http://fake-auth.t.goauthentik.io/userinfo",
+			},
 		},
 		http.DefaultClient,
 		nil,
diff --git a/internal/web/proxy.go b/internal/web/proxy.go
index 24364e2e4..b7ebce92a 100644
--- a/internal/web/proxy.go
+++ b/internal/web/proxy.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/prometheus/client_golang/prometheus"
+	"goauthentik.io/internal/outpost/proxyv2/application"
 	"goauthentik.io/internal/utils/sentry"
 	"goauthentik.io/internal/utils/web"
 )
@@ -52,7 +53,8 @@ func (ws *WebServer) configureProxy() {
 		}
 		before := time.Now()
 		if ws.ProxyServer != nil {
-			if ws.ProxyServer.HandleHost(rw, r) {
+			_, oauthCallbackSet := r.URL.Query()[application.CallbackSignature]
+			if ws.ProxyServer.HandleHost(rw, r) || oauthCallbackSet {
 				Requests.With(prometheus.Labels{
 					"dest": "embedded_outpost",
 				}).Observe(float64(time.Since(before)))
diff --git a/internal/web/web.go b/internal/web/web.go
index 50198dd6e..86a5d7f8e 100644
--- a/internal/web/web.go
+++ b/internal/web/web.go
@@ -39,12 +39,12 @@ func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
 	mainHandler.Use(sentryhttp.New(sentryhttp.Options{}).Handle)
 	mainHandler.Use(handlers.ProxyHeaders)
 	mainHandler.Use(handlers.CompressHandler)
-	logginRouter := mainHandler.NewRoute().Subrouter()
-	logginRouter.Use(web.NewLoggingHandler(l, nil))
+	loggingHandler := mainHandler.NewRoute().Subrouter()
+	loggingHandler.Use(web.NewLoggingHandler(l, nil))
 
 	ws := &WebServer{
 		m:   mainHandler,
-		lh:  logginRouter,
+		lh:  loggingHandler,
 		log: l,
 		p:   g,
 	}