authentik.providers.proxy.models

authentik proxy models

  1"""authentik proxy models"""
  2
  3import string
  4from collections.abc import Iterable
  5from random import SystemRandom
  6from urllib.parse import urljoin
  7from uuid import uuid4
  8
  9from django.db import models
 10from django.templatetags.static import static
 11from django.utils.translation import gettext as _
 12from rest_framework.serializers import Serializer
 13
 14from authentik.core.models import ExpiringModel
 15from authentik.crypto.models import CertificateKeyPair
 16from authentik.lib.models import DomainlessURLValidator, InternallyManagedMixin
 17from authentik.outposts.models import OutpostModel
 18from authentik.providers.oauth2.models import (
 19    ClientTypes,
 20    OAuth2Provider,
 21    RedirectURI,
 22    RedirectURIMatchingMode,
 23    ScopeMapping,
 24)
 25
 26SCOPE_AK_PROXY = "ak_proxy"
 27OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback"
 28
 29
 30class ProxySession(InternallyManagedMixin, ExpiringModel):
 31    """Session storage for proxyv2 outposts using PostgreSQL"""
 32
 33    uuid = models.UUIDField(default=uuid4, primary_key=True)
 34    session_key = models.TextField(unique=True, db_index=True)
 35    user_id = models.UUIDField(null=True, blank=True, db_index=True)
 36
 37    session_data = models.JSONField(default=dict, blank=True)
 38
 39    class Meta:
 40        verbose_name = _("Proxy Session")
 41        verbose_name_plural = _("Proxy Sessions")
 42        indexes = [
 43            models.Index(fields=["user_id"]),
 44        ]
 45
 46    def __str__(self) -> str:
 47        return f"Session {self.session_key[:8]}..."
 48
 49
 50def get_cookie_secret():
 51    """Generate random 32-character string for cookie-secret"""
 52    return "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32))
 53
 54
 55def _get_callback_url(uri: str) -> list[RedirectURI]:
 56    return [
 57        RedirectURI(
 58            RedirectURIMatchingMode.STRICT,
 59            urljoin(uri, "outpost.goauthentik.io/callback") + f"?{OUTPOST_CALLBACK_SIGNATURE}=true",
 60        ),
 61        RedirectURI(RedirectURIMatchingMode.STRICT, uri + f"?{OUTPOST_CALLBACK_SIGNATURE}=true"),
 62    ]
 63
 64
 65class ProxyMode(models.TextChoices):
 66    """All modes a Proxy provider can operate in"""
 67
 68    PROXY = "proxy"
 69    FORWARD_SINGLE = "forward_single"
 70    FORWARD_DOMAIN = "forward_domain"
 71
 72
 73class ProxyProvider(OutpostModel, OAuth2Provider):
 74    """Protect applications that don't support any of the other
 75    Protocols by using a Reverse-Proxy."""
 76
 77    internal_host = models.TextField(
 78        validators=[DomainlessURLValidator(schemes=("http", "https"))],
 79        blank=True,
 80    )
 81    external_host = models.TextField(validators=[DomainlessURLValidator(schemes=("http", "https"))])
 82    internal_host_ssl_validation = models.BooleanField(
 83        default=True,
 84        help_text=_("Validate SSL Certificates of upstream servers"),
 85        verbose_name=_("Internal host SSL Validation"),
 86    )
 87    mode = models.TextField(
 88        default=ProxyMode.PROXY,
 89        choices=ProxyMode.choices,
 90        help_text=_(
 91            "Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with "
 92            "internal_host."
 93        ),
 94    )
 95
 96    skip_path_regex = models.TextField(
 97        default="",
 98        blank=True,
 99        help_text=_(
100            "Regular expressions for which authentication is not required. "
101            "Each new line is interpreted as a new Regular Expression."
102        ),
103    )
104
105    intercept_header_auth = models.BooleanField(
106        default=True,
107        help_text=_(
108            "When enabled, this provider will intercept the authorization header and authenticate "
109            "requests based on its value."
110        ),
111    )
112    basic_auth_enabled = models.BooleanField(
113        default=False,
114        verbose_name=_("Set HTTP-Basic Authentication"),
115        help_text=_(
116            "Set a custom HTTP-Basic Authentication header based on values from authentik."
117        ),
118    )
119    basic_auth_user_attribute = models.TextField(
120        blank=True,
121        verbose_name=_("HTTP-Basic Username Key"),
122        help_text=_(
123            "User/Group Attribute used for the user part of the HTTP-Basic Header. "
124            "If not set, the user's Email address is used."
125        ),
126    )
127    basic_auth_password_attribute = models.TextField(
128        blank=True,
129        verbose_name=_("HTTP-Basic Password Key"),
130        help_text=_("User/Group Attribute used for the password part of the HTTP-Basic Header."),
131    )
132
133    certificate = models.ForeignKey(
134        CertificateKeyPair,
135        on_delete=models.SET_NULL,
136        null=True,
137        blank=True,
138    )
139
140    cookie_secret = models.TextField(default=get_cookie_secret)
141    cookie_domain = models.TextField(default="", blank=True)
142
143    @property
144    def component(self) -> str:
145        return "ak-provider-proxy-form"
146
147    @property
148    def icon_url(self) -> str | None:
149        return static("authentik/sources/proxy.svg")
150
151    @property
152    def serializer(self) -> type[Serializer]:
153        from authentik.providers.proxy.api import ProxyProviderSerializer
154
155        return ProxyProviderSerializer
156
157    @property
158    def launch_url(self) -> str | None:
159        """Use external_host as launch URL"""
160        return self.external_host
161
162    def set_oauth_defaults(self):
163        """Ensure all OAuth2-related settings are correct"""
164        self.client_type = ClientTypes.CONFIDENTIAL
165        self.signing_key = None
166        self.include_claims_in_id_token = True
167        scopes = ScopeMapping.objects.filter(
168            managed__in=[
169                "goauthentik.io/providers/oauth2/scope-openid",
170                "goauthentik.io/providers/oauth2/scope-profile",
171                "goauthentik.io/providers/oauth2/scope-email",
172                "goauthentik.io/providers/oauth2/scope-entitlements",
173                "goauthentik.io/providers/proxy/scope-proxy",
174            ]
175        )
176        self.property_mappings.add(*list(scopes))
177        self.redirect_uris = _get_callback_url(self.external_host)
178
179    def __str__(self):
180        return f"Proxy Provider {self.name}"
181
182    def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
183        required = [self]
184        if self.certificate is not None:
185            required.append(("authentik_crypto.view_certificatekeypair", self.certificate))
186            required.append(
187                ("authentik_crypto.view_certificatekeypair_certificate", self.certificate)
188            )
189            required.append(("authentik_crypto.view_certificatekeypair_key", self.certificate))
190        return required
191
192    class Meta:
193        verbose_name = _("Proxy Provider")
194        verbose_name_plural = _("Proxy Providers")
195        authentik_used_by_shadows = ["authentik_providers_oauth2.oauth2provider"]
SCOPE_AK_PROXY = 'ak_proxy'
OUTPOST_CALLBACK_SIGNATURE = 'X-authentik-auth-callback'
31class ProxySession(InternallyManagedMixin, ExpiringModel):
32    """Session storage for proxyv2 outposts using PostgreSQL"""
33
34    uuid = models.UUIDField(default=uuid4, primary_key=True)
35    session_key = models.TextField(unique=True, db_index=True)
36    user_id = models.UUIDField(null=True, blank=True, db_index=True)
37
38    session_data = models.JSONField(default=dict, blank=True)
39
40    class Meta:
41        verbose_name = _("Proxy Session")
42        verbose_name_plural = _("Proxy Sessions")
43        indexes = [
44            models.Index(fields=["user_id"]),
45        ]
46
47    def __str__(self) -> str:
48        return f"Session {self.session_key[:8]}..."

