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
97 changes: 59 additions & 38 deletions internal/outpost/proxyv2/application/mode_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,62 +14,83 @@ import (
"goauthentik.io/internal/outpost/proxyv2/types"
)

// Attempt to set basic auth based on user's attributes
func (a *Application) setAuthorizationHeader(headers http.Header, c *types.Claims) {
if !*a.proxyConfig.BasicAuthEnabled {
return
}
userAttributes := c.Proxy.UserAttributes
var ok bool
var password string
if password, ok = userAttributes[*a.proxyConfig.BasicAuthPasswordAttribute].(string); !ok {
password = ""
}
// Check if we should use email or a custom attribute as username
var username string
if username, ok = userAttributes[*a.proxyConfig.BasicAuthUserAttribute].(string); !ok {
username = c.Email
func (a *Application) addHeaders(headers http.Header, c *types.Claims) {
nh := a.getHeaders(c)
for key, val := range nh {
headers.Set(key, val)
}
if username == "" && password == "" {
return
a.removeDuplicateUnderscoreHeader(headers)
}

func (a *Application) removeDuplicateUnderscoreHeader(h http.Header) {
for key := range h {
ush := strings.ReplaceAll(key, "_", "-")
if _, ok := h[ush]; !ok {
h.Del(key)
}
}
authVal := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
a.log.WithField("username", username).Trace("setting http basic auth")
headers.Set("Authorization", fmt.Sprintf("Basic %s", authVal))
}

func (a *Application) addHeaders(headers http.Header, c *types.Claims) {
func (a *Application) getHeaders(c *types.Claims) map[string]string {
headers := map[string]string{}
// https://docs.goauthentik.io/add-secure-apps/providers/proxy
headers.Set("X-authentik-username", c.PreferredUsername)
headers.Set("X-authentik-groups", strings.Join(c.Groups, "|"))
headers.Set("X-authentik-entitlements", strings.Join(c.Entitlements, "|"))
headers.Set("X-authentik-email", c.Email)
headers.Set("X-authentik-name", c.Name)
headers.Set("X-authentik-uid", c.Sub)
headers.Set("X-authentik-jwt", c.RawToken)
headers["X-authentik-username"] = c.PreferredUsername
headers["X-authentik-groups"] = strings.Join(c.Groups, "|")
headers["X-authentik-entitlements"] = strings.Join(c.Entitlements, "|")
headers["X-authentik-email"] = c.Email
headers["X-authentik-name"] = c.Name
headers["X-authentik-uid"] = c.Sub
headers["X-authentik-jwt"] = c.RawToken

// System headers
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)
headers.Set("X-authentik-meta-version", constants.UserAgentOutpost())
headers["X-authentik-meta-jwks"] = a.endpoint.JwksUri
headers["X-authentik-meta-outpost"] = a.outpostName
headers["X-authentik-meta-provider"] = a.proxyConfig.Name
headers["X-authentik-meta-app"] = a.proxyConfig.AssignedApplicationSlug
headers["X-authentik-meta-version"] = constants.UserAgentOutpost()

if c.Proxy == nil {
return
return headers
}
if authz := a.setAuthorizationHeader(c); authz != "" {
headers["Authorization"] = authz
}
userAttributes := c.Proxy.UserAttributes
a.setAuthorizationHeader(headers, c)
// Check if user has additional headers set that we should sent
userAttributes := c.Proxy.UserAttributes
if additionalHeaders, ok := userAttributes["additionalHeaders"]; ok {
a.log.WithField("headers", additionalHeaders).Trace("setting additional headers")
if additionalHeaders == nil {
return
return headers
}
for key, value := range additionalHeaders.(map[string]interface{}) {
headers.Set(key, toString(value))
headers[key] = toString(value)
}
}
return headers
}

// Attempt to set basic auth based on user's attributes
func (a *Application) setAuthorizationHeader(c *types.Claims) string {
if !*a.proxyConfig.BasicAuthEnabled {
return ""
}
userAttributes := c.Proxy.UserAttributes
var ok bool
var username string
var password string
if password, ok = userAttributes[*a.proxyConfig.BasicAuthPasswordAttribute].(string); !ok {
password = ""
}
// Check if we should use email or a custom attribute as username
if username, ok = userAttributes[*a.proxyConfig.BasicAuthUserAttribute].(string); !ok {
username = c.Email
}
if password == "" {
return ""
}
authVal := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
a.log.WithField("username", username).Trace("setting http basic auth")
return fmt.Sprintf("Basic %s", authVal)
}

// getTraefikForwardUrl See https://doc.traefik.io/traefik/middlewares/forwardauth/
Expand Down
135 changes: 135 additions & 0 deletions internal/outpost/proxyv2/application/mode_common_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package application

import (
"net/http"
"net/url"
"regexp"
"testing"

"github.com/stretchr/testify/assert"
"goauthentik.io/api/v3"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/proxyv2/types"
)

func urlMustParse(u string) *url.URL {
Expand Down Expand Up @@ -48,3 +51,135 @@ func TestIsAllowlisted_Proxy_Domain(t *testing.T) {
assert.Equal(t, false, a.IsAllowlisted(urlMustParse("https://health.domain.tld/")))
assert.Equal(t, true, a.IsAllowlisted(urlMustParse("https://health.domain.tld/ping/qq")))
}

func TestAdHeaders_Standard(t *testing.T) {
a := newTestApplication()
h := http.Header{}
a.addHeaders(h, &types.Claims{
PreferredUsername: "foo",
Groups: []string{"foo", "bar"},
Entitlements: []string{"bar", "quox"},
Email: "bar@authentik.company",
Name: "foo",
Sub: "bar",
RawToken: "baz",
})
assert.Equal(t, http.Header{
"X-Authentik-Email": []string{"bar@authentik.company"},
"X-Authentik-Entitlements": []string{"bar|quox"},
"X-Authentik-Groups": []string{"foo|bar"},
"X-Authentik-Jwt": []string{"baz"},
"X-Authentik-Meta-App": []string{""},
"X-Authentik-Meta-Jwks": []string{""},
"X-Authentik-Meta-Outpost": []string{""},
"X-Authentik-Meta-Provider": []string{a.proxyConfig.Name},
"X-Authentik-Meta-Version": []string{constants.UserAgentOutpost()},
"X-Authentik-Name": []string{"foo"},
"X-Authentik-Uid": []string{"bar"},
"X-Authentik-Username": []string{"foo"},
}, h)
}

func TestAdHeaders_BasicAuth(t *testing.T) {
a := newTestApplication()
a.proxyConfig.BasicAuthEnabled = api.PtrBool(true)
a.proxyConfig.BasicAuthUserAttribute = api.PtrString("user")
a.proxyConfig.BasicAuthPasswordAttribute = api.PtrString("pass")
h := http.Header{}
a.addHeaders(h, &types.Claims{
PreferredUsername: "foo",
Groups: []string{"foo", "bar"},
Entitlements: []string{"bar", "quox"},
Email: "bar@authentik.company",
Name: "foo",
Sub: "bar",
RawToken: "baz",
Proxy: &types.ProxyClaims{
UserAttributes: map[string]any{
"user": "foo",
"pass": "baz",
},
},
})
assert.Equal(t, http.Header{
"Authorization": []string{"Basic Zm9vOmJheg=="},
"X-Authentik-Email": []string{"bar@authentik.company"},
"X-Authentik-Entitlements": []string{"bar|quox"},
"X-Authentik-Groups": []string{"foo|bar"},
"X-Authentik-Jwt": []string{"baz"},
"X-Authentik-Meta-App": []string{""},
"X-Authentik-Meta-Jwks": []string{""},
"X-Authentik-Meta-Outpost": []string{""},
"X-Authentik-Meta-Provider": []string{a.proxyConfig.Name},
"X-Authentik-Meta-Version": []string{constants.UserAgentOutpost()},
"X-Authentik-Name": []string{"foo"},
"X-Authentik-Uid": []string{"bar"},
"X-Authentik-Username": []string{"foo"},
}, h)
}

func TestAdHeaders_Extra(t *testing.T) {
a := newTestApplication()
h := http.Header{}
a.addHeaders(h, &types.Claims{
PreferredUsername: "foo",
Groups: []string{"foo", "bar"},
Entitlements: []string{"bar", "quox"},
Email: "bar@authentik.company",
Name: "foo",
Sub: "bar",
RawToken: "baz",
Proxy: &types.ProxyClaims{
UserAttributes: map[string]any{
"additionalHeaders": map[string]any{
"foo": "bar",
},
},
},
})
assert.Equal(t, http.Header{
"Foo": []string{"bar"},
"X-Authentik-Email": []string{"bar@authentik.company"},
"X-Authentik-Entitlements": []string{"bar|quox"},
"X-Authentik-Groups": []string{"foo|bar"},
"X-Authentik-Jwt": []string{"baz"},
"X-Authentik-Meta-App": []string{""},
"X-Authentik-Meta-Jwks": []string{""},
"X-Authentik-Meta-Outpost": []string{""},
"X-Authentik-Meta-Provider": []string{a.proxyConfig.Name},
"X-Authentik-Meta-Version": []string{constants.UserAgentOutpost()},
"X-Authentik-Name": []string{"foo"},
"X-Authentik-Uid": []string{"bar"},
"X-Authentik-Username": []string{"foo"},
}, h)
}

func TestAdHeaders_UnderscoreInitial(t *testing.T) {
a := newTestApplication()
h := http.Header{}
h.Set("X_AUTHENTIK_USERNAME", "another user")
h.Set("X-Authentik_username", "another user")
a.addHeaders(h, &types.Claims{
PreferredUsername: "foo",
Groups: []string{"foo", "bar"},
Entitlements: []string{"bar", "quox"},
Email: "bar@authentik.company",
Name: "foo",
Sub: "bar",
RawToken: "baz",
})
assert.Equal(t, http.Header{
"X-Authentik-Email": []string{"bar@authentik.company"},
"X-Authentik-Entitlements": []string{"bar|quox"},
"X-Authentik-Groups": []string{"foo|bar"},
"X-Authentik-Jwt": []string{"baz"},
"X-Authentik-Meta-App": []string{""},
"X-Authentik-Meta-Jwks": []string{""},
"X-Authentik-Meta-Outpost": []string{""},
"X-Authentik-Meta-Provider": []string{a.proxyConfig.Name},
"X-Authentik-Meta-Version": []string{constants.UserAgentOutpost()},
"X-Authentik-Name": []string{"foo"},
"X-Authentik-Uid": []string{"bar"},
"X-Authentik-Username": []string{"foo"},
}, h)
}
Loading