Merge branch 'master' into version-2021.2

This commit is contained in:
Jens Langhammer 2021-02-07 19:04:37 +01:00
commit d93927755a
43 changed files with 3333 additions and 1126 deletions

View File

@ -16,6 +16,14 @@ updates:
open-pull-requests-limit: 10 open-pull-requests-limit: 10
assignees: assignees:
- BeryJu - BeryJu
- package-ecosystem: npm
directory: "/website"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
assignees:
- BeryJu
- package-ecosystem: pip - package-ecosystem: pip
directory: "/" directory: "/"
schedule: schedule:

17
Pipfile.lock generated
View File

@ -53,10 +53,10 @@
}, },
"autobahn": { "autobahn": {
"hashes": [ "hashes": [
"sha256:410a93e0e29882c8b5d5ab05d220b07609b886ef5f23c0b8d39153254ffd6895", "sha256:93df8fc9d1821c9dabff9fed52181a9ad6eea5e9989d53102c391607d7c1666e",
"sha256:52ee4236ff9a1fcbbd9500439dcf3284284b37f8a6b31ecc8a36e00cf9f95049" "sha256:cceed2121b7a93024daa93c91fae33007f8346f0e522796421f36a6183abea99"
], ],
"version": "==20.12.3" "version": "==21.1.1"
}, },
"automat": { "automat": {
"hashes": [ "hashes": [
@ -74,17 +74,18 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:1a282c1cd7d5028cbb3a75d747df32162295253f55d263ac85840e264830963b" "sha256:92041aa7589c886020cabd80eb58b89ace2f0094571792fccae24b9a8b3b97d7",
"sha256:9f132c34e20110dea019293c89cede49b0a56be615b3e1debf98390ed9f1f7b9"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.2" "version": "==1.17.3"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:7442fdbbdc841bfac7f94f92ecb807de070e32ed205743eb72d4ea27c5e8e778", "sha256:1dae84c68b109f596f58cc2e9fa87704ccd40dcbc12144a89205f85efa7f9135",
"sha256:bf587b044983a91a0124cc133ff167b8528c19fbbc8f0b956d9a1ac256cad7d7" "sha256:a0fdded1c9636899ab273f50bf123f79b91439a8c282b5face8b5f4a48b493cb"
], ],
"version": "==1.20.2" "version": "==1.20.3"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [

View File

@ -0,0 +1,14 @@
{% extends base_template|default:"generic/form.html" %}
{% load authentik_utils %}
{% load i18n %}
{% block above_form %}
<h1>
{% trans 'Generate Certificate-Key Pair' %}
</h1>
{% endblock %}
{% block action %}
{% trans 'Generate Certificate-Key Pair' %}
{% endblock %}

View File

@ -26,6 +26,12 @@
</ak-spinner-button> </ak-spinner-button>
<div slot="modal"></div> <div slot="modal"></div>
</ak-modal-button> </ak-modal-button>
<ak-modal-button href="{% url 'authentik_admin:certificatekeypair-generate' %}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
{% trans 'Generate' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<button role="ak-refresh" class="pf-c-button pf-m-primary"> <button role="ak-refresh" class="pf-c-button pf-m-primary">
{% trans 'Refresh' %} {% trans 'Refresh' %}
</button> </button>

View File

@ -14,7 +14,7 @@
<span class="pf-c-form__label-text">{% trans 'Passing' %}</span> <span class="pf-c-form__label-text">{% trans 'Passing' %}</span>
</label> </label>
</div> </div>
<div class="pf-c-form__group-control"> <div class="pf-c-form__group-label">
<div class="c-form__horizontal-group"> <div class="c-form__horizontal-group">
<span class="pf-c-form__label-text">{{ result.passing|yesno:"Yes,No" }}</span> <span class="pf-c-form__label-text">{{ result.passing|yesno:"Yes,No" }}</span>
</div> </div>
@ -26,7 +26,7 @@
<span class="pf-c-form__label-text">{% trans 'Messages' %}</span> <span class="pf-c-form__label-text">{% trans 'Messages' %}</span>
</label> </label>
</div> </div>
<div class="pf-c-form__group-control"> <div class="pf-c-form__group-label">
<div class="c-form__horizontal-group"> <div class="c-form__horizontal-group">
<ul> <ul>
{% for m in result.messages %} {% for m in result.messages %}

View File

@ -1,181 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load authentik_utils %}
{% load admin_reflection %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-integration"></i>
{% trans 'Providers' %}
</h1>
<p>{% trans "Provide support for protocols like SAML and OAuth to assigned applications." %}
</p>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<ak-modal-button href="{% url 'authentik_admin:provider-create' %}?type={{ type }}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
{% endfor %}
<li>
<ak-modal-button href="{% url 'authentik_admin:provider-saml-from-metadata' %}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
{% trans 'SAML Provider from Metadata' %}<br>
<small>
{% trans "Create a SAML Provider by importing its Metadata." %}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
</ul>
</ak-dropdown>
<button role="ak-refresh" class="pf-c-button pf-m-primary">
{% trans 'Refresh' %}
</button>
</div>
{% include 'partials/pagination.html' %}
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for provider in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<div>{{ provider.name }}</div>
{% if not provider.application %}
<i class="pf-icon pf-icon-warning-triangle"></i>
<small>{% trans 'Warning: Provider not assigned to any application.' %}</small>
{% else %}
<i class="pf-icon pf-icon-ok"></i>
<small>
{% blocktrans with app=provider.application %}
Assigned to application {{ app }}.
{% endblocktrans %}
</small>
{% endif %}
</div>
</th>
<td role="cell">
<span>
{{ provider|verbose_name }}
</span>
</td>
<td>
<ak-modal-button href="{% url 'authentik_admin:provider-update' pk=provider.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
{% trans 'Edit' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="{% url 'authentik_admin:provider-delete' pk=provider.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
{% trans 'Delete' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
{% get_links provider as links %}
{% for name, href in links.items %}
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
{% get_htmls provider as htmls %}
{% for html in htmls %}
{{ html|safe }}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="pf-icon-integration pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Providers.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any providers." %}
{% else %}
{% trans 'Currently no providers exist. Click the button below to create one.' %}
{% endif %}
</div>
<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<ak-modal-button href="{% url 'authentik_admin:provider-create' %}?type={{ type }}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
{% endfor %}
<li>
<ak-modal-button href="{% url 'authentik_admin:provider-saml-from-metadata' %}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
{% trans 'SAML Provider from Metadata' %}<br>
<small>
{% trans "Create a SAML Provider by importing its Metadata." %}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
</ul>
</ak-dropdown>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View File

@ -24,7 +24,7 @@ from authentik.admin.views import (
tokens, tokens,
users, users,
) )
from authentik.providers.saml.views import MetadataImportView from authentik.providers.saml.views.metadata import MetadataImportView
urlpatterns = [ urlpatterns = [
path( path(
@ -113,7 +113,6 @@ urlpatterns = [
name="policy-binding-delete", name="policy-binding-delete",
), ),
# Providers # Providers
path("providers/", providers.ProviderListView.as_view(), name="providers"),
path( path(
"providers/create/", "providers/create/",
providers.ProviderCreateView.as_view(), providers.ProviderCreateView.as_view(),
@ -296,6 +295,11 @@ urlpatterns = [
certificate_key_pair.CertificateKeyPairCreateView.as_view(), certificate_key_pair.CertificateKeyPairCreateView.as_view(),
name="certificatekeypair-create", name="certificatekeypair-create",
), ),
path(
"crypto/certificates/generate/",
certificate_key_pair.CertificateKeyPairGenerateView.as_view(),
name="certificatekeypair-generate",
),
path( path(
"crypto/certificates/<uuid:pk>/update/", "crypto/certificates/<uuid:pk>/update/",
certificate_key_pair.CertificateKeyPairUpdateView.as_view(), certificate_key_pair.CertificateKeyPairUpdateView.as_view(),
@ -329,22 +333,22 @@ urlpatterns = [
), ),
# Outpost Service Connections # Outpost Service Connections
path( path(
"outposts/service_connections/", "outpost_service_connections/",
outposts_service_connections.OutpostServiceConnectionListView.as_view(), outposts_service_connections.OutpostServiceConnectionListView.as_view(),
name="outpost-service-connections", name="outpost-service-connections",
), ),
path( path(
"outposts/service_connections/create/", "outpost_service_connections/create/",
outposts_service_connections.OutpostServiceConnectionCreateView.as_view(), outposts_service_connections.OutpostServiceConnectionCreateView.as_view(),
name="outpost-service-connection-create", name="outpost-service-connection-create",
), ),
path( path(
"outposts/service_connections/<uuid:pk>/update/", "outpost_service_connections/<uuid:pk>/update/",
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(), outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
name="outpost-service-connection-update", name="outpost-service-connection-update",
), ),
path( path(
"outposts/service_connections/<uuid:pk>/delete/", "outpost_service_connections/<uuid:pk>/delete/",
outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(), outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(),
name="outpost-service-connection-delete", name="outpost-service-connection-delete",
), ),

View File

@ -4,9 +4,11 @@ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin, PermissionRequiredMixin as DjangoPermissionRequiredMixin,
) )
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http.response import HttpResponse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView from django.views.generic import ListView, UpdateView
from django.views.generic.edit import FormView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from authentik.admin.views.utils import ( from authentik.admin.views.utils import (
@ -15,7 +17,11 @@ from authentik.admin.views.utils import (
SearchListMixin, SearchListMixin,
UserPaginateListMixin, UserPaginateListMixin,
) )
from authentik.crypto.forms import CertificateKeyPairForm from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.forms import (
CertificateKeyPairForm,
CertificateKeyPairGenerateForm,
)
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView
@ -52,7 +58,35 @@ class CertificateKeyPairCreateView(
template_name = "generic/create.html" template_name = "generic/create.html"
success_url = reverse_lazy("authentik_admin:certificate_key_pair") success_url = reverse_lazy("authentik_admin:certificate_key_pair")
success_message = _("Successfully created CertificateKeyPair") success_message = _("Successfully created Certificate-Key Pair")
class CertificateKeyPairGenerateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
FormView,
):
"""Generate new CertificateKeyPair"""
model = CertificateKeyPair
form_class = CertificateKeyPairGenerateForm
permission_required = "authentik_crypto.add_certificatekeypair"
template_name = "administration/certificatekeypair/generate.html"
success_url = reverse_lazy("authentik_admin:certificate_key_pair")
success_message = _("Successfully generated Certificate-Key Pair")
def form_valid(self, form: CertificateKeyPairGenerateForm) -> HttpResponse:
builder = CertificateBuilder()
builder.common_name = form.data["common_name"]
builder.build(
subject_alt_names=form.data.get("subject_alt_name", "").split(","),
validity_days=int(form.data["validity_days"]),
)
builder.save()
return super().form_valid(form)
class CertificateKeyPairUpdateView( class CertificateKeyPairUpdateView(

View File

@ -3,6 +3,7 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache from django.core.cache import cache
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.urls.base import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView from django.views.generic import FormView
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -20,7 +21,7 @@ class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
form_class = PolicyCacheClearForm form_class = PolicyCacheClearForm
template_name = "generic/form_non_model.html" template_name = "generic/form_non_model.html"
success_url = "/" success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully cleared Policy cache") success_message = _("Successfully cleared Policy cache")
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
@ -39,7 +40,7 @@ class FlowCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
form_class = FlowCacheClearForm form_class = FlowCacheClearForm
template_name = "generic/form_non_model.html" template_name = "generic/form_non_model.html"
success_url = "/" success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully cleared Flow cache") success_message = _("Successfully cleared Flow cache")
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@ -115,6 +115,7 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
user = form.cleaned_data.get("user") user = form.cleaned_data.get("user")
p_request = PolicyRequest(user) p_request = PolicyRequest(user)
p_request.debug = True
p_request.http_request = self.request p_request.http_request = self.request
p_request.context = form.cleaned_data.get("context", {}) p_request.context = form.cleaned_data.get("context", {})

View File

@ -37,7 +37,7 @@ class PropertyMappingCreateView(
permission_required = "authentik_core.add_propertymapping" permission_required = "authentik_core.add_propertymapping"
template_name = "generic/create.html" template_name = "generic/create.html"
success_url = reverse_lazy("authentik_admin:property-mappings") success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully created Property Mapping") success_message = _("Successfully created Property Mapping")
@ -54,7 +54,7 @@ class PropertyMappingUpdateView(
permission_required = "authentik_core.change_propertymapping" permission_required = "authentik_core.change_propertymapping"
template_name = "generic/update.html" template_name = "generic/update.html"
success_url = reverse_lazy("authentik_admin:property-mappings") success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully updated Property Mapping") success_message = _("Successfully updated Property Mapping")
@ -67,7 +67,7 @@ class PropertyMappingDeleteView(
permission_required = "authentik_core.delete_propertymapping" permission_required = "authentik_core.delete_propertymapping"
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_admin:property-mappings") success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully deleted Property Mapping") success_message = _("Successfully deleted Property Mapping")

View File

@ -6,36 +6,17 @@ from django.contrib.auth.mixins import (
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from guardian.mixins import PermissionRequiredMixin
from authentik.admin.views.utils import ( from authentik.admin.views.utils import (
BackSuccessUrlMixin, BackSuccessUrlMixin,
DeleteMessageView, DeleteMessageView,
InheritanceCreateView, InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView, InheritanceUpdateView,
SearchListMixin,
UserPaginateListMixin,
) )
from authentik.core.models import Provider from authentik.core.models import Provider
class ProviderListView(
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
InheritanceListView,
):
"""Show list of all providers"""
model = Provider
permission_required = "authentik_core.add_provider"
template_name = "administration/provider/list.html"
ordering = "pk"
search_fields = ["pk", "name"]
class ProviderCreateView( class ProviderCreateView(
SuccessMessageMixin, SuccessMessageMixin,
BackSuccessUrlMixin, BackSuccessUrlMixin,

View File

@ -1,6 +1,7 @@
"""Create self-signed certificates""" """Create self-signed certificates"""
import datetime import datetime
import uuid import uuid
from typing import Optional
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
@ -8,6 +9,9 @@ from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID
from authentik import __version__
from authentik.crypto.models import CertificateKeyPair
class CertificateBuilder: class CertificateBuilder:
"""Build self-signed certificates""" """Build self-signed certificates"""
@ -17,19 +21,39 @@ class CertificateBuilder:
__builder = None __builder = None
__certificate = None __certificate = None
common_name: str
def __init__(self): def __init__(self):
self.__public_key = None self.__public_key = None
self.__private_key = None self.__private_key = None
self.__builder = None self.__builder = None
self.__certificate = None self.__certificate = None
self.common_name = "authentik Self-signed Certificate"
def build(self): def save(self) -> Optional[CertificateKeyPair]:
"""Save generated certificate as model"""
if not self.__certificate:
return None
return CertificateKeyPair.objects.create(
name=self.common_name,
certificate_data=self.certificate,
key_data=self.private_key,
)
def build(
self,
validity_days: int = 365,
subject_alt_names: Optional[list[str]] = None,
):
"""Build self-signed certificate""" """Build self-signed certificate"""
one_day = datetime.timedelta(1, 0, 0) one_day = datetime.timedelta(1, 0, 0)
self.__private_key = rsa.generate_private_key( self.__private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend() public_exponent=65537, key_size=2048, backend=default_backend()
) )
self.__public_key = self.__private_key.public_key() self.__public_key = self.__private_key.public_key()
alt_names: list[x509.GeneralName] = [
x509.DNSName(x) for x in subject_alt_names or []
]
self.__builder = ( self.__builder = (
x509.CertificateBuilder() x509.CertificateBuilder()
.subject_name( .subject_name(
@ -37,7 +61,7 @@ class CertificateBuilder:
[ [
x509.NameAttribute( x509.NameAttribute(
NameOID.COMMON_NAME, NameOID.COMMON_NAME,
"authentik Self-signed Certificate", self.common_name,
), ),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "authentik"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "authentik"),
x509.NameAttribute( x509.NameAttribute(
@ -51,13 +75,16 @@ class CertificateBuilder:
[ [
x509.NameAttribute( x509.NameAttribute(
NameOID.COMMON_NAME, NameOID.COMMON_NAME,
"authentik Self-signed Certificate", f"authentik {__version__}",
), ),
] ]
) )
) )
.add_extension(x509.SubjectAlternativeName(alt_names), critical=True)
.not_valid_before(datetime.datetime.today() - one_day) .not_valid_before(datetime.datetime.today() - one_day)
.not_valid_after(datetime.datetime.today() + datetime.timedelta(days=365)) .not_valid_after(
datetime.datetime.today() + datetime.timedelta(days=validity_days)
)
.serial_number(int(uuid.uuid4())) .serial_number(int(uuid.uuid4()))
.public_key(self.__public_key) .public_key(self.__public_key)
) )

View File

@ -8,6 +8,14 @@ from django.utils.translation import gettext_lazy as _
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
class CertificateKeyPairGenerateForm(forms.Form):
"""CertificateKeyPair generation form"""
common_name = forms.CharField()
subject_alt_name = forms.CharField(required=False, label=_("Subject-alt name"))
validity_days = forms.IntegerField(initial=365)
class CertificateKeyPairForm(forms.ModelForm): class CertificateKeyPairForm(forms.ModelForm):
"""CertificateKeyPair Form""" """CertificateKeyPair Form"""

View File

@ -67,7 +67,7 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
# Create the notification objects # Create the notification objects
for transport in trigger.transports.all(): for transport in trigger.transports.all():
for user in trigger.group.users.all(): for user in trigger.group.users.all():
LOGGER.debug("created notif") LOGGER.debug("created notification")
notification = Notification.objects.create( notification = Notification.objects.create(
severity=trigger.severity, body=event.summary, event=event, user=user severity=trigger.severity, body=event.summary, event=event, user=user
) )

View File

@ -80,7 +80,7 @@ class PolicyProcess(PROCESS_CLASS):
) )
try: try:
policy_result = self.binding.policy.passes(self.request) policy_result = self.binding.policy.passes(self.request)
if self.binding.policy.execution_logging: if self.binding.policy.execution_logging and not self.request.debug:
self.create_event( self.create_event(
EventAction.POLICY_EXECUTION, EventAction.POLICY_EXECUTION,
message="Policy Execution", message="Policy Execution",
@ -94,7 +94,8 @@ class PolicyProcess(PROCESS_CLASS):
+ "".join(format_tb(src_exc.__traceback__)) + "".join(format_tb(src_exc.__traceback__))
+ str(src_exc) + str(src_exc)
) )
# Create policy exception event # Create policy exception event, only when we're not debugging
if not self.request.debug:
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string) self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
LOGGER.debug("P_ENG(proc): error", exc=src_exc) LOGGER.debug("P_ENG(proc): error", exc=src_exc)
policy_result = PolicyResult(False, str(src_exc)) policy_result = PolicyResult(False, str(src_exc))

View File

@ -20,6 +20,7 @@ class PolicyRequest:
http_request: Optional[HttpRequest] http_request: Optional[HttpRequest]
obj: Optional[Model] obj: Optional[Model]
context: dict[str, Any] context: dict[str, Any]
debug: bool = False
def __init__(self, user: User): def __init__(self, user: User):
super().__init__() super().__init__()

View File

@ -11,7 +11,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.utils import MetaNameSerializer from authentik.core.api.utils import MetaNameSerializer
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.views import DescriptorDownloadView from authentik.providers.saml.views.metadata import DescriptorDownloadView
class SAMLProviderSerializer(ProviderSerializer): class SAMLProviderSerializer(ProviderSerializer):

View File

@ -19,6 +19,7 @@ class SAMLProviderManager(ObjectManager):
name="authentik default SAML Mapping: UPN", name="authentik default SAML Mapping: UPN",
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
expression="return user.attributes.get('upn', user.email)", expression="return user.attributes.get('upn', user.email)",
friendly_name="",
), ),
EnsureExists( EnsureExists(
SAMLPropertyMapping, SAMLPropertyMapping,
@ -26,6 +27,7 @@ class SAMLProviderManager(ObjectManager):
name="authentik default SAML Mapping: Name", name="authentik default SAML Mapping: Name",
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
expression="return user.name", expression="return user.name",
friendly_name="",
), ),
EnsureExists( EnsureExists(
SAMLPropertyMapping, SAMLPropertyMapping,
@ -33,6 +35,7 @@ class SAMLProviderManager(ObjectManager):
name="authentik default SAML Mapping: Email", name="authentik default SAML Mapping: Email",
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
expression="return user.email", expression="return user.email",
friendly_name="",
), ),
EnsureExists( EnsureExists(
SAMLPropertyMapping, SAMLPropertyMapping,
@ -40,6 +43,7 @@ class SAMLProviderManager(ObjectManager):
name="authentik default SAML Mapping: Username", name="authentik default SAML Mapping: Username",
saml_name="http://schemas.goauthentik.io/2021/02/saml/username", saml_name="http://schemas.goauthentik.io/2021/02/saml/username",
expression="return user.username", expression="return user.username",
friendly_name="",
), ),
EnsureExists( EnsureExists(
SAMLPropertyMapping, SAMLPropertyMapping,
@ -47,6 +51,7 @@ class SAMLProviderManager(ObjectManager):
name="authentik default SAML Mapping: User ID", name="authentik default SAML Mapping: User ID",
saml_name="http://schemas.goauthentik.io/2021/02/saml/uid", saml_name="http://schemas.goauthentik.io/2021/02/saml/uid",
expression="return user.pk", expression="return user.pk",
friendly_name="",
), ),
EnsureExists( EnsureExists(
SAMLPropertyMapping, SAMLPropertyMapping,
@ -54,6 +59,7 @@ class SAMLProviderManager(ObjectManager):
name="authentik default SAML Mapping: Groups", name="authentik default SAML Mapping: Groups",
saml_name="http://schemas.xmlsoap.org/claims/Group", saml_name="http://schemas.xmlsoap.org/claims/Group",
expression=GROUP_EXPRESSION, expression=GROUP_EXPRESSION,
friendly_name="",
), ),
EnsureExists( EnsureExists(
SAMLPropertyMapping, SAMLPropertyMapping,
@ -63,5 +69,6 @@ class SAMLProviderManager(ObjectManager):
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
), ),
expression="return user.username", expression="return user.username",
friendly_name="",
), ),
] ]

View File

@ -1,29 +1,29 @@
"""authentik SAML IDP URLs""" """authentik SAML IDP URLs"""
from django.urls import path from django.urls import path
from authentik.providers.saml import views from authentik.providers.saml.views import metadata, sso
urlpatterns = [ urlpatterns = [
# SSO Bindings # SSO Bindings
path( path(
"<slug:application_slug>/sso/binding/redirect/", "<slug:application_slug>/sso/binding/redirect/",
views.SAMLSSOBindingRedirectView.as_view(), sso.SAMLSSOBindingRedirectView.as_view(),
name="sso-redirect", name="sso-redirect",
), ),
path( path(
"<slug:application_slug>/sso/binding/post/", "<slug:application_slug>/sso/binding/post/",
views.SAMLSSOBindingPOSTView.as_view(), sso.SAMLSSOBindingPOSTView.as_view(),
name="sso-post", name="sso-post",
), ),
# SSO IdP Initiated # SSO IdP Initiated
path( path(
"<slug:application_slug>/sso/binding/init/", "<slug:application_slug>/sso/binding/init/",
views.SAMLSSOBindingInitView.as_view(), sso.SAMLSSOBindingInitView.as_view(),
name="sso-init", name="sso-init",
), ),
path( path(
"<slug:application_slug>/metadata/", "<slug:application_slug>/metadata/",
views.DescriptorDownloadView.as_view(), metadata.DescriptorDownloadView.as_view(),
name="metadata", name="metadata",
), ),
] ]

View File

@ -1,284 +0,0 @@
"""authentik SAML IDP Views"""
from typing import Optional
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.validators import URLValidator
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls.base import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.edit import FormView
from structlog.stdlib import get_logger
from authentik.core.models import Application, Provider
from authentik.events.models import Event, EventAction
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_APPLICATION,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from authentik.flows.stage import StageView
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.forms import SAMLProviderImportForm
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.metadata import MetadataProcessor
from authentik.providers.saml.processors.metadata_parser import (
ServiceProviderMetadataParser,
)
from authentik.providers.saml.processors.request_parser import (
AuthNRequest,
AuthNRequestParser,
)
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode, nice64
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE
LOGGER = get_logger()
URL_VALIDATOR = URLValidator(schemes=("http", "https"))
REQUEST_KEY_SAML_REQUEST = "SAMLRequest"
REQUEST_KEY_SAML_SIGNATURE = "Signature"
REQUEST_KEY_SAML_SIG_ALG = "SigAlg"
REQUEST_KEY_SAML_RESPONSE = "SAMLResponse"
REQUEST_KEY_RELAY_STATE = "RelayState"
SESSION_KEY_AUTH_N_REQUEST = "authn_request"
class SAMLSSOView(PolicyAccessView):
""" "SAML SSO Base View, which plans a flow and injects our final stage.
Calls get/post handler."""
def resolve_provider_application(self):
self.application = get_object_or_404(
Application, slug=self.kwargs["application_slug"]
)
self.provider: SAMLProvider = get_object_or_404(
SAMLProvider, pk=self.application.provider_id
)
def check_saml_request(self) -> Optional[HttpRequest]:
"""Handler to verify the SAML Request. Must be implemented by a subclass"""
raise NotImplementedError
# pylint: disable=unused-argument
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
# Call the method handler, which checks the SAML
# Request and returns a HTTP Response on error
method_response = self.check_saml_request()
if method_response:
return method_response
# Regardless, we start the planner and return to it
planner = FlowPlanner(self.provider.authorization_flow)
planner.allow_empty_flows = True
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
PLAN_CONTEXT_CONSENT_TEMPLATE: "providers/saml/consent.html",
},
)
plan.append(in_memory_stage(SAMLFlowFinalView))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_flows:flow-executor-shell",
request.GET,
flow_slug=self.provider.authorization_flow.slug,
)
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""GET and POST use the same handler, but we can't
override .dispatch easily because PolicyAccessView's dispatch"""
return self.get(request, application_slug)
class SAMLSSOBindingRedirectView(SAMLSSOView):
"""SAML Handler for SSO/Redirect bindings, which are sent via GET"""
def check_saml_request(self) -> Optional[HttpRequest]:
"""Handle REDIRECT bindings"""
if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
LOGGER.info("handle_saml_request: SAML payload missing")
return bad_request_message(
self.request, "The SAML request payload is missing."
)
try:
auth_n_request = AuthNRequestParser(self.provider).parse_detached(
self.request.GET[REQUEST_KEY_SAML_REQUEST],
self.request.GET.get(REQUEST_KEY_RELAY_STATE),
self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
)
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
except CannotHandleAssertion as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
provider=self.provider,
message=str(exc),
).save()
LOGGER.info(str(exc))
return bad_request_message(self.request, str(exc))
return None
@method_decorator(csrf_exempt, name="dispatch")
class SAMLSSOBindingPOSTView(SAMLSSOView):
"""SAML Handler for SSO/POST bindings"""
def check_saml_request(self) -> Optional[HttpRequest]:
"""Handle POST bindings"""
if REQUEST_KEY_SAML_REQUEST not in self.request.POST:
LOGGER.info("check_saml_request: SAML payload missing")
return bad_request_message(
self.request, "The SAML request payload is missing."
)
try:
auth_n_request = AuthNRequestParser(self.provider).parse(
self.request.POST[REQUEST_KEY_SAML_REQUEST],
self.request.POST.get(REQUEST_KEY_RELAY_STATE),
)
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
except CannotHandleAssertion as exc:
LOGGER.info(str(exc))
return bad_request_message(self.request, str(exc))
return None
class SAMLSSOBindingInitView(SAMLSSOView):
"""SAML Handler for for IdP Initiated login flows"""
def check_saml_request(self) -> Optional[HttpRequest]:
"""Create SAML Response from scratch"""
LOGGER.debug(
"handle_saml_no_request: No SAML Request, using IdP-initiated flow."
)
auth_n_request = AuthNRequestParser(self.provider).idp_initiated()
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
# This View doesn't have a URL on purpose, as its called by the FlowExecutor
class SAMLFlowFinalView(StageView):
"""View used by FlowExecutor after all stages have passed. Logs the authorization,
and redirects to the SP (if REDIRECT is configured) or shows and auto-submit for
(if POST is configured)."""
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
provider: SAMLProvider = get_object_or_404(
SAMLProvider, pk=application.provider_id
)
# Log Application Authorization
Event.new(
EventAction.AUTHORIZE_APPLICATION,
authorized_application=application,
flow=self.executor.plan.flow_pk,
).from_http(self.request)
if SESSION_KEY_AUTH_N_REQUEST not in self.request.session:
return self.executor.stage_invalid()
auth_n_request: AuthNRequest = self.request.session.pop(
SESSION_KEY_AUTH_N_REQUEST
)
response = AssertionProcessor(
provider, request, auth_n_request
).build_response()
if provider.sp_binding == SAMLBindings.POST:
form_attrs = {
"ACSUrl": provider.acs_url,
REQUEST_KEY_SAML_RESPONSE: nice64(response),
}
if auth_n_request.relay_state:
form_attrs[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
return render(
self.request,
"generic/autosubmit_form.html",
{
"url": provider.acs_url,
"title": _("Redirecting to %(app)s..." % {"app": application.name}),
"attrs": form_attrs,
},
)
if provider.sp_binding == SAMLBindings.REDIRECT:
url_args = {
REQUEST_KEY_SAML_RESPONSE: deflate_and_base64_encode(response),
}
if auth_n_request.relay_state:
url_args[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
querystring = urlencode(url_args)
return redirect(f"{provider.acs_url}?{querystring}")
return bad_request_message(request, "Invalid sp_binding specified")
class DescriptorDownloadView(View):
"""Replies with the XML Metadata IDSSODescriptor."""
@staticmethod
def get_metadata(request: HttpRequest, provider: SAMLProvider) -> str:
"""Return rendered XML Metadata"""
return MetadataProcessor(provider, request).build_entity_descriptor()
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""Replies with the XML Metadata IDSSODescriptor."""
application = get_object_or_404(Application, slug=application_slug)
provider: SAMLProvider = get_object_or_404(
SAMLProvider, pk=application.provider_id
)
try:
metadata = DescriptorDownloadView.get_metadata(request, provider)
except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member
return bad_request_message(
request, "Provider is not assigned to an application."
)
else:
response = HttpResponse(metadata, content_type="application/xml")
response[
"Content-Disposition"
] = f'attachment; filename="{provider.name}_authentik_meta.xml"'
return response
class MetadataImportView(LoginRequiredMixin, FormView):
"""Import Metadata from XML, and create provider"""
form_class = SAMLProviderImportForm
template_name = "providers/saml/import.html"
success_url = reverse_lazy("authentik_admin:providers")
def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser:
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form: SAMLProviderImportForm) -> HttpResponse:
try:
metadata = ServiceProviderMetadataParser().parse(
form.cleaned_data["metadata"].read().decode()
)
metadata.to_provider(
form.cleaned_data["provider_name"],
form.cleaned_data["authorization_flow"],
)
messages.success(self.request, _("Successfully created Provider"))
except ValueError as exc:
LOGGER.warning(str(exc))
messages.error(
self.request,
_("Failed to import Metadata: %(message)s" % {"message": str(exc)}),
)
return super().form_invalid(form)
return super().form_valid(form)

View File

@ -0,0 +1,82 @@
"""authentik SAML IDP Views"""
from django.core.validators import URLValidator
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger
from authentik.core.models import Application
from authentik.events.models import Event, EventAction
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION
from authentik.flows.stage import StageView
from authentik.lib.views import bad_request_message
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.request_parser import AuthNRequest
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode, nice64
LOGGER = get_logger()
URL_VALIDATOR = URLValidator(schemes=("http", "https"))
REQUEST_KEY_SAML_REQUEST = "SAMLRequest"
REQUEST_KEY_SAML_SIGNATURE = "Signature"
REQUEST_KEY_SAML_SIG_ALG = "SigAlg"
REQUEST_KEY_SAML_RESPONSE = "SAMLResponse"
REQUEST_KEY_RELAY_STATE = "RelayState"
SESSION_KEY_AUTH_N_REQUEST = "authn_request"
# This View doesn't have a URL on purpose, as its called by the FlowExecutor
class SAMLFlowFinalView(StageView):
"""View used by FlowExecutor after all stages have passed. Logs the authorization,
and redirects to the SP (if REDIRECT is configured) or shows and auto-submit for
(if POST is configured)."""
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
provider: SAMLProvider = get_object_or_404(
SAMLProvider, pk=application.provider_id
)
# Log Application Authorization
Event.new(
EventAction.AUTHORIZE_APPLICATION,
authorized_application=application,
flow=self.executor.plan.flow_pk,
).from_http(self.request)
if SESSION_KEY_AUTH_N_REQUEST not in self.request.session:
return self.executor.stage_invalid()
auth_n_request: AuthNRequest = self.request.session.pop(
SESSION_KEY_AUTH_N_REQUEST
)
response = AssertionProcessor(
provider, request, auth_n_request
).build_response()
if provider.sp_binding == SAMLBindings.POST:
form_attrs = {
"ACSUrl": provider.acs_url,
REQUEST_KEY_SAML_RESPONSE: nice64(response),
}
if auth_n_request.relay_state:
form_attrs[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
return render(
self.request,
"generic/autosubmit_form.html",
{
"url": provider.acs_url,
"title": _("Redirecting to %(app)s..." % {"app": application.name}),
"attrs": form_attrs,
},
)
if provider.sp_binding == SAMLBindings.REDIRECT:
url_args = {
REQUEST_KEY_SAML_RESPONSE: deflate_and_base64_encode(response),
}
if auth_n_request.relay_state:
url_args[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
querystring = urlencode(url_args)
return redirect(f"{provider.acs_url}?{querystring}")
return bad_request_message(request, "Invalid sp_binding specified")

View File

@ -0,0 +1,82 @@
"""authentik SAML IDP Views"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.urls.base import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic.edit import FormView
from structlog.stdlib import get_logger
from authentik.core.models import Application, Provider
from authentik.lib.views import bad_request_message
from authentik.providers.saml.forms import SAMLProviderImportForm
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.processors.metadata import MetadataProcessor
from authentik.providers.saml.processors.metadata_parser import (
ServiceProviderMetadataParser,
)
LOGGER = get_logger()
class DescriptorDownloadView(View):
"""Replies with the XML Metadata IDSSODescriptor."""
@staticmethod
def get_metadata(request: HttpRequest, provider: SAMLProvider) -> str:
"""Return rendered XML Metadata"""
return MetadataProcessor(provider, request).build_entity_descriptor()
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""Replies with the XML Metadata IDSSODescriptor."""
application = get_object_or_404(Application, slug=application_slug)
provider: SAMLProvider = get_object_or_404(
SAMLProvider, pk=application.provider_id
)
try:
metadata = DescriptorDownloadView.get_metadata(request, provider)
except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member
return bad_request_message(
request, "Provider is not assigned to an application."
)
else:
response = HttpResponse(metadata, content_type="application/xml")
response[
"Content-Disposition"
] = f'attachment; filename="{provider.name}_authentik_meta.xml"'
return response
class MetadataImportView(LoginRequiredMixin, FormView):
"""Import Metadata from XML, and create provider"""
form_class = SAMLProviderImportForm
template_name = "providers/saml/import.html"
success_url = reverse_lazy("authentik_admin:providers")
def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser:
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form: SAMLProviderImportForm) -> HttpResponse:
try:
metadata = ServiceProviderMetadataParser().parse(
form.cleaned_data["metadata"].read().decode()
)
metadata.to_provider(
form.cleaned_data["provider_name"],
form.cleaned_data["authorization_flow"],
)
messages.success(self.request, _("Successfully created Provider"))
except ValueError as exc:
LOGGER.warning(str(exc))
messages.error(
self.request,
_("Failed to import Metadata: %(message)s" % {"message": str(exc)}),
)
return super().form_invalid(form)
return super().form_valid(form)

View File

@ -0,0 +1,150 @@
"""authentik SAML IDP Views"""
from typing import Optional
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger
from authentik.core.models import Application
from authentik.events.models import Event, EventAction
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_APPLICATION,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
from authentik.providers.saml.views.flows import (
REQUEST_KEY_RELAY_STATE,
REQUEST_KEY_SAML_REQUEST,
REQUEST_KEY_SAML_SIG_ALG,
REQUEST_KEY_SAML_SIGNATURE,
SESSION_KEY_AUTH_N_REQUEST,
SAMLFlowFinalView,
)
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE
LOGGER = get_logger()
class SAMLSSOView(PolicyAccessView):
""" "SAML SSO Base View, which plans a flow and injects our final stage.
Calls get/post handler."""
def resolve_provider_application(self):
self.application = get_object_or_404(
Application, slug=self.kwargs["application_slug"]
)
self.provider: SAMLProvider = get_object_or_404(
SAMLProvider, pk=self.application.provider_id
)
def check_saml_request(self) -> Optional[HttpRequest]:
"""Handler to verify the SAML Request. Must be implemented by a subclass"""
raise NotImplementedError
# pylint: disable=unused-argument
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
# Call the method handler, which checks the SAML
# Request and returns a HTTP Response on error
method_response = self.check_saml_request()
if method_response:
return method_response
# Regardless, we start the planner and return to it
planner = FlowPlanner(self.provider.authorization_flow)
planner.allow_empty_flows = True
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
PLAN_CONTEXT_CONSENT_TEMPLATE: "providers/saml/consent.html",
},
)
plan.append(in_memory_stage(SAMLFlowFinalView))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_flows:flow-executor-shell",
request.GET,
flow_slug=self.provider.authorization_flow.slug,
)
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""GET and POST use the same handler, but we can't
override .dispatch easily because PolicyAccessView's dispatch"""
return self.get(request, application_slug)
class SAMLSSOBindingRedirectView(SAMLSSOView):
"""SAML Handler for SSO/Redirect bindings, which are sent via GET"""
def check_saml_request(self) -> Optional[HttpRequest]:
"""Handle REDIRECT bindings"""
if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
LOGGER.info("handle_saml_request: SAML payload missing")
return bad_request_message(
self.request, "The SAML request payload is missing."
)
try:
auth_n_request = AuthNRequestParser(self.provider).parse_detached(
self.request.GET[REQUEST_KEY_SAML_REQUEST],
self.request.GET.get(REQUEST_KEY_RELAY_STATE),
self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
)
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
except CannotHandleAssertion as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
provider=self.provider,
message=str(exc),
).save()
LOGGER.info(str(exc))
return bad_request_message(self.request, str(exc))
return None
@method_decorator(csrf_exempt, name="dispatch")
class SAMLSSOBindingPOSTView(SAMLSSOView):
"""SAML Handler for SSO/POST bindings"""
def check_saml_request(self) -> Optional[HttpRequest]:
"""Handle POST bindings"""
if REQUEST_KEY_SAML_REQUEST not in self.request.POST:
LOGGER.info("check_saml_request: SAML payload missing")
return bad_request_message(
self.request, "The SAML request payload is missing."
)
try:
auth_n_request = AuthNRequestParser(self.provider).parse(
self.request.POST[REQUEST_KEY_SAML_REQUEST],
self.request.POST.get(REQUEST_KEY_RELAY_STATE),
)
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
except CannotHandleAssertion as exc:
LOGGER.info(str(exc))
return bad_request_message(self.request, str(exc))
return None
class SAMLSSOBindingInitView(SAMLSSOView):
"""SAML Handler for for IdP Initiated login flows"""
def check_saml_request(self) -> Optional[HttpRequest]:
"""Create SAML Response from scratch"""
LOGGER.debug(
"handle_saml_no_request: No SAML Request, using IdP-initiated flow."
)
auth_n_request = AuthNRequestParser(self.provider).idp_initiated()
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request

View File

@ -94,12 +94,6 @@ class ASGILogger:
self.log(runtime) self.log(runtime)
await send(message) await send(message)
if self.headers.get(b"host", b"") == b"authentik-healthcheck-host":
# Don't log healthcheck/readiness requests
await send({"type": "http.response.start", "status": 204, "headers": []})
await send({"type": "http.response.body", "body": ""})
return
self.start = time() self.start = time()
if scope["type"] == "lifespan": if scope["type"] == "lifespan":
# https://code.djangoproject.com/ticket/31508 # https://code.djangoproject.com/ticket/31508
@ -129,7 +123,7 @@ class ASGILogger:
method=self.scope.get("method", ""), method=self.scope.get("method", ""),
scheme=self.scope.get("scheme", ""), scheme=self.scope.get("scheme", ""),
status=self.status_code, status=self.status_code,
size=self.content_length / 1000 if self.content_length > 0 else "-", size=self.content_length / 1000 if self.content_length > 0 else 0,
runtime=runtime, runtime=runtime,
) )

View File

@ -2,6 +2,8 @@
from base64 import b64encode from base64 import b64encode
from django.conf import settings from django.conf import settings
from django.db import connections
from django.db.utils import OperationalError
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.views import View from django.views import View
from django_prometheus.exports import ExportToDjangoView from django_prometheus.exports import ExportToDjangoView
@ -23,3 +25,22 @@ class MetricsView(View):
return response return response
return ExportToDjangoView(request) return ExportToDjangoView(request)
class LiveView(View):
"""View for liveness probe, always returns Http 201"""
def dispatch(self, request: HttpRequest) -> HttpResponse:
return HttpResponse(status=201)
class ReadyView(View):
"""View for liveness probe, always returns Http 201"""
def dispatch(self, request: HttpRequest) -> HttpResponse:
db_conn = connections["default"]
try:
_ = db_conn.cursor()
except OperationalError:
return HttpResponse(status=503)
return HttpResponse(status=201)

View File

@ -9,7 +9,7 @@ from structlog.stdlib import get_logger
from authentik.core.views import error from authentik.core.views import error
from authentik.lib.utils.reflection import get_apps from authentik.lib.utils.reflection import get_apps
from authentik.root.monitoring import MetricsView from authentik.root.monitoring import LiveView, MetricsView, ReadyView
LOGGER = get_logger() LOGGER = get_logger()
admin.autodiscover() admin.autodiscover()
@ -57,6 +57,8 @@ for _authentik_app in get_apps():
urlpatterns += [ urlpatterns += [
path("administration/django/", admin.site.urls), path("administration/django/", admin.site.urls),
path("metrics/", MetricsView.as_view(), name="metrics"), path("metrics/", MetricsView.as_view(), name="metrics"),
path("-/health/live/", LiveView.as_view(), name="health-live"),
path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
path("-/jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), path("-/jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
] ]

View File

@ -40,7 +40,7 @@ services:
traefik.http.routers.app-router.rule: PathPrefix(`/`) traefik.http.routers.app-router.rule: PathPrefix(`/`)
traefik.http.routers.app-router.service: app-service traefik.http.routers.app-router.service: app-service
traefik.http.routers.app-router.tls: 'true' traefik.http.routers.app-router.tls: 'true'
traefik.http.services.app-service.loadbalancer.healthcheck.hostname: authentik-healthcheck-host traefik.http.services.app-service.loadbalancer.healthcheck.path: /-/health/live/
traefik.http.services.app-service.loadbalancer.server.port: '8000' traefik.http.services.app-service.loadbalancer.server.port: '8000'
env_file: env_file:
- .env - .env

View File

@ -97,18 +97,12 @@ spec:
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
httpGet: httpGet:
path: / path: /-/health/live/
port: http port: http
httpHeaders:
- name: Host
value: authentik-healthcheck-host
readinessProbe: readinessProbe:
httpGet: httpGet:
path: / path: /-/health/ready/
port: http port: http
httpHeaders:
- name: Host
value: authentik-healthcheck-host
resources: resources:
requests: requests:
cpu: 100m cpu: 100m

View File

@ -77,7 +77,15 @@ func (ac *APIController) startWSHandler() {
logger.WithField("wait", notConnectedWait).Info("Not connected, trying again...") logger.WithField("wait", notConnectedWait).Info("Not connected, trying again...")
time.Sleep(notConnectedWait) time.Sleep(notConnectedWait)
notConnectedBackoff += notConnectedBackoff notConnectedBackoff += notConnectedBackoff
// Limit backoff to max 60 seconds
if notConnectedBackoff >= 60 {
notConnectedBackoff = 60
}
ac.wsConn.CloseAndReconnect()
continue continue
} else {
// When we're connected, reset backoff to 1
notConnectedBackoff = 1
} }
var wsMsg websocketMessage var wsMsg websocketMessage
err := ac.wsConn.ReadJSON(&wsMsg) err := ac.wsConn.ReadJSON(&wsMsg)
@ -109,7 +117,7 @@ func (ac *APIController) startWSHealth() {
}, },
} }
err := ac.wsConn.WriteJSON(aliveMsg) err := ac.wsConn.WriteJSON(aliveMsg)
ac.logger.WithField("loop", "ws-health").Debug("hello'd") ac.logger.WithField("loop", "ws-health").Trace("hello'd")
if err != nil { if err != nil {
ac.logger.WithField("loop", "ws-health").Println("write:", err) ac.logger.WithField("loop", "ws-health").Println("write:", err)
ac.wsConn.CloseAndReconnect() ac.wsConn.CloseAndReconnect()

12
web/package-lock.json generated
View File

@ -1537,9 +1537,9 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
}, },
"fsevents": { "fsevents": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"optional": true "optional": true
}, },
"functional-red-black-tree": { "functional-red-black-tree": {
@ -2638,9 +2638,9 @@
} }
}, },
"rollup": { "rollup": {
"version": "2.38.4", "version": "2.38.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.4.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.5.tgz",
"integrity": "sha512-B0LcJhjiwKkTl79aGVF/u5KdzsH8IylVfV56Ut6c9ouWLJcUK17T83aZBetNYSnZtXf2OHD4+2PbmRW+Fp5ulg==", "integrity": "sha512-VoWt8DysFGDVRGWuHTqZzT02J0ASgjVq/hPs9QcBOGMd7B+jfTr/iqMVEyOi901rE3xq+Deq66GzIT1yt7sGwQ==",
"requires": { "requires": {
"fsevents": "~2.3.1" "fsevents": "~2.3.1"
} }

View File

@ -22,7 +22,7 @@
"flowchart.js": "^1.15.0", "flowchart.js": "^1.15.0",
"lit-element": "^2.4.0", "lit-element": "^2.4.0",
"lit-html": "^1.3.0", "lit-html": "^1.3.0",
"rollup": "^2.38.4", "rollup": "^2.38.5",
"rollup-plugin-copy": "^3.3.0", "rollup-plugin-copy": "^3.3.0",
"rollup-plugin-cssimport": "^1.0.2", "rollup-plugin-cssimport": "^1.0.2",
"rollup-plugin-external-globals": "^0.6.1", "rollup-plugin-external-globals": "^0.6.1",

View File

@ -28,7 +28,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
), ),
new SidebarItem("Providers", "/providers"), new SidebarItem("Providers", "/providers"),
new SidebarItem("Outposts", "/administration/outposts/"), new SidebarItem("Outposts", "/administration/outposts/"),
new SidebarItem("Outpost Service Connections", "/administration/outposts/service_connections/"), new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return User.me().then(u => u.is_superuser);
}), }),

