outposts: set cookies for a domain to authenticate an entire domain (#971)

* outposts: initial cookie domain implementation

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

* web/admin: add cookie domain setting

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

* providers/proxy: replace forward_auth_mode with general mode

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

* web/admin: rebuild proxy provider form

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

* providers/proxy: re-add forward_auth_mode for backwards compat

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

* web/admin: fix data.mode not being set

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

* root: always set log level to debug when testing

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

* providers/proxy: use new mode attribute

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

* providers/proxy: only ingress /akprox on forward_domain

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

* providers/proxy: fix lint error

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

* web/admin: fix error on ProxyProviderForm when not using proxy mode

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

* web/admin: fix default for outpost form's type missing

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

* web/admin: add additional desc for proxy modes

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

* outposts: fix service account permissions not always being updated

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

* outpost/proxy: fix redirecting to incorrect host for domain mode

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

* web: improve error handling for network errors

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

* outpost: fix image naming not matching main imaeg

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

* outposts/proxy: fix redirects for domain mode and traefik

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

* web: fix colour for paragraphs

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

* web/flows: fix consent stage not showing permissions correctly

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

* website/docs: add domain-level docs

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

* website/docs: fix broken links

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

* outposts/proxy: remove dead code

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

* web/flows: fix missing id for #header-text

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2021-06-08 23:10:17 +02:00 committed by GitHub
parent fb8d67a9d9
commit dad24c03ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 473 additions and 232 deletions

View file

@ -149,8 +149,9 @@ def outpost_post_save(model_class: str, model_pk: Any):
return return
if isinstance(instance, Outpost): if isinstance(instance, Outpost):
LOGGER.debug("Ensuring token for outpost", instance=instance) LOGGER.debug("Ensuring token and permissions for outpost", instance=instance)
_ = instance.token _ = instance.token
_ = instance.user
LOGGER.debug("Trigger reconcile for outpost") LOGGER.debug("Trigger reconcile for outpost")
outpost_controller.delay(instance.pk) outpost_controller.delay(instance.pk)
@ -201,6 +202,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
# Ensure token again, because this function is called when anything related to an # Ensure token again, because this function is called when anything related to an
# OutpostModel is saved, so we can be sure permissions are right # OutpostModel is saved, so we can be sure permissions are right
_ = outpost.token _ = outpost.token
_ = outpost.user
if not layer: # pragma: no cover if not layer: # pragma: no cover
layer = get_channel_layer() layer = get_channel_layer()
for state in OutpostState.for_outpost(outpost): for state in OutpostState.for_outpost(outpost):

View file

@ -10,7 +10,7 @@ from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.providers.oauth2.views.provider import ProviderInfoView from authentik.providers.oauth2.views.provider import ProviderInfoView
from authentik.providers.proxy.models import ProxyProvider from authentik.providers.proxy.models import ProxyMode, ProxyProvider
class OpenIDConnectConfigurationSerializer(PassiveSerializer): class OpenIDConnectConfigurationSerializer(PassiveSerializer):
@ -36,9 +36,9 @@ class ProxyProviderSerializer(ProviderSerializer):
redirect_uris = CharField(read_only=True) redirect_uris = CharField(read_only=True)
def validate(self, attrs) -> dict[Any, str]: def validate(self, attrs) -> dict[Any, str]:
"""Check that internal_host is set when forward_auth_mode is disabled""" """Check that internal_host is set when mode is Proxy"""
if ( if (
not attrs.get("forward_auth_mode", False) attrs.get("mode", ProxyMode.PROXY) == ProxyMode.PROXY
and attrs.get("internal_host", "") == "" and attrs.get("internal_host", "") == ""
): ):
raise ValidationError( raise ValidationError(
@ -70,8 +70,9 @@ class ProxyProviderSerializer(ProviderSerializer):
"basic_auth_enabled", "basic_auth_enabled",
"basic_auth_password_attribute", "basic_auth_password_attribute",
"basic_auth_user_attribute", "basic_auth_user_attribute",
"forward_auth_mode", "mode",
"redirect_uris", "redirect_uris",
"cookie_domain",
] ]
@ -84,9 +85,15 @@ class ProxyProviderViewSet(ModelViewSet):
class ProxyOutpostConfigSerializer(ModelSerializer): class ProxyOutpostConfigSerializer(ModelSerializer):
"""ProxyProvider Serializer""" """Proxy provider serializer for outposts"""
oidc_configuration = SerializerMethodField() oidc_configuration = SerializerMethodField()
forward_auth_mode = SerializerMethodField()
def get_forward_auth_mode(self, instance: ProxyProvider) -> bool:
"""Legacy field for 2021.5 outposts"""
# TODO: remove in 2021.7
return instance.mode in [ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN]
class Meta: class Meta:
@ -106,6 +113,9 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
"basic_auth_enabled", "basic_auth_enabled",
"basic_auth_password_attribute", "basic_auth_password_attribute",
"basic_auth_user_attribute", "basic_auth_user_attribute",
"mode",
"cookie_domain",
# Legacy field, remove in 2021.7
"forward_auth_mode", "forward_auth_mode",
] ]

View file

