From fb0a88f2cf489dbb35224f594b5504e2a13fd80c Mon Sep 17 00:00:00 2001 From: Jens L Date: Sat, 18 Mar 2023 18:51:20 +0100 Subject: [PATCH] providers/proxy: rework endpoints logic (#4993) * providers/proxy: rework endpoints logic again...this time with tests and better logic Signed-off-by: Jens Langhammer * fix tests Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- .../outpost/proxyv2/application/endpoint.go | 58 +++++++----- .../proxyv2/application/endpoint_test.go | 88 +++++++++++++++++++ 2 files changed, 124 insertions(+), 22 deletions(-) create mode 100644 internal/outpost/proxyv2/application/endpoint_test.go diff --git a/internal/outpost/proxyv2/application/endpoint.go b/internal/outpost/proxyv2/application/endpoint.go index de6ec3662..8029952b5 100644 --- a/internal/outpost/proxyv2/application/endpoint.go +++ b/internal/outpost/proxyv2/application/endpoint.go @@ -2,7 +2,6 @@ package application import ( "net/url" - "strings" log "github.com/sirupsen/logrus" "goauthentik.io/api/v3" @@ -31,40 +30,55 @@ func updateURL(rawUrl string, scheme string, host string) string { func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bool) OIDCEndpoint { authUrl := p.OidcConfiguration.AuthorizationEndpoint endUrl := p.OidcConfiguration.EndSessionEndpoint - tokenUrl := p.OidcConfiguration.TokenEndpoint - jwksUrl := p.OidcConfiguration.JwksUri issuer := p.OidcConfiguration.Issuer - if config.Get().AuthentikHostBrowser != "" { - authUrl = strings.ReplaceAll(authUrl, authentikHost, config.Get().AuthentikHostBrowser) - endUrl = strings.ReplaceAll(endUrl, authentikHost, config.Get().AuthentikHostBrowser) - jwksUrl = strings.ReplaceAll(jwksUrl, authentikHost, config.Get().AuthentikHostBrowser) - issuer = strings.ReplaceAll(issuer, authentikHost, config.Get().AuthentikHostBrowser) - } ep := OIDCEndpoint{ Endpoint: oauth2.Endpoint{ AuthURL: authUrl, - TokenURL: tokenUrl, + TokenURL: p.OidcConfiguration.TokenEndpoint, AuthStyle: oauth2.AuthStyleInParams, }, EndSessionEndpoint: endUrl, - JwksUri: jwksUrl, + JwksUri: p.OidcConfiguration.JwksUri, TokenIntrospection: p.OidcConfiguration.IntrospectionEndpoint, Issuer: issuer, } - if !embedded { + // For the embedded outpost, we use the configure `authentik_host` for the browser URLs + // and localhost (which is what we've got from the API) for backchannel URLs + // + // For other outposts, when `AUTHENTIK_HOST_BROWSER` is set, we use that for the browser URLs + // and use what we got from the API for backchannel + hostBrowser := config.Get().AuthentikHostBrowser + if !embedded && hostBrowser == "" { return ep } - if authentikHost == "" { - log.Warning("Outpost has localhost/blank API Connection but no authentik_host is configured.") - return ep + var newHost *url.URL + if embedded { + if authentikHost == "" { + log.Warning("Outpost has localhost/blank API Connection but no authentik_host is configured.") + return ep + } + aku, err := url.Parse(authentikHost) + if err != nil { + return ep + } + newHost = aku + } else if hostBrowser != "" { + aku, err := url.Parse(hostBrowser) + if err != nil { + return ep + } + newHost = aku } - aku, err := url.Parse(authentikHost) - if err != nil { - return ep + // Update all browser-accessed URLs to use the new host and scheme + ep.AuthURL = updateURL(authUrl, newHost.Scheme, newHost.Host) + ep.EndSessionEndpoint = updateURL(endUrl, newHost.Scheme, newHost.Host) + // Update issuer to use the same host and scheme, which would normally break as we don't + // change the token URL here, but the token HTTP transport overwrites the Host header + // + // This is only used in embedded outposts as there we can guarantee that the request + // is routed correctly + if embedded { + ep.Issuer = updateURL(ep.Issuer, newHost.Scheme, newHost.Host) } - ep.AuthURL = updateURL(authUrl, aku.Scheme, aku.Host) - ep.EndSessionEndpoint = updateURL(endUrl, aku.Scheme, aku.Host) - ep.JwksUri = updateURL(jwksUrl, aku.Scheme, aku.Host) - ep.Issuer = updateURL(ep.Issuer, aku.Scheme, aku.Host) return ep } diff --git a/internal/outpost/proxyv2/application/endpoint_test.go b/internal/outpost/proxyv2/application/endpoint_test.go new file mode 100644 index 000000000..145226f36 --- /dev/null +++ b/internal/outpost/proxyv2/application/endpoint_test.go @@ -0,0 +1,88 @@ +package application + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "goauthentik.io/api/v3" + "goauthentik.io/internal/config" +) + +func TestEndpointDefault(t *testing.T) { + pc := api.ProxyOutpostConfig{ + OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{ + AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/", + EndSessionEndpoint: "https://test.goauthentik.io/application/o/test-app/end-session/", + IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/", + Issuer: "https://test.goauthentik.io/application/o/test-app/", + JwksUri: "https://test.goauthentik.io/application/o/test-app/jwks/", + TokenEndpoint: "https://test.goauthentik.io/application/o/token/", + }, + } + + ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", false) + // Standard outpost, non embedded + // All URLs should use the host that they get from the config + assert.Equal(t, "https://test.goauthentik.io/application/o/authorize/", ep.AuthURL) + assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL) + assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer) + assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri) + assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint) + assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection) +} + +func TestEndpointAuthentikHostBrowser(t *testing.T) { + c := config.Get() + c.AuthentikHostBrowser = "https://browser.test.goauthentik.io" + defer func() { + c.AuthentikHostBrowser = "" + }() + pc := api.ProxyOutpostConfig{ + OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{ + AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/", + EndSessionEndpoint: "https://test.goauthentik.io/application/o/test-app/end-session/", + IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/", + Issuer: "https://test.goauthentik.io/application/o/test-app/", + JwksUri: "https://test.goauthentik.io/application/o/test-app/jwks/", + TokenEndpoint: "https://test.goauthentik.io/application/o/token/", + UserinfoEndpoint: "https://test.goauthentik.io/application/o/userinfo/", + }, + } + + ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", false) + // Standard outpost, with AUTHENTIK_HOST_BROWSER set + // Only the authorize/end session URLs should be changed + assert.Equal(t, "https://browser.test.goauthentik.io/application/o/authorize/", ep.AuthURL) + assert.Equal(t, "https://browser.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint) + assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL) + assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/", ep.Issuer) + assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri) + assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection) +} + +func TestEndpointEmbedded(t *testing.T) { + pc := api.ProxyOutpostConfig{ + OidcConfiguration: api.ProxyOutpostConfigOidcConfiguration{ + AuthorizationEndpoint: "https://test.goauthentik.io/application/o/authorize/", + EndSessionEndpoint: "https://test.goauthentik.io/application/o/test-app/end-session/", + IntrospectionEndpoint: "https://test.goauthentik.io/application/o/introspect/", + Issuer: "https://test.goauthentik.io/application/o/test-app/", + JwksUri: "https://test.goauthentik.io/application/o/test-app/jwks/", + TokenEndpoint: "https://test.goauthentik.io/application/o/token/", + UserinfoEndpoint: "https://test.goauthentik.io/application/o/userinfo/", + }, + } + + ep := GetOIDCEndpoint(pc, "https://authentik-host.test.goauthentik.io", true) + // Embedded outpost + // Browser URLs should use the config of "authentik_host", everything else can use what's + // received from the API endpoint + // Token URL is an exception since it's sent via a special HTTP transport that overrides the + // HTTP Host header, to make sure it's the same value as the issuer + assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/authorize/", ep.AuthURL) + assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/", ep.Issuer) + assert.Equal(t, "https://test.goauthentik.io/application/o/token/", ep.TokenURL) + assert.Equal(t, "https://test.goauthentik.io/application/o/test-app/jwks/", ep.JwksUri) + assert.Equal(t, "https://authentik-host.test.goauthentik.io/application/o/test-app/end-session/", ep.EndSessionEndpoint) + assert.Equal(t, "https://test.goauthentik.io/application/o/introspect/", ep.TokenIntrospection) +}