diff --git a/authentik/admin/templates/administration/certificatekeypair/generate.html b/authentik/admin/templates/administration/certificatekeypair/generate.html new file mode 100644 index 000000000..6af5c916c --- /dev/null +++ b/authentik/admin/templates/administration/certificatekeypair/generate.html @@ -0,0 +1,14 @@ +{% extends base_template|default:"generic/form.html" %} + +{% load authentik_utils %} +{% load i18n %} + +{% block above_form %} +

+ {% trans 'Generate Certificate-Key Pair' %} +

+{% endblock %} + +{% block action %} +{% trans 'Generate Certificate-Key Pair' %} +{% endblock %} diff --git a/authentik/admin/templates/administration/certificatekeypair/list.html b/authentik/admin/templates/administration/certificatekeypair/list.html index 8acfcb016..9bf8af298 100644 --- a/authentik/admin/templates/administration/certificatekeypair/list.html +++ b/authentik/admin/templates/administration/certificatekeypair/list.html @@ -26,6 +26,12 @@
+ + + {% trans 'Generate' %} + +
+
diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index c419bf185..fa90ab1d6 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -295,6 +295,11 @@ urlpatterns = [ certificate_key_pair.CertificateKeyPairCreateView.as_view(), name="certificatekeypair-create", ), + path( + "crypto/certificates/generate/", + certificate_key_pair.CertificateKeyPairGenerateView.as_view(), + name="certificatekeypair-generate", + ), path( "crypto/certificates//update/", certificate_key_pair.CertificateKeyPairUpdateView.as_view(), diff --git a/authentik/admin/views/certificate_key_pair.py b/authentik/admin/views/certificate_key_pair.py index 09e154cf4..7f01e45de 100644 --- a/authentik/admin/views/certificate_key_pair.py +++ b/authentik/admin/views/certificate_key_pair.py @@ -4,9 +4,11 @@ from django.contrib.auth.mixins import ( PermissionRequiredMixin as DjangoPermissionRequiredMixin, ) from django.contrib.messages.views import SuccessMessageMixin +from django.http.response import HttpResponse from django.urls import reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import ListView, UpdateView +from django.views.generic.edit import FormView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from authentik.admin.views.utils import ( @@ -15,7 +17,11 @@ from authentik.admin.views.utils import ( SearchListMixin, 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.lib.views import CreateAssignPermView @@ -52,7 +58,35 @@ class CertificateKeyPairCreateView( template_name = "generic/create.html" 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( diff --git a/authentik/crypto/builder.py b/authentik/crypto/builder.py index 122766a86..55799a496 100644 --- a/authentik/crypto/builder.py +++ b/authentik/crypto/builder.py @@ -1,6 +1,7 @@ """Create self-signed certificates""" import datetime import uuid +from typing import Optional from cryptography import x509 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.x509.oid import NameOID +from authentik import __version__ +from authentik.crypto.models import CertificateKeyPair + class CertificateBuilder: """Build self-signed certificates""" @@ -17,19 +21,39 @@ class CertificateBuilder: __builder = None __certificate = None + common_name: str + def __init__(self): self.__public_key = None self.__private_key = None self.__builder = 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""" one_day = datetime.timedelta(1, 0, 0) self.__private_key = rsa.generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend() ) 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 = ( x509.CertificateBuilder() .subject_name( @@ -37,7 +61,7 @@ class CertificateBuilder: [ x509.NameAttribute( NameOID.COMMON_NAME, - "authentik Self-signed Certificate", + self.common_name, ), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "authentik"), x509.NameAttribute( @@ -51,13 +75,16 @@ class CertificateBuilder: [ x509.NameAttribute( 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_after(datetime.datetime.today() + datetime.timedelta(days=365)) + .not_valid_after( + datetime.datetime.today() + datetime.timedelta(days=validity_days) + ) .serial_number(int(uuid.uuid4())) .public_key(self.__public_key) ) diff --git a/authentik/crypto/forms.py b/authentik/crypto/forms.py index 61d8cd594..f289cc207 100644 --- a/authentik/crypto/forms.py +++ b/authentik/crypto/forms.py @@ -8,6 +8,14 @@ from django.utils.translation import gettext_lazy as _ 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): """CertificateKeyPair Form"""