Session storage for proxyv2 outposts using PostgreSQL

def uuid(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def session_key(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def user_id(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def session_data(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def expires(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def expiring(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

class ProxySession.DoesNotExist(django.core.exceptions.ObjectDoesNotExist):

The requested object does not exist

class ProxySession.MultipleObjectsReturned(django.core.exceptions.MultipleObjectsReturned):

The query returned multiple objects when only one was expected.

class ProxyMode(django.db.models.enums.TextChoices):
66class ProxyMode(models.TextChoices):
67    """All modes a Proxy provider can operate in"""
68
69    PROXY = "proxy"
70    FORWARD_SINGLE = "forward_single"
71    FORWARD_DOMAIN = "forward_domain"

All modes a Proxy provider can operate in

PROXY = ProxyMode.PROXY
FORWARD_SINGLE = ProxyMode.FORWARD_SINGLE
FORWARD_DOMAIN = ProxyMode.FORWARD_DOMAIN
 74class ProxyProvider(OutpostModel, OAuth2Provider):
 75    """Protect applications that don't support any of the other
 76    Protocols by using a Reverse-Proxy."""
 77
 78    internal_host = models.TextField(
 79        validators=[DomainlessURLValidator(schemes=("http", "https"))],
 80        blank=True,
 81    )
 82    external_host = models.TextField(validators=[DomainlessURLValidator(schemes=("http", "https"))])
 83    internal_host_ssl_validation = models.BooleanField(
 84        default=True,
 85        help_text=_("Validate SSL Certificates of upstream servers"),
 86        verbose_name=_("Internal host SSL Validation"),
 87    )
 88    mode = models.TextField(
 89        default=ProxyMode.PROXY,
 90        choices=ProxyMode.choices,
 91        help_text=_(
 92            "Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with "
 93            "internal_host."
 94        ),
 95    )
 96
 97    skip_path_regex = models.TextField(
 98        default="",
 99        blank=True,
100        help_text=_(
101            "Regular expressions for which authentication is not required. "
102            "Each new line is interpreted as a new Regular Expression."
103        ),
104    )
105
106    intercept_header_auth = models.BooleanField(
107        default=True,
108        help_text=_(
109            "When enabled, this provider will intercept the authorization header and authenticate "
110            "requests based on its value."
111        ),
112    )
113    basic_auth_enabled = models.BooleanField(
114        default=False,
115        verbose_name=_("Set HTTP-Basic Authentication"),
116        help_text=_(
117            "Set a custom HTTP-Basic Authentication header based on values from authentik."
118        ),
119    )
120    basic_auth_user_attribute = models.TextField(
121        blank=True,
122        verbose_name=_("HTTP-Basic Username Key"),
123        help_text=_(
124            "User/Group Attribute used for the user part of the HTTP-Basic Header. "
125            "If not set, the user's Email address is used."
126        ),
127    )
128    basic_auth_password_attribute = models.TextField(
129        blank=True,
130        verbose_name=_("HTTP-Basic Password Key"),
131        help_text=_("User/Group Attribute used for the password part of the HTTP-Basic Header."),
132    )
133
134    certificate = models.ForeignKey(
135        CertificateKeyPair,
136        on_delete=models.SET_NULL,
137        null=True,
138        blank=True,
139    )
140
141    cookie_secret = models.TextField(default=get_cookie_secret)
142    cookie_domain = models.TextField(default="", blank=True)
143
144    @property
145    def component(self) -> str:
146        return "ak-provider-proxy-form"
147
148    @property
149    def icon_url(self) -> str | None:
150        return static("authentik/sources/proxy.svg")
151
152    @property
153    def serializer(self) -> type[Serializer]:
154        from authentik.providers.proxy.api import ProxyProviderSerializer
155
156        return ProxyProviderSerializer
157
158    @property
159    def launch_url(self) -> str | None:
160        """Use external_host as launch URL"""
161        return self.external_host
162
163    def set_oauth_defaults(self):
164        """Ensure all OAuth2-related settings are correct"""
165        self.client_type = ClientTypes.CONFIDENTIAL
166        self.signing_key = None
167        self.include_claims_in_id_token = True
168        scopes = ScopeMapping.objects.filter(
169            managed__in=[
170                "goauthentik.io/providers/oauth2/scope-openid",
171                "goauthentik.io/providers/oauth2/scope-profile",
172                "goauthentik.io/providers/oauth2/scope-email",
173                "goauthentik.io/providers/oauth2/scope-entitlements",
174                "goauthentik.io/providers/proxy/scope-proxy",
175            ]
176        )
177        self.property_mappings.add(*list(scopes))
178        self.redirect_uris = _get_callback_url(self.external_host)
179
180    def __str__(self):
181        return f"Proxy Provider {self.name}"
182
183    def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
184        required = [self]
185        if self.certificate is not None:
186            required.append(("authentik_crypto.view_certificatekeypair", self.certificate))
187            required.append(
188                ("authentik_crypto.view_certificatekeypair_certificate", self.certificate)
189            )
190            required.append(("authentik_crypto.view_certificatekeypair_key", self.certificate))
191        return required
192
193    class Meta:
194        verbose_name = _("Proxy Provider")
195        verbose_name_plural = _("Proxy Providers")
196        authentik_used_by_shadows = ["authentik_providers_oauth2.oauth2provider"]

Protect applications that don't support any of the other Protocols by using a Reverse-Proxy.

def internal_host(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def external_host(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def internal_host_ssl_validation(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def mode(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def skip_path_regex(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def intercept_header_auth(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def basic_auth_enabled(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def basic_auth_user_attribute(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def basic_auth_password_attribute(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

certificate

Accessor to the related object on the forward side of a many-to-one or one-to-one (via ForwardOneToOneDescriptor subclass) relation.

In the example::

class Child(Model):
    parent = ForeignKey(Parent, related_name='children')

Child.parent is a ForwardManyToOneDescriptor instance.

def cookie_secret(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def cookie_domain(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

component: str
144    @property
145    def component(self) -> str:
146        return "ak-provider-proxy-form"

Return component used to edit this object

icon_url: str | None
148    @property
149    def icon_url(self) -> str | None:
150        return static("authentik/sources/proxy.svg")
serializer: type[rest_framework.serializers.Serializer]
152    @property
153    def serializer(self) -> type[Serializer]:
154        from authentik.providers.proxy.api import ProxyProviderSerializer
155
156        return ProxyProviderSerializer

Get serializer for this model

launch_url: str | None
158    @property
159    def launch_url(self) -> str | None:
160        """Use external_host as launch URL"""
161        return self.external_host

Use external_host as launch URL

def set_oauth_defaults(self):
163    def set_oauth_defaults(self):
164        """Ensure all OAuth2-related settings are correct"""
165        self.client_type = ClientTypes.CONFIDENTIAL
166        self.signing_key = None
167        self.include_claims_in_id_token = True
168        scopes = ScopeMapping.objects.filter(
169            managed__in=[
170                "goauthentik.io/providers/oauth2/scope-openid",
171                "goauthentik.io/providers/oauth2/scope-profile",
172                "goauthentik.io/providers/oauth2/scope-email",
173                "goauthentik.io/providers/oauth2/scope-entitlements",
174                "goauthentik.io/providers/proxy/scope-proxy",
175            ]
176        )
177        self.property_mappings.add(*list(scopes))
178        self.redirect_uris = _get_callback_url(self.external_host)

Ensure all OAuth2-related settings are correct

def get_required_objects( self) -> Iterable[django.db.models.base.Model | str | tuple[str, django.db.models.base.Model]]:
183    def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
184        required = [self]
185        if self.certificate is not None:
186            required.append(("authentik_crypto.view_certificatekeypair", self.certificate))
187            required.append(
188                ("authentik_crypto.view_certificatekeypair_certificate", self.certificate)
189            )
190            required.append(("authentik_crypto.view_certificatekeypair_key", self.certificate))
191        return required

Return a list of all required objects

def get_mode_display(unknown):

Method descriptor with partial application of the given arguments and keywords.

Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.

certificate_id
oauth2provider_ptr_id
oauth2provider_ptr

Accessor to the related object on the forward side of a one-to-one relation.

In the example::

class Restaurant(Model):
    place = OneToOneField(Place, related_name='restaurant')

Restaurant.place is a ForwardOneToOneDescriptor instance.

The requested object does not exist

class ProxyProvider.MultipleObjectsReturned(authentik.providers.oauth2.models.OAuth2Provider.MultipleObjectsReturned):

The query returned multiple objects when only one was expected.