@ -20,7 +20,7 @@ from authentik.outposts.controllers.k8s.base import (
KubernetesObjectReconciler, KubernetesObjectReconciler,
NeedsUpdate, NeedsUpdate,
) )
from authentik.providers.proxy.models import ProxyProvider from authentik.providers.proxy.models import ProxyMode, ProxyProvider
if TYPE_CHECKING: if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController from authentik.outposts.controllers.kubernetes import KubernetesController
@ -51,7 +51,6 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
expected_hosts_tls = [] expected_hosts_tls = []
for proxy_provider in ProxyProvider.objects.filter( for proxy_provider in ProxyProvider.objects.filter(
outpost__in=[self.controller.outpost], outpost__in=[self.controller.outpost],
forward_auth_mode=False,
): ):
proxy_provider: ProxyProvider proxy_provider: ProxyProvider
external_host_name = urlparse(proxy_provider.external_host) external_host_name = urlparse(proxy_provider.external_host)
@ -105,7 +104,10 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
external_host_name = urlparse(proxy_provider.external_host) external_host_name = urlparse(proxy_provider.external_host)
if external_host_name.scheme == "https": if external_host_name.scheme == "https":
tls_hosts.append(external_host_name.hostname) tls_hosts.append(external_host_name.hostname)
if proxy_provider.forward_auth_mode: if proxy_provider.mode in [
ProxyMode.FORWARD_SINGLE,
ProxyMode.FORWARD_DOMAIN,
]:
rule = NetworkingV1beta1IngressRule( rule = NetworkingV1beta1IngressRule(
host=external_host_name.hostname, host=external_host_name.hostname,
http=NetworkingV1beta1HTTPIngressRuleValue( http=NetworkingV1beta1HTTPIngressRuleValue(

View file

@ -10,7 +10,7 @@ from authentik.outposts.controllers.k8s.base import (
KubernetesObjectReconciler, KubernetesObjectReconciler,
NeedsUpdate, NeedsUpdate,
) )
from authentik.providers.proxy.models import ProxyProvider from authentik.providers.proxy.models import ProxyMode, ProxyProvider
if TYPE_CHECKING: if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController from authentik.outposts.controllers.kubernetes import KubernetesController
@ -73,7 +73,7 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
def noop(self) -> bool: def noop(self) -> bool:
if not ProxyProvider.objects.filter( if not ProxyProvider.objects.filter(
outpost__in=[self.controller.outpost], outpost__in=[self.controller.outpost],
forward_auth_mode=True, mode__in=[ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN],
).exists(): ).exists():
self.logger.debug("No providers with forward auth enabled.") self.logger.debug("No providers with forward auth enabled.")
return True return True

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.3 on 2021-05-31 20:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_proxy", "0011_proxyprovider_forward_auth_mode"),
]
operations = [
migrations.AddField(
model_name="proxyprovider",
name="cookie_domain",
field=models.TextField(blank=True, default=""),
),
]

View file

@ -0,0 +1,44 @@
# Generated by Django 3.2.3 on 2021-06-06 16:29
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_mode(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.providers.proxy.models import ProxyMode
db_alias = schema_editor.connection.alias
ProxyProvider = apps.get_model("authentik_providers_proxy", "proxyprovider")
for provider in ProxyProvider.objects.using(db_alias).all():
if provider.forward_auth_mode:
provider.mode = ProxyMode.FORWARD_SINGLE
provider.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_proxy", "0012_proxyprovider_cookie_domain"),
]
operations = [
migrations.AddField(
model_name="proxyprovider",
name="mode",
field=models.TextField(
choices=[
("proxy", "Proxy"),
("forward_single", "Forward Single"),
("forward_domain", "Forward Domain"),
],
default="proxy",
help_text="Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with internal_host.",
),
),
migrations.RunPython(migrate_mode),
migrations.RemoveField(
model_name="proxyprovider",
name="forward_auth_mode",
),
]

View file

@ -37,6 +37,14 @@ def _get_callback_url(uri: str) -> str:
return urljoin(uri, "/akprox/callback") return urljoin(uri, "/akprox/callback")
class ProxyMode(models.TextChoices):
"""All modes a Proxy provider can operate in"""
PROXY = "proxy"
FORWARD_SINGLE = "forward_single"
FORWARD_DOMAIN = "forward_domain"
class ProxyProvider(OutpostModel, OAuth2Provider): class ProxyProvider(OutpostModel, OAuth2Provider):
"""Protect applications that don't support any of the other """Protect applications that don't support any of the other
Protocols by using a Reverse-Proxy.""" Protocols by using a Reverse-Proxy."""
@ -53,8 +61,9 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
help_text=_("Validate SSL Certificates of upstream servers"), help_text=_("Validate SSL Certificates of upstream servers"),
verbose_name=_("Internal host SSL Validation"), verbose_name=_("Internal host SSL Validation"),
) )
forward_auth_mode = models.BooleanField( mode = models.TextField(
default=False, default=ProxyMode.PROXY,
choices=ProxyMode.choices,
help_text=_( help_text=_(
"Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with " "Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with "
"internal_host." "internal_host."
@ -107,6 +116,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
) )
cookie_secret = models.TextField(default=get_cookie_secret) cookie_secret = models.TextField(default=get_cookie_secret)
cookie_domain = models.TextField(default="", blank=True)
@property @property
def component(self) -> str: def component(self) -> str:

View file

@ -155,6 +155,7 @@ SPECTACULAR_SETTINGS = {
"ChallengeChoices": "authentik.flows.challenge.ChallengeTypes", "ChallengeChoices": "authentik.flows.challenge.ChallengeTypes",
"FlowDesignationEnum": "authentik.flows.models.FlowDesignation", "FlowDesignationEnum": "authentik.flows.models.FlowDesignation",
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode", "PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
"ProxyMode": "authentik.providers.proxy.models.ProxyMode",
}, },
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False, "ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
"POSTPROCESSING_HOOKS": [ "POSTPROCESSING_HOOKS": [
@ -391,7 +392,10 @@ if _ERROR_REPORTING:
STATIC_URL = "/static/" STATIC_URL = "/static/"
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
LOG_LEVEL = CONFIG.y("log_level").upper() TEST = False
TEST_RUNNER = "authentik.root.test_runner.PytestTestRunner"
LOG_LEVEL = CONFIG.y("log_level").upper() if not TEST else "DEBUG"
structlog.configure_once( structlog.configure_once(
@ -449,9 +453,6 @@ LOGGING = {
"loggers": {}, "loggers": {},
} }
TEST = False
TEST_RUNNER = "authentik.root.test_runner.PytestTestRunner"
_LOGGING_HANDLER_MAP = { _LOGGING_HANDLER_MAP = {
"": LOG_LEVEL, "": LOG_LEVEL,
"authentik": LOG_LEVEL, "authentik": LOG_LEVEL,

View file

@ -77,7 +77,7 @@ stages:
buildContext: '$(Build.SourcesDirectory)' buildContext: '$(Build.SourcesDirectory)'
tags: | tags: |
gh-$(branchName) gh-$(branchName)
gh-$(Build.SourceVersion) gh-$(branchName)-$(timestamp)
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)' arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
- task: Docker@2 - task: Docker@2
inputs: inputs:
@ -86,7 +86,7 @@ stages:
command: 'push' command: 'push'
tags: | tags: |
gh-$(branchName) gh-$(branchName)
gh-$(Build.SourceVersion) gh-$(branchName)-$(timestamp)
- job: ldap_build_docker - job: ldap_build_docker
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
@ -108,7 +108,7 @@ stages:
buildContext: '$(Build.SourcesDirectory)' buildContext: '$(Build.SourcesDirectory)'
tags: | tags: |
gh-$(branchName) gh-$(branchName)
gh-$(Build.SourceVersion) gh-$(branchName)-$(timestamp)
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)' arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
- task: Docker@2 - task: Docker@2
inputs: inputs:
@ -117,4 +117,4 @@ stages:
command: 'push' command: 'push'
tags: | tags: |
gh-$(branchName) gh-$(branchName)
gh-$(Build.SourceVersion) gh-$(branchName)-$(timestamp)

View file

@ -64,7 +64,7 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.
providerOpts.SkipAuthRegex = skipRegexes providerOpts.SkipAuthRegex = skipRegexes
} }
if *provider.ForwardAuthMode { if *provider.Mode == api.PROXYMODE_FORWARD_SINGLE || *provider.Mode == api.PROXYMODE_FORWARD_DOMAIN {
providerOpts.UpstreamServers = []options.Upstream{ providerOpts.UpstreamServers = []options.Upstream{
{ {
ID: "static", ID: "static",
@ -111,6 +111,10 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.
func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) { func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
opts := pb.prepareOpts(provider) opts := pb.prepareOpts(provider)
if *provider.Mode == api.PROXYMODE_FORWARD_DOMAIN {
opts.Cookie.Domains = []string{*provider.CookieDomain}
}
chain := alice.New() chain := alice.New()
if opts.ForceHTTPS { if opts.ForceHTTPS {
@ -123,10 +127,6 @@ func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
healthCheckPaths := []string{opts.PingPath} healthCheckPaths := []string{opts.PingPath}
healthCheckUserAgents := []string{opts.PingUserAgent} healthCheckUserAgents := []string{opts.PingUserAgent}
if opts.GCPHealthChecks {
healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
}
// To silence logging of health checks, register the health check handler before // To silence logging of health checks, register the health check handler before
// the logging handler // the logging handler
@ -153,6 +153,8 @@ func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
oauthproxy.BasicAuthPasswordAttribute = *provider.BasicAuthPasswordAttribute oauthproxy.BasicAuthPasswordAttribute = *provider.BasicAuthPasswordAttribute
} }
oauthproxy.ExternalHost = pb.Host
pb.proxy = oauthproxy pb.proxy = oauthproxy
pb.Handler = chain.Then(oauthproxy) pb.Handler = chain.Then(oauthproxy)
} }

View file

@ -106,35 +106,22 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"): case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
redirectURL, err := url.Parse(redirect) redirectURL, err := url.Parse(redirect)
if err != nil { if err != nil {
p.logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect) p.logger.WithField("redirect", redirect).Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
return false return false
} }
redirectHostname := redirectURL.Hostname() redirectHostname := redirectURL.Hostname()
for _, domain := range p.whitelistDomains { for _, domain := range p.CookieDomains {
domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, ".")) if strings.HasSuffix(redirectHostname, domain) {
if domainHostname == "" { p.logger.WithField("redirect", redirect).WithField("domain", domain).Debug("allowing redirect")
continue return true
}
if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
// the domain names match, now validate the ports
// if the whitelisted domain's port is '*', allow all ports
// if the whitelisted domain contains a specific port, only allow that port
// if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
redirectPort := redirectURL.Port()
if (domainPort == "*") ||
(domainPort == redirectPort) ||
(domainPort == "" && redirectPort == "") {
return true
}
} }
} }
p.logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect) p.logger.WithField("redirect", redirect).Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
return false return false
default: default:
p.logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect) p.logger.WithField("redirect", redirect).Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
return false return false
} }
} }

