Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion authentik/providers/proxy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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():
Expand All @@ -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):
Expand Down
34 changes: 24 additions & 10 deletions internal/outpost/proxyv2/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"html/template"
"net/http"
"net/url"
"path"
"regexp"
"strings"
"time"
Expand All @@ -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
Expand Down Expand Up @@ -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,
}
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
}
9 changes: 1 addition & 8 deletions internal/outpost/proxyv2/application/mode_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}

Expand Down
87 changes: 12 additions & 75 deletions internal/outpost/proxyv2/application/mode_forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
}
24 changes: 18 additions & 6 deletions internal/outpost/proxyv2/application/mode_forward_caddy_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package application

import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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])
}

Expand Down Expand Up @@ -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])
}
Loading