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"""