View file

@ -65,7 +65,7 @@ type OAuthProxy struct {
AuthOnlyPath string AuthOnlyPath string
UserInfoPath string UserInfoPath string
forwardAuthMode bool mode api.ProxyMode
redirectURL *url.URL // the url to receive requests at redirectURL *url.URL // the url to receive requests at
whitelistDomains []string whitelistDomains []string
provider providers.Provider provider providers.Provider
@ -77,6 +77,7 @@ type OAuthProxy struct {
PassUserHeaders bool PassUserHeaders bool
BasicAuthUserAttribute string BasicAuthUserAttribute string
BasicAuthPasswordAttribute string BasicAuthPasswordAttribute string
ExternalHost string
PassAccessToken bool PassAccessToken bool
SetAuthorization bool SetAuthorization bool
PassAuthorization bool PassAuthorization bool
@ -136,7 +137,7 @@ func NewOAuthProxy(opts *options.Options, provider api.ProxyOutpostConfig, c *ht
CookieRefresh: opts.Cookie.Refresh, CookieRefresh: opts.Cookie.Refresh,
CookieSameSite: opts.Cookie.SameSite, CookieSameSite: opts.Cookie.SameSite,
forwardAuthMode: *provider.ForwardAuthMode, mode: *provider.Mode,
RobotsPath: "/robots.txt", RobotsPath: "/robots.txt",
SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix), SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
@ -216,43 +217,6 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, m
} }
} }
// splitHostPort separates host and port. If the port is not valid, it returns
// the entire input as host, and it doesn't check the validity of the host.
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
// *** taken from net/url, modified validOptionalPort() to accept ":*"
func splitHostPort(hostport string) (host, port string) {
host = hostport
colon := strings.LastIndexByte(host, ':')
if colon != -1 && validOptionalPort(host[colon:]) {
host, port = host[:colon], host[colon+1:]
}
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
host = host[1 : len(host)-1]
}
return
}
// validOptionalPort reports whether port is either an empty string
// or matches /^:\d*$/
// *** taken from net/url, modified to accept ":*"
func validOptionalPort(port string) bool {
if port == "" || port == ":*" {
return true
}
if port[0] != ':' {
return false
}
for _, b := range port[1:] {
if b < '0' || b > '9' {
return false
}
}
return true
}
// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en // See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
var noCacheHeaders = map[string]string{ var noCacheHeaders = map[string]string{
"Expires": time.Unix(0, 0).Format(time.RFC1123), "Expires": time.Unix(0, 0).Format(time.RFC1123),
@ -340,18 +304,41 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) { func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
session, err := p.getAuthenticatedSession(rw, req) session, err := p.getAuthenticatedSession(rw, req)
if err != nil { if err != nil {
if p.forwardAuthMode { if p.mode == api.PROXYMODE_FORWARD_SINGLE || p.mode == api.PROXYMODE_FORWARD_DOMAIN {
if _, ok := req.URL.Query()["nginx"]; ok { if _, ok := req.URL.Query()["nginx"]; ok {
rw.WriteHeader(401) rw.WriteHeader(401)
return return
} }
if _, ok := req.URL.Query()["traefik"]; ok { if _, ok := req.URL.Query()["traefik"]; ok {
host := getHost(req) host := ""
// Optional suffix, which is appended to the URL
suffix := ""
if p.mode == api.PROXYMODE_FORWARD_SINGLE {
host = getHost(req)
} else if p.mode == api.PROXYMODE_FORWARD_DOMAIN {
host = p.ExternalHost
// set the ?rd 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
v := url.Values{
// see https://doc.traefik.io/traefik/middlewares/forwardauth/
// X-Forwarded-Uri is only the path, so we need to build the entire URL
"rd": []string{fmt.Sprintf(
"%s://%s%s",
req.Header.Get("X-Forwarded-Proto"),
req.Header.Get("X-Forwarded-Host"),
req.Header.Get("X-Forwarded-Uri"),
)},
}
suffix = fmt.Sprintf("?%s", v.Encode())
}
proto := req.Header.Get("X-Forwarded-Proto") proto := req.Header.Get("X-Forwarded-Proto")
if proto != "" { if proto != "" {
proto = proto + ":" proto = proto + ":"
} }
http.Redirect(rw, req, fmt.Sprintf("%s//%s%s", proto, host, p.OAuthStartPath), http.StatusTemporaryRedirect) rdFinal := fmt.Sprintf("%s//%s%s%s", proto, host, p.OAuthStartPath, suffix)
p.logger.WithField("url", rdFinal).Debug("Redirecting to login")
http.Redirect(rw, req, rdFinal, http.StatusTemporaryRedirect)
return return
} }
} }
@ -360,7 +347,7 @@ func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request)
} }
// we are authenticated // we are authenticated
p.addHeadersForProxying(rw, req, session) p.addHeadersForProxying(rw, req, session)
if p.forwardAuthMode { if p.mode == api.PROXYMODE_FORWARD_SINGLE || p.mode == api.PROXYMODE_FORWARD_DOMAIN {
for headerKey, headers := range req.Header { for headerKey, headers := range req.Header {
for _, value := range headers { for _, value := range headers {
rw.Header().Set(headerKey, value) rw.Header().Set(headerKey, value)

View file

@ -22966,10 +22966,13 @@ components:
title: HTTP-Basic Username Key title: HTTP-Basic Username Key
description: User/Group Attribute used for the user part of the HTTP-Basic description: User/Group Attribute used for the user part of the HTTP-Basic
Header. If not set, the user's Email address is used. Header. If not set, the user's Email address is used.
forward_auth_mode: mode:
type: boolean allOf:
- $ref: '#/components/schemas/ProxyMode'
description: Enable support for forwardAuth in traefik and nginx auth_request. description: Enable support for forwardAuth in traefik and nginx auth_request.
Exclusive with internal_host. Exclusive with internal_host.
cookie_domain:
type: string
PatchedReputationPolicyRequest: PatchedReputationPolicyRequest:
type: object type: object
description: Reputation Policy Serializer description: Reputation Policy Serializer
@ -23971,9 +23974,15 @@ components:
required: required:
- authorization_flow - authorization_flow
- name - name
ProxyMode:
enum:
- proxy
- forward_single
- forward_domain
type: string
ProxyOutpostConfig: ProxyOutpostConfig:
type: object type: object
description: ProxyProvider Serializer description: Proxy provider serializer for outposts
properties: properties:
pk: pk:
type: integer type: integer
@ -24025,12 +24034,19 @@ components:
title: HTTP-Basic Username Key title: HTTP-Basic Username Key
description: User/Group Attribute used for the user part of the HTTP-Basic description: User/Group Attribute used for the user part of the HTTP-Basic
Header. If not set, the user's Email address is used. Header. If not set, the user's Email address is used.
forward_auth_mode: mode:
type: boolean allOf:
- $ref: '#/components/schemas/ProxyMode'
description: Enable support for forwardAuth in traefik and nginx auth_request. description: Enable support for forwardAuth in traefik and nginx auth_request.
Exclusive with internal_host. Exclusive with internal_host.
cookie_domain:
type: string
forward_auth_mode:
type: boolean
readOnly: true
required: required:
- external_host - external_host
- forward_auth_mode
- name - name
- oidc_configuration - oidc_configuration
- pk - pk
@ -24102,13 +24118,16 @@ components:
title: HTTP-Basic Username Key title: HTTP-Basic Username Key
description: User/Group Attribute used for the user part of the HTTP-Basic description: User/Group Attribute used for the user part of the HTTP-Basic
Header. If not set, the user's Email address is used. Header. If not set, the user's Email address is used.
forward_auth_mode: mode:
type: boolean allOf:
- $ref: '#/components/schemas/ProxyMode'
description: Enable support for forwardAuth in traefik and nginx auth_request. description: Enable support for forwardAuth in traefik and nginx auth_request.
Exclusive with internal_host. Exclusive with internal_host.
redirect_uris: redirect_uris:
type: string type: string
readOnly: true readOnly: true
cookie_domain:
type: string
required: required:
- assigned_application_name - assigned_application_name
- assigned_application_slug - assigned_application_slug
@ -24167,10 +24186,13 @@ components:
title: HTTP-Basic Username Key title: HTTP-Basic Username Key
description: User/Group Attribute used for the user part of the HTTP-Basic description: User/Group Attribute used for the user part of the HTTP-Basic
Header. If not set, the user's Email address is used. Header. If not set, the user's Email address is used.
forward_auth_mode: mode:
type: boolean allOf:
- $ref: '#/components/schemas/ProxyMode'
description: Enable support for forwardAuth in traefik and nginx auth_request. description: Enable support for forwardAuth in traefik and nginx auth_request.
Exclusive with internal_host. Exclusive with internal_host.
cookie_domain:
type: string
required: required:
- authorization_flow - authorization_flow
- external_host - external_host

View file

@ -25,8 +25,8 @@ export function configureSentry(canDoPpi: boolean = false, tags: { [key: string]
if (hint.originalException instanceof SentryIgnoredError) { if (hint.originalException instanceof SentryIgnoredError) {
return null; return null;
} }
if (hint.originalException instanceof Error) { if ((hint.originalException as Error | undefined)?.hasOwnProperty("name")) {
if (hint.originalException.name == 'NetworkError') { if ((hint.originalException as Error | undefined)?.name == 'NetworkError') {
return null; return null;
} }
} }

View file

@ -120,6 +120,9 @@ body {
.pf-c-title { .pf-c-title {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-u-mb-xl {
color: var(--ak-dark-foreground);
}
/* Header sections */ /* Header sections */
.pf-c-page__main-section { .pf-c-page__main-section {
--pf-c-page__main-section--BackgroundColor: var(--ak-dark-background); --pf-c-page__main-section--BackgroundColor: var(--ak-dark-background);

View file

@ -1,11 +1,13 @@
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { CSSResult, customElement, html, TemplateResult } from "lit-element"; import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css"; import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css"; import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
import AKGlobal from "../../../authentik.css"; import AKGlobal from "../../../authentik.css";
import { BaseStage } from "../base"; import { BaseStage } from "../base";
import "../../../elements/EmptyState"; import "../../../elements/EmptyState";
@ -18,7 +20,7 @@ import { ifDefined } from "lit-html/directives/if-defined";
export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> { export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal]; return [PFBase, PFLogin, PFList, PFForm, PFSpacing, PFFormControl, PFTitle, PFButton, AKGlobal];
} }
render(): TemplateResult { render(): TemplateResult {
@ -44,11 +46,11 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
</div> </div>
</ak-form-static> </ak-form-static>
<div class="pf-c-form__group"> <div class="pf-c-form__group">
<p id="header-text"> <p id="header-text" class="pf-u-mb-xl">
${this.challenge.headerText} ${this.challenge.headerText}
</p> </p>
${this.challenge.permissions.length > 0 ? html` ${this.challenge.permissions.length > 0 ? html`
<p>${t`Application requires following permissions`}</p> <p class="pf-u-mb-sm">${t`Application requires following permissions:`}</p>
<ul class="pf-c-list" id="permmissions"> <ul class="pf-c-list" id="permmissions">
${this.challenge.permissions.map((permission) => { ${this.challenge.permissions.map((permission) => {
return html`<li data-permission-code="${permission.id}">${permission.name}</li>`; return html`<li data-permission-code="${permission.id}">${permission.name}</li>`;

View file

@ -793,6 +793,10 @@ msgstr "Continue flow without invitation"
msgid "Control how authentik exposes and interprets information." msgid "Control how authentik exposes and interprets information."
msgstr "Control how authentik exposes and interprets information." msgstr "Control how authentik exposes and interprets information."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Cookie domain"
msgstr "Cookie domain"
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts
msgid "Copy" msgid "Copy"
msgstr "Copy" msgstr "Copy"
@ -1302,14 +1306,6 @@ msgstr "Enable TOTP"
msgid "Enable compatibility mode, increases compatibility with password managers on mobile devices." msgid "Enable compatibility mode, increases compatibility with password managers on mobile devices."
msgstr "Enable compatibility mode, increases compatibility with password managers on mobile devices." msgstr "Enable compatibility mode, increases compatibility with password managers on mobile devices."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Enable forward-auth mode"
msgstr "Enable forward-auth mode"
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Enable this if you don't want to use this provider as a proxy, and want to use it with Traefik's forwardAuth or nginx's auth_request."
msgstr "Enable this if you don't want to use this provider as a proxy, and want to use it with Traefik's forwardAuth or nginx's auth_request."
#: src/pages/policies/BoundPoliciesList.ts #: src/pages/policies/BoundPoliciesList.ts
#: src/pages/policies/PolicyBindingForm.ts #: src/pages/policies/PolicyBindingForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
@ -1471,6 +1467,8 @@ msgstr "External Applications which use authentik as Identity-Provider, utilizin
msgid "External Host" msgid "External Host"
msgstr "External Host" msgstr "External Host"
#: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "External host" msgid "External host"
msgstr "External host" msgstr "External host"
@ -1624,9 +1622,13 @@ msgstr "Forgot username or password?"
msgid "Form didn't return a promise for submitting" msgid "Form didn't return a promise for submitting"
msgstr "Form didn't return a promise for submitting" msgstr "Form didn't return a promise for submitting"
#: src/pages/providers/proxy/ProxyProviderViewPage.ts #: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Forward auth" msgid "Forward auth (domain level)"
msgstr "Forward auth" msgstr "Forward auth (domain level)"
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Forward auth (single application)"
msgstr "Forward auth (single application)"
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts #: src/pages/property-mappings/PropertyMappingSAMLForm.ts
msgid "Friendly Name" msgid "Friendly Name"
@ -2179,6 +2181,7 @@ msgstr "Minimum length"
#: src/pages/events/TransportForm.ts #: src/pages/events/TransportForm.ts
#: src/pages/events/TransportListPage.ts #: src/pages/events/TransportListPage.ts
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
#: src/pages/stages/consent/ConsentStageForm.ts #: src/pages/stages/consent/ConsentStageForm.ts
msgid "Mode" msgid "Mode"
msgstr "Mode" msgstr "Mode"
@ -2308,7 +2311,6 @@ msgstr "New version available!"
#: src/pages/policies/BoundPoliciesList.ts #: src/pages/policies/BoundPoliciesList.ts
#: src/pages/policies/PolicyTestForm.ts #: src/pages/policies/PolicyTestForm.ts
#: src/pages/providers/proxy/ProxyProviderViewPage.ts #: src/pages/providers/proxy/ProxyProviderViewPage.ts
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
#: src/pages/tenants/TenantListPage.ts #: src/pages/tenants/TenantListPage.ts
#: src/pages/tokens/TokenListPage.ts #: src/pages/tokens/TokenListPage.ts
#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts
@ -2514,6 +2516,10 @@ msgstr "Optionally pre-fill the input value"
msgid "Optionally set the 'FriendlyName' value of the Assertion attribute." msgid "Optionally set the 'FriendlyName' value of the Assertion attribute."
msgstr "Optionally set the 'FriendlyName' value of the Assertion attribute." msgstr "Optionally set the 'FriendlyName' value of the Assertion attribute."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
msgstr "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
#: src/pages/flows/BoundStagesList.ts #: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts #: src/pages/flows/StageBindingForm.ts
#: src/pages/policies/BoundPoliciesList.ts #: src/pages/policies/BoundPoliciesList.ts
@ -2753,7 +2759,6 @@ msgstr "Protocol Settings"
#: src/pages/providers/ldap/LDAPProviderForm.ts #: src/pages/providers/ldap/LDAPProviderForm.ts
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts
#: src/pages/sources/oauth/OAuthSourceForm.ts #: src/pages/sources/oauth/OAuthSourceForm.ts
#: src/pages/sources/plex/PlexSourceForm.ts #: src/pages/sources/plex/PlexSourceForm.ts
@ -2791,6 +2796,7 @@ msgid "Providers"
msgstr "Providers" msgstr "Providers"
#: src/pages/outposts/OutpostForm.ts #: src/pages/outposts/OutpostForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Proxy" msgid "Proxy"
msgstr "Proxy" msgstr "Proxy"
@ -3776,10 +3782,15 @@ msgstr "Text: Simple Text input"
msgid "The URL \"{0}\" was not found." msgid "The URL \"{0}\" was not found."
msgstr "The URL \"{0}\" was not found." msgstr "The URL \"{0}\" was not found."
#: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "The external URL you'll access the application at. Include any non-standard port." msgid "The external URL you'll access the application at. Include any non-standard port."
msgstr "The external URL you'll access the application at. Include any non-standard port." msgstr "The external URL you'll access the application at. Include any non-standard port."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
msgstr "The external URL you'll authenticate at. Can be the same domain as authentik."
#: src/pages/policies/dummy/DummyPolicyForm.ts #: src/pages/policies/dummy/DummyPolicyForm.ts
msgid "The policy takes a random time to execute. This controls the minimum time it will take." msgid "The policy takes a random time to execute. This controls the minimum time it will take."
msgstr "The policy takes a random time to execute. This controls the minimum time it will take." msgstr "The policy takes a random time to execute. This controls the minimum time it will take."
@ -3814,6 +3825,10 @@ msgstr ""
msgid "These policies control which users can access this application." msgid "These policies control which users can access this application."
msgstr "These policies control which users can access this application." msgstr "These policies control which users can access this application."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
msgstr "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
#: src/pages/stages/invitation/InvitationStageForm.ts #: src/pages/stages/invitation/InvitationStageForm.ts
msgid "This stage can be included in enrollment flows to accept invitations." msgid "This stage can be included in enrollment flows to accept invitations."
msgstr "This stage can be included in enrollment flows to accept invitations." msgstr "This stage can be included in enrollment flows to accept invitations."
@ -4169,6 +4184,14 @@ msgstr "Use the user's email address, but deny enrollment when the email address
msgid "Use the user's username, but deny enrollment when the username already exists." msgid "Use the user's username, but deny enrollment when the username already exists."
msgstr "Use the user's username, but deny enrollment when the username already exists." msgstr "Use the user's username, but deny enrollment when the username already exists."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
msgstr "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
msgstr "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
#: src/pages/tenants/TenantForm.ts #: src/pages/tenants/TenantForm.ts
msgid "Use this tenant for each domain that doesn't have a dedicated tenant." msgid "Use this tenant for each domain that doesn't have a dedicated tenant."
msgstr "Use this tenant for each domain that doesn't have a dedicated tenant." msgstr "Use this tenant for each domain that doesn't have a dedicated tenant."
@ -4447,7 +4470,6 @@ msgstr "X509 Subject"
#: src/pages/policies/BoundPoliciesList.ts #: src/pages/policies/BoundPoliciesList.ts
#: src/pages/policies/PolicyTestForm.ts #: src/pages/policies/PolicyTestForm.ts
#: src/pages/providers/proxy/ProxyProviderViewPage.ts #: src/pages/providers/proxy/ProxyProviderViewPage.ts
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
#: src/pages/tenants/TenantListPage.ts #: src/pages/tenants/TenantListPage.ts
#: src/pages/tokens/TokenListPage.ts #: src/pages/tokens/TokenListPage.ts
#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts

View file

@ -787,6 +787,10 @@ msgstr ""
msgid "Control how authentik exposes and interprets information." msgid "Control how authentik exposes and interprets information."
msgstr "" msgstr ""
#:
msgid "Cookie domain"
msgstr ""
#: #:
msgid "Copy" msgid "Copy"
msgstr "" msgstr ""
@ -1294,14 +1298,6 @@ msgstr ""
msgid "Enable compatibility mode, increases compatibility with password managers on mobile devices." msgid "Enable compatibility mode, increases compatibility with password managers on mobile devices."
msgstr "" msgstr ""
#:
msgid "Enable forward-auth mode"
msgstr ""
#:
msgid "Enable this if you don't want to use this provider as a proxy, and want to use it with Traefik's forwardAuth or nginx's auth_request."
msgstr ""
#: #:
#: #:
#: #:
@ -1463,6 +1459,8 @@ msgstr ""
msgid "External Host" msgid "External Host"
msgstr "" msgstr ""
#:
#:
#: #:
msgid "External host" msgid "External host"
msgstr "" msgstr ""
@ -1617,7 +1615,11 @@ msgid "Form didn't return a promise for submitting"
msgstr "" msgstr ""
#: #:
msgid "Forward auth" msgid "Forward auth (domain level)"
msgstr ""
#:
msgid "Forward auth (single application)"
msgstr "" msgstr ""
#: #:
@ -2172,6 +2174,7 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Mode" msgid "Mode"
msgstr "" msgstr ""
@ -2304,7 +2307,6 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "No" msgid "No"
msgstr "" msgstr ""
@ -2506,6 +2508,10 @@ msgstr ""
msgid "Optionally set the 'FriendlyName' value of the Assertion attribute." msgid "Optionally set the 'FriendlyName' value of the Assertion attribute."
msgstr "" msgstr ""
#:
msgid "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
msgstr ""
#: #:
#: #:
#: #:
@ -2749,7 +2755,6 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Protocol settings" msgid "Protocol settings"
msgstr "" msgstr ""
@ -2782,6 +2787,7 @@ msgstr ""
msgid "Providers" msgid "Providers"
msgstr "" msgstr ""
#:
#: #:
msgid "Proxy" msgid "Proxy"
msgstr "" msgstr ""
@ -3768,10 +3774,15 @@ msgstr ""
msgid "The URL \"{0}\" was not found." msgid "The URL \"{0}\" was not found."
msgstr "" msgstr ""
#:
#: #:
msgid "The external URL you'll access the application at. Include any non-standard port." msgid "The external URL you'll access the application at. Include any non-standard port."
msgstr "" msgstr ""
#:
msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
msgstr ""
#: #:
msgid "The policy takes a random time to execute. This controls the minimum time it will take." msgid "The policy takes a random time to execute. This controls the minimum time it will take."
msgstr "" msgstr ""
@ -3802,6 +3813,10 @@ msgstr ""
msgid "These policies control which users can access this application." msgid "These policies control which users can access this application."
msgstr "" msgstr ""
#:
msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
msgstr ""
#: #:
msgid "This stage can be included in enrollment flows to accept invitations." msgid "This stage can be included in enrollment flows to accept invitations."
msgstr "" msgstr ""
@ -4157,6 +4172,14 @@ msgstr ""
msgid "Use the user's username, but deny enrollment when the username already exists." msgid "Use the user's username, but deny enrollment when the username already exists."
msgstr "" msgstr ""
#:
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
msgstr ""
#:
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
msgstr ""
#: #:
msgid "Use this tenant for each domain that doesn't have a dedicated tenant." msgid "Use this tenant for each domain that doesn't have a dedicated tenant."
msgstr "" msgstr ""
@ -4437,7 +4460,6 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""

View file

@ -14,7 +14,7 @@ import { ModelForm } from "../../elements/forms/ModelForm";
export class OutpostForm extends ModelForm<Outpost, string> { export class OutpostForm extends ModelForm<Outpost, string> {
@property() @property()
type!: OutpostTypeEnum; type: OutpostTypeEnum = OutpostTypeEnum.Proxy;
loadInstance(pk: string): Promise<Outpost> { loadInstance(pk: string): Promise<Outpost> {
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesRetrieve({ return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesRetrieve({

View file

@ -1,9 +1,12 @@
import { CryptoApi, FlowsApi, FlowsInstancesListDesignationEnum, ProvidersApi, ProxyProvider } from "authentik-api"; import { CryptoApi, FlowsApi, FlowsInstancesListDesignationEnum, ProvidersApi, ProxyMode, ProxyProvider } from "authentik-api";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { customElement, property } from "lit-element"; import { css, CSSResult, customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html"; import { html, TemplateResult } from "lit-html";
import { DEFAULT_CONFIG } from "../../../api/Config"; import { DEFAULT_CONFIG } from "../../../api/Config";
import { ModelForm } from "../../../elements/forms/ModelForm"; import { ModelForm } from "../../../elements/forms/ModelForm";
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
import { until } from "lit-html/directives/until"; import { until } from "lit-html/directives/until";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import "../../../elements/forms/HorizontalFormElement"; import "../../../elements/forms/HorizontalFormElement";
@ -13,12 +16,20 @@ import { first } from "../../../utils";
@customElement("ak-provider-proxy-form") @customElement("ak-provider-proxy-form")
export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> { export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
static get styles(): CSSResult[] {
return super.styles.concat(PFToggleGroup, PFContent, PFSpacing, css`
.pf-c-toggle-group {
justify-content: center;
}
`);
}
loadInstance(pk: number): Promise<ProxyProvider> { loadInstance(pk: number): Promise<ProxyProvider> {
return new ProvidersApi(DEFAULT_CONFIG).providersProxyRetrieve({ return new ProvidersApi(DEFAULT_CONFIG).providersProxyRetrieve({
id: pk, id: pk,
}).then(provider => { }).then(provider => {
this.showHttpBasic = first(provider.basicAuthEnabled, true); this.showHttpBasic = first(provider.basicAuthEnabled, true);
this.showInternalServer = first(!provider.forwardAuthMode, true); this.mode = first(provider.mode, ProxyMode.Proxy);
return provider; return provider;
}); });
} }
@ -26,8 +37,8 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
@property({type: Boolean}) @property({type: Boolean})
showHttpBasic = true; showHttpBasic = true;
@property({type: Boolean}) @property({attribute: false})
showInternalServer = true; mode: ProxyMode = ProxyMode.Proxy;
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
@ -38,6 +49,7 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
} }
send = (data: ProxyProvider): Promise<ProxyProvider> => { send = (data: ProxyProvider): Promise<ProxyProvider> => {
data.mode = this.mode;
if (this.instance) { if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersProxyUpdate({ return new ProvidersApi(DEFAULT_CONFIG).providersProxyUpdate({
id: this.instance.pk || 0, id: this.instance.pk || 0,
@ -68,26 +80,94 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
</ak-form-element-horizontal>`; </ak-form-element-horizontal>`;
} }
renderInternalServer(): TemplateResult { renderModeSelector(): TemplateResult {
if (!this.showInternalServer) { return html`
return html``; <div class="pf-c-toggle-group__item">
<button class="pf-c-toggle-group__button ${this.mode === ProxyMode.Proxy ? "pf-m-selected" : ""}" type="button" @click=${() => {
this.mode = ProxyMode.Proxy;
}}>
<span class="pf-c-toggle-group__text">${t`Proxy`}</span>
</button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
<div class="pf-c-toggle-group__item">
<button class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardSingle ? "pf-m-selected" : ""}" type="button" @click=${() => {
this.mode = ProxyMode.ForwardSingle;
}}>
<span class="pf-c-toggle-group__text">${t`Forward auth (single application)`}</span>
</button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
<div class="pf-c-toggle-group__item">
<button class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardDomain ? "pf-m-selected" : ""}" type="button" @click=${() => {
this.mode = ProxyMode.ForwardDomain;
}}>
<span class="pf-c-toggle-group__text">${t`Forward auth (domain level)`}</span>
</button>
</div>`;
}
renderSettings(): TemplateResult {
switch (this.mode) {
case ProxyMode.Proxy:
return html`
<p class="pf-u-mb-xl">
${t`This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.`}
</p>
<ak-form-element-horizontal
label=${t`External host`}
?required=${true}
name="externalHost">
<input type="text" value="${ifDefined(this.instance?.externalHost)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`The external URL you'll access the application at. Include any non-standard port.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Internal host`}
?required=${true}
name="internalHost">
<input type="text" value="${ifDefined(this.instance?.internalHost)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`Upstream host that the requests are forwarded to.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="internalHostSslValidation">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.internalHostSslValidation, true)}>
<label class="pf-c-check__label">
${t`Internal host SSL Validation`}
</label>
</div>
<p class="pf-c-form__helper-text">${t`Validate SSL Certificates of upstream servers.`}</p>
</ak-form-element-horizontal>`;
case ProxyMode.ForwardSingle:
return html`
<p class="pf-u-mb-xl">
${t`Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you).`}
</p>
<ak-form-element-horizontal
label=${t`External host`}
?required=${true}
name="externalHost">
<input type="text" value="${ifDefined(this.instance?.externalHost)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`The external URL you'll access the application at. Include any non-standard port.`}</p>
</ak-form-element-horizontal>`;
case ProxyMode.ForwardDomain:
return html`
<p class="pf-u-mb-xl">
${t`Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.`}
</p>
<ak-form-element-horizontal
label=${t`External host`}
?required=${true}
name="externalHost">
<input type="text" value="${first(this.instance?.externalHost, window.location.origin)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`The external URL you'll authenticate at. Can be the same domain as authentik.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Cookie domain`}
name="cookieDomain">
<input type="text" value="${ifDefined(this.instance?.cookieDomain)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.`}</p>
</ak-form-element-horizontal>`;
} }
return html`<ak-form-element-horizontal
label=${t`Internal host`}
?required=${true}
name="internalHost">
<input type="text" value="${ifDefined(this.instance?.internalHost)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`Upstream host that the requests are forwarded to.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="internalHostSslValidation">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.internalHostSslValidation, true)}>
<label class="pf-c-check__label">
${t`Internal host SSL Validation`}
</label>
</div>
<p class="pf-c-form__helper-text">${t`Validate SSL Certificates of upstream servers.`}</p>
</ak-form-element-horizontal>`;
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {
@ -115,35 +195,16 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
<p class="pf-c-form__helper-text">${t`Flow used when authorizing this provider.`}</p> <p class="pf-c-form__helper-text">${t`Flow used when authorizing this provider.`}</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group .expanded=${true}> <div class="pf-c-card pf-m-selectable pf-m-selected">
<span slot="header"> <div class="pf-c-card__body">
${t`Protocol settings`} <div class="pf-c-toggle-group">
</span> ${this.renderModeSelector()}
<div slot="body" class="pf-c-form"> </div>
<ak-form-element-horizontal
label=${t`External host`}
?required=${true}
name="externalHost">
<input type="text" value="${ifDefined(this.instance?.externalHost)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`The external URL you'll access the application at. Include any non-standard port.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="forwardAuthMode">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.forwardAuthMode, false)} @change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showInternalServer = !el.checked;
}}>
<label class="pf-c-check__label">
${t`Enable forward-auth mode`}
</label>
</div>
<p class="pf-c-form__helper-text">
${t`Enable this if you don't want to use this provider as a proxy, and want to use it with Traefik's forwardAuth or nginx's auth_request.`}
</p>
</ak-form-element-horizontal>
${this.renderInternalServer()}
</div> </div>
</ak-form-group> <div class="pf-c-card__footer">
${this.renderSettings()}
</div>
</div>
<ak-form-group> <ak-form-group>
<span slot="header"> <span slot="header">

View file

@ -20,6 +20,7 @@ import "./ProxyProviderForm";
import { ProvidersApi, ProxyProvider } from "authentik-api"; import { ProvidersApi, ProxyProvider } from "authentik-api";
import { DEFAULT_CONFIG } from "../../../api/Config"; import { DEFAULT_CONFIG } from "../../../api/Config";
import { EVENT_REFRESH } from "../../../constants"; import { EVENT_REFRESH } from "../../../constants";
import { convertToTitle } from "../../../utils";
@customElement("ak-provider-proxy-view") @customElement("ak-provider-proxy-view")
export class ProxyProviderViewPage extends LitElement { export class ProxyProviderViewPage extends LitElement {
@ -115,18 +116,11 @@ export class ProxyProviderViewPage extends LitElement {
</div> </div>
<div class="pf-c-description-list__group"> <div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term"> <dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Forward auth`}</span> <span class="pf-c-description-list__text">${t`Mode`}</span>
</dt> </dt>
<dd class="pf-c-description-list__description"> <dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text"> <div class="pf-c-description-list__text">
${this.provider.forwardAuthMode ? ${convertToTitle(this.provider.mode || "")}
html`<span class="pf-c-button__icon pf-m-start">
<i class="fas fa-check-circle" aria-hidden="true"></i>&nbsp;
</span>${t`Yes`}`:
html`<span class="pf-c-button__icon pf-m-start">
<i class="fas fa-times-circle" aria-hidden="true"></i>&nbsp;
</span>${t`No`}`
}
</div> </div>
</dd> </dd>
</div> </div>

View file

@ -31,24 +31,24 @@ ldapsearch \
The following fields are currently sent for users: The following fields are currently sent for users:
- cn: User's username - `cn`: User's username
- uid: Unique user identifier - `uid`: Unique user identifier
- name: User's name - `name`: User's name
- displayName: User's name - `displayName`: User's name
- mail: User's email address - `mail`: User's email address
- objectClass: A list of these strings: - `objectClass`: A list of these strings:
- "user" - "user"
- "organizationalPerson" - "organizationalPerson"
- "goauthentik.io/ldap/user" - "goauthentik.io/ldap/user"
- accountStatus: "active" if the account is active, otherwise "inactive" - `accountStatus`: "active" if the account is active, otherwise "inactive"
- superuser: "active" if the account is part of a group with superuser permissions, otherwise "inactive" - `superuser`: "active" if the account is part of a group with superuser permissions, otherwise "inactive"
- memberOf: A list of all DNs that the user is a member of - `memberOf`: A list of all DNs that the user is a member of
The following fields are current set for groups: The following fields are current set for groups:
- cn: The group's name - `cn`: The group's name
- uid: Unique group identifier - `uid`: Unique group identifier
- objectClass: A list of these strings: - `objectClass`: A list of these strings:
- "group" - "group"
- "goauthentik.io/ldap/group" - "goauthentik.io/ldap/group"

View file

@ -1,31 +1,36 @@
--- ---
title: Proxy Outpost title: Forward auth
--- ---
The proxy outpost sets the following headers: Using forward auth uses your existing reverse proxy to do the proxying, and only uses the
authentik outpost to check authentication and authoirzation.
``` To use forward auth instead of proxying, you have to change a couple of settings.
X-Auth-Username: akadmin # The username of the currently logged in user In the Proxy Provider, make sure to use one of the Forward auth modes.
X-Forwarded-Email: root@localhost # The email address of the currently logged in user
X-Forwarded-Preferred-Username: akadmin # The username of the currently logged in user
X-Forwarded-User: 900347b8a29876b45ca6f75722635ecfedf0e931c6022e3a29a8aa13fb5516fb # The hashed identifier of the currently logged in user.
```
Additionally, you can set `additionalHeaders` on groups or users to set additional headers. ## Single application
If you enable *Set HTTP-Basic Authentication* option, the HTTP Authorization header is being set. Single application mode works for a single application hosted on its dedicated subdomain. This
has the advantage that you can still do per-application access policies in authentik.
# HTTPS ## Domain level
The outpost listens on both 4180 for HTTP and 4443 for HTTPS. To use forward auth instead of proxying, you have to change a couple of settings.
In the Proxy Provider, make sure to use the *Forward auth (domain level)* mode.
:::warning This mode differs from the *Forward auth (single application)* mode in the following points:
If your upstream host is HTTPS, and you're not using forward auth, you need to access the outpost over HTTPS too. - You don't have to configure an application in authentik for each domain
::: - Users don't have to authorize multiple times
# Forward auth There are however also some downsides, mainly the fact that you **can't** restrict individual
applications to different users.
To use forward auth instead of proxying, you have to change a couple of settings. In the Proxy Provider, make sure to enable `Enable forward-auth mode` on the provider. The only configuration difference between single application and domain level is the host you specify.
For single application, you'd use the domain which the application is running on, and only /akprox
is redirect to the outpost.
For domain level, you'd use the same domain as authentik.
## Nginx ## Nginx
@ -42,8 +47,8 @@ import TabItem from '@theme/TabItem';
``` ```
location /akprox { location /akprox {
proxy_pass http://*ip of your outpost*:4180; proxy_pass http://*ip of your outpost*:4180;
error_page 401 = @akprox_signin; error_page 401 = @akprox_signin;
proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Host $http_host;
auth_request_set $auth_cookie $upstream_http_set_cookie; auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie; add_header Set-Cookie $auth_cookie;

View file

@ -0,0 +1,24 @@
---
title: Proxy Outpost
---
The proxy outpost sets the following headers:
```
X-Auth-Username: akadmin # The username of the currently logged in user
X-Forwarded-Email: root@localhost # The email address of the currently logged in user
X-Forwarded-Preferred-Username: akadmin # The username of the currently logged in user
X-Forwarded-User: 900347b8a29876b45ca6f75722635ecfedf0e931c6022e3a29a8aa13fb5516fb # The hashed identifier of the currently logged in user.
```
Additionally, you can set `additionalHeaders` on groups or users to set additional headers.
If you enable *Set HTTP-Basic Authentication* option, the HTTP Authorization header is being set.
# HTTPS
The outpost listens on both 4180 for HTTP and 4443 for HTTPS.
:::info
If your upstream host is HTTPS, and you're not using forward auth, you need to access the outpost over HTTPS too.
:::

View file

@ -2,6 +2,10 @@
title: Policies title: Policies
--- ---
## Event-matcher policy
This policy is used by the events subsystem. You can use this policy to match events by multiple different criteria, to choose when you get notified.
## Reputation Policy ## Reputation Policy
authentik keeps track of failed login attempts by source IP and attempted username. These values are saved as scores. Each failed login decreases the score for the client IP as well as the targeted username by 1 (one). authentik keeps track of failed login attempts by source IP and attempted username. These values are saved as scores. Each failed login decreases the score for the client IP as well as the targeted username by 1 (one).
@ -12,10 +16,6 @@ This policy can be used, for example, to prompt clients with a low score to pass
See [Expression Policy](expression.mdx). See [Expression Policy](expression.mdx).
## Password Policies
---
## Password Policy ## Password Policy
This policy allows you to specify password rules, such as length and required characters. This policy allows you to specify password rules, such as length and required characters.
@ -34,3 +34,7 @@ This policy checks the hashed password against the [Have I Been Pwned](https://h
## Password-Expiry Policy ## Password-Expiry Policy
This policy can enforce regular password rotation by expiring set passwords after a finite amount of time. This forces users to set a new password. This policy can enforce regular password rotation by expiring set passwords after a finite amount of time. This forces users to set a new password.
## Reputation Policy
This policy checks the reputation of the client's IP address and the username is attempted to be authenticated as.

View file

@ -20,7 +20,7 @@ This feature is still in technical preview, so please report any Bugs you run in
- Compatibility with forwardAuth/auth_request - Compatibility with forwardAuth/auth_request
The authentik proxy is now compatible with forwardAuth (traefik) / auth_request (nginx). All that is required is the latest version of the outpost, The authentik proxy is now compatible with forwardAuth (traefik) / auth_request (nginx). All that is required is the latest version of the outpost,
and the correct config from [here](../outposts/proxy.mdx). and the correct config from [here](../outposts/proxy/forward_auth.mdx).
- Docker images for ARM - Docker images for ARM

View file

@ -34,11 +34,30 @@ module.exports = {
label: "Outposts", label: "Outposts",
items: [ items: [
"outposts/outposts", "outposts/outposts",
"outposts/proxy", {
"outposts/ldap", type: "category",
"outposts/upgrading", label: "Running and upgrading",
"outposts/manual-deploy-docker-compose", items: [
"outposts/manual-deploy-kubernetes", "outposts/upgrading",
"outposts/manual-deploy-docker-compose",
"outposts/manual-deploy-kubernetes",
],
},
{
type: "category",
label: "Proxy",
items: [
"outposts/proxy/proxy",
"outposts/proxy/forward_auth",
],
},
{
type: "category",
label: "LDAP",
items: [
"outposts/ldap/ldap",
],
},
], ],
}, },
{ {