View File

@ -36,7 +36,7 @@ export class AdminOverviewPage extends LitElement {
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Apps with most usage" style="grid-column-end: span 2;grid-row-end: span 3;"> <ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Apps with most usage" style="grid-column-end: span 2;grid-row-end: span 3;">
<ak-top-applications-table></ak-top-applications-table> <ak-top-applications-table></ak-top-applications-table>
</ak-aggregate-card> </ak-aggregate-card>
<ak-admin-status-card-provider class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Providers" headerLink="#/administration/providers/"> <ak-admin-status-card-provider class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Providers" headerLink="#/providers/">
</ak-admin-status-card-provider> </ak-admin-status-card-provider>
<ak-admin-status-card-policy-unbound class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-infrastructure" header="Policies" headerLink="#/administration/policies/"> <ak-admin-status-card-policy-unbound class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-infrastructure" header="Policies" headerLink="#/administration/policies/">
</ak-admin-status-card-policy-unbound> </ak-admin-status-card-policy-unbound>

View File

@ -68,7 +68,6 @@ In the `SAML Enabled Identity Providers` paste the following configuration:
"attr_user_permanent_id": "http://schemas.goauthentik.io/2021/02/saml/uid", "attr_user_permanent_id": "http://schemas.goauthentik.io/2021/02/saml/uid",
"x509cert": "MIIDEjCCAfqgAwIBAgIRAJZ9pOZ1g0xjiHtQAAejsMEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlcGFzc2Jvb2sgU2VsZi1zaWduZWQgU0FNTCBDZXJ0aWZpY2F0ZTAeFw0xOTEyMjYyMDEwNDFaFw0yMDEyMjYyMDEwNDFaMFkxLjAsBgNVBAMMJXBhc3Nib29rIFNlbGYtc2lnbmVkIFNBTUwgQ2VydGlmaWNhdGUxETAPBgNVBAoMCHBhc3Nib29rMRQwEgYDVQQLDAtTZWxmLXNpZ25lZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO/ktBYZkY9xAijF4acvzX6Q1K8KoIZeyde8fVgcWBz4L5FgDQ4/dni4k2YAcPdwteGL4nKVzetUzjbRCBUNuO6lqU4J4WNNX4Xg4Ir7XLRoAQeo+omTPBdpJ1p02HjtN5jT01umN3bK2yto1e37CJhK6WJiaXqRewPxh4lI4aqdj3BhFkJ3I3r2qxaWOAXQ6X7fg3w/ny7QP53//ouZo7hSLY3GIcRKgvdjjVM3OW5C3WLpOq5Dez5GWVJ17aeFCfGQ8bwFKde6qfYqyGcU9xHB36TtVHB9hSFP/tUFhkiSOxtsrYwCgCyXm4UTSpP+wiNyjKfFw7qGLBvA2hGTNw8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh9PeAqPRQk1/SSygIFADZBi08O/DPCshFwEHvJATIcTzcDD8UGAjXh+H5OlkDyX7KyrcaNvYaafCUo63A+WprdtdY5Ty6SBEwTYyiQyQfwM9BfK+imCoif1Ai7xAelD7p9lNazWq7JU+H/Ep7U7Q7LvpxAbK0JArt+IWTb2NcMb3OWE1r0gFbs44O1l6W9UbJTbyLMzbGbe5i+NHlgnwPwuhtRMh0NUYabGHKcHbhwyFhfGAQv2dAp5KF1E5gu6ZzCiFePzc0FrqXQyb2zpFYcJHXquiqaOeG7cZxRHYcjrl10Vxzki64XVA9BpdELgKSnupDGUEJsRUt3WVOmvZuA==", "x509cert": "MIIDEjCCAfqgAwIBAgIRAJZ9pOZ1g0xjiHtQAAejsMEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlcGFzc2Jvb2sgU2VsZi1zaWduZWQgU0FNTCBDZXJ0aWZpY2F0ZTAeFw0xOTEyMjYyMDEwNDFaFw0yMDEyMjYyMDEwNDFaMFkxLjAsBgNVBAMMJXBhc3Nib29rIFNlbGYtc2lnbmVkIFNBTUwgQ2VydGlmaWNhdGUxETAPBgNVBAoMCHBhc3Nib29rMRQwEgYDVQQLDAtTZWxmLXNpZ25lZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO/ktBYZkY9xAijF4acvzX6Q1K8KoIZeyde8fVgcWBz4L5FgDQ4/dni4k2YAcPdwteGL4nKVzetUzjbRCBUNuO6lqU4J4WNNX4Xg4Ir7XLRoAQeo+omTPBdpJ1p02HjtN5jT01umN3bK2yto1e37CJhK6WJiaXqRewPxh4lI4aqdj3BhFkJ3I3r2qxaWOAXQ6X7fg3w/ny7QP53//ouZo7hSLY3GIcRKgvdjjVM3OW5C3WLpOq5Dez5GWVJ17aeFCfGQ8bwFKde6qfYqyGcU9xHB36TtVHB9hSFP/tUFhkiSOxtsrYwCgCyXm4UTSpP+wiNyjKfFw7qGLBvA2hGTNw8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh9PeAqPRQk1/SSygIFADZBi08O/DPCshFwEHvJATIcTzcDD8UGAjXh+H5OlkDyX7KyrcaNvYaafCUo63A+WprdtdY5Ty6SBEwTYyiQyQfwM9BfK+imCoif1Ai7xAelD7p9lNazWq7JU+H/Ep7U7Q7LvpxAbK0JArt+IWTb2NcMb3OWE1r0gFbs44O1l6W9UbJTbyLMzbGbe5i+NHlgnwPwuhtRMh0NUYabGHKcHbhwyFhfGAQv2dAp5KF1E5gu6ZzCiFePzc0FrqXQyb2zpFYcJHXquiqaOeG7cZxRHYcjrl10Vxzki64XVA9BpdELgKSnupDGUEJsRUt3WVOmvZuA==",
"url": "https://authentik.company/application/saml/awx/login/", "url": "https://authentik.company/application/saml/awx/login/",
"attr_last_name": "User.LastName",
"entity_id": "https://awx.company/sso/metadata/saml/", "entity_id": "https://awx.company/sso/metadata/saml/",
"attr_email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "attr_email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"attr_first_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" "attr_first_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"

View File

@ -18,14 +18,36 @@ The following placeholders will be used:
- `rancher.company` is the FQDN of the Rancher install. - `rancher.company` is the FQDN of the Rancher install.
- `authentik.company` is the FQDN of the authentik install. - `authentik.company` is the FQDN of the authentik install.
Create an application in authentik and note the slug, as this will be used later. Create a SAML provider with the following parameters: Under *Property Mappings*, create a *SAML Property Mapping*. Give it a name like "SAML Rancher User ID". Set the SAML name to `rancherUidUsername` and the expression to the following
```python
return f"{user.pk}-{user.username}"
```
Create an application in authentik. Create a SAML provider with the following parameters:
- ACS URL: `https://rancher.company/v1-saml/adfs/saml/acs` - ACS URL: `https://rancher.company/v1-saml/adfs/saml/acs`
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata` - Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
- Issuer: `authentik` - Issuer: `authentik`
- Property mappings: Select all default mappings and the mapping you've created above.
You can of course use a custom signing certificate, and adjust durations. You can of course use a custom signing certificate, and adjust durations.
## Rancher ## Rancher
In Rancher, navigate to *Global* -> *Security* -> *Authentication*, and select ADFS.
Fill in the fields
- Display Name Field: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`
- User Name Field: `http://schemas.goauthentik.io/2021/02/saml/username`
- UID Field: `rancherUidUsername`
- Groups Field: `http://schemas.xmlsoap.org/claims/Group`
For the private key and certificate, you can either generate a new pair (in authentik, navigate to *Identity & Cryptography* -> *Certificates* and select Generate), or use an existing pair.
Copy the metadata from authentik, and paste it in the metadata field.
Click on save to test the authentication.
![](./rancher.png) ![](./rancher.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 320 KiB

View File

@ -41,8 +41,8 @@ In authentik, get the Metadata URL by right-clicking `Download Metadata` and sel
On the next screen, input these Values On the next screen, input these Values
IdP User ID: `http://schemas.goauthentik.io/2021/02/saml/uid` - IdP User ID: `http://schemas.goauthentik.io/2021/02/saml/uid`
User Email: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress` - User Email: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`
First Name: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name` - First Name: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`
After confirming, Sentry will authenticate with authentik, and you should be redirected back to a page confirming your settings. After confirming, Sentry will authenticate with authentik, and you should be redirected back to a page confirming your settings.

View File

@ -44,10 +44,11 @@ Due to the switch to managed objects, some default property mappings are changin
The change affects the "SAML Name" property, which has been changed from an oid to a Schema URI to aid readability. The change affects the "SAML Name" property, which has been changed from an oid to a Schema URI to aid readability.
The integrations affected are: The integrations affected are:
- [NextCloud](../integrations/services/nextcloud/index) - [Ansible Tower/AWX](/docs/integrations/services/awx-tower/index)
- [Sentry](../integrations/services/sentry/index) - [GitLab](/docs/integrations/services/gitlab/index)
- [GitLab](../integrations/services/gitlab/index) - [NextCloud](/docs/integrations/services/nextcloud/index)
- [Ansible Tower/AWX](../integrations/services/awx-tower/index) - [Rancher](/docs/integrations/services/rancher/index)
- [Sentry](/docs/integrations/services/sentry/index)
### docker-compose ### docker-compose

3392
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,19 @@
{ {
"name": "docs", "name": "authentik-docs",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"docusaurus": "docusaurus", "docusaurus": "docusaurus",
"start": "docusaurus start", "watch": "docusaurus start",
"build": "docusaurus build", "build": "docusaurus build",
"swizzle": "docusaurus swizzle", "swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy", "deploy": "docusaurus deploy",
"serve": "docusaurus serve" "serve": "docusaurus serve"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "2.0.0-alpha.66", "@docusaurus/core": "2.0.0-alpha.70",
"@docusaurus/preset-classic": "2.0.0-alpha.66", "@docusaurus/preset-classic": "2.0.0-alpha.70",
"@mdx-js/react": "^1.5.8", "@mdx-js/react": "^1.6.22",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"react": "^16.8.4", "react": "^16.8.4",
"react-dom": "^16.8.4" "react-dom": "^16.8.4"
@ -31,6 +31,6 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"prettier": "2.1.2" "prettier": "2.2.1"
} }
} }

View File

@ -141,6 +141,7 @@ module.exports = {
"releases/0.13", "releases/0.13",
"releases/0.14", "releases/0.14",
"releases/2021.1", "releases/2021.1",
"releases/2021.2",
], ],
}, },
{ {