Start SAML implementation

This commit is contained in:
Kumi 2022-08-22 09:37:16 +00:00
parent 8359a98fdd
commit 860c497c86
Signed by: kumi
GPG key ID: ECBCC9082395383F
10 changed files with 129 additions and 3 deletions

3
.gitignore vendored
View file

@ -4,4 +4,5 @@ db.sqlite3
db.sqlite3-journal
venv/
config.ini
/static/
/static/
/certificates/

View file

@ -1,7 +1,9 @@
[App]
Debug = 0
Hosts = ["kumidc.local"]
BaseURL = "https://kumidc.local/"
# StaticDir = /var/www/html/kumidc/static
# CertificateDir = /etc/ssl/kumidc/
# [MySQL]
# Database = kumidc

View file

View file

@ -0,0 +1,47 @@
from django.core.management.base import BaseCommand
from django.conf import settings
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization, hashes
from cryptography import x509
from cryptography.x509.oid import NameOID
from datetime import datetime, timedelta
class Command(BaseCommand):
help = 'Generates self-signed certificate for SAML IdP'
def add_arguments(self, parser):
parser.add_argument('-f', '--force', action='store_true', help="Force re-creation of certificates if the files already exist")
parser.add_argument('--commonname', type=str, help="Common Name to use for certificate, default: KumiDC", default="KumiDC")
parser.add_argument('--country', type=str, help="Country Code to use for the certificate, default: US", default="US")
parser.add_argument('--state', type=str, help="State name to use for the certificate, default: New York", default="New York")
parser.add_argument('--locality', type=str, help="Locality name to use for the certificate, default: New York City", default="New York City")
parser.add_argument('--organization', type=str, help="Organization name to use for the certificate, default: KumiDC", default="KumiDC")
parser.add_argument('--validity-days', type=int, help="How many days the certificate should be \"valid\" for, default: 3650", default=3650)
def handle(self, *args, **kwargs):
if (settings.CERTIFICATE_DIR / "saml.key").exists() or (settings.CERTIFICATE_DIR / "saml.crt").exists():
if not kwargs["force"]:
print(f"Error: saml.crt and/or saml.key already in CERTIFICATE_DIR ({settings.CERTIFICATE_DIR}). Add --force to create new key pair.")
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, kwargs["commonname"]),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, kwargs["state"]),
x509.NameAttribute(NameOID.COUNTRY_NAME, kwargs["country"]),
x509.NameAttribute(NameOID.LOCALITY_NAME, kwargs["locality"]),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, kwargs["organization"]),
])
cert = x509.CertificateBuilder().subject_name(subject).issuer_name(issuer).public_key(key.public_key()).serial_number(x509.random_serial_number()).not_valid_before(datetime.utcnow()).not_valid_after(datetime.utcnow() + timedelta(days=3650)).add_extension(x509.SubjectAlternativeName([x509.DNSName(name) for name in settings.ALLOWED_HOSTS]), critical=False).sign(key, hashes.SHA256())
settings.CERTIFICATE_DIR.mkdir(exist_ok=True)
with open(settings.CERTIFICATE_DIR / "saml.key", "wb") as keyfile:
keyfile.write(key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption()))
with open(settings.CERTIFICATE_DIR / "saml.crt", "wb") as certfile:
certfile.write(cert.public_bytes(serialization.Encoding.PEM))

0
core/saml/__init__.py Normal file
View file

6
core/saml/processors.py Normal file
View file

@ -0,0 +1,6 @@
from djangosaml2idp.processors import BaseProcessor
class SAMLProcessor(BaseProcessor):
def enable_multifactor(self, user):
return user.totpsecret.exists() and user.totpsecret.active

7
frontend/views/saml.py Normal file
View file

@ -0,0 +1,7 @@
from djangosaml2idp.views import ProcessMultiFactorView
from authentication.mixins.timeout import TimeoutMixin
class SAMLMultiFactorView(TimeoutMixin, ProcessMultiFactorView):
pass

View file

@ -1,9 +1,15 @@
from django.urls import reverse_lazy
from pathlib import Path
from urllib.parse import urljoin
import json
import saml2
from saml2.saml import NAMEID_FORMAT_EMAILADDRESS, NAMEID_FORMAT_UNSPECIFIED
from saml2.sigver import get_xmlsec_binary
from autosecretkey import AutoSecretKey
@ -16,7 +22,9 @@ SECRET_KEY = CONFIG_FILE.secret_key
DEBUG = CONFIG_FILE.config.getboolean("App", "Debug", fallback=False)
ALLOWED_HOSTS = json.loads(CONFIG_FILE.config["App"]["Hosts"])
BASE_URL = CONFIG_FILE.config["App"]["BaseURL"]
CERTIFICATE_DIR = Path(CONFIG_FILE.config.get("App", "CertificateDir", fallback=BASE_DIR / "certificates"))
# Application definition
@ -35,7 +43,9 @@ INSTALLED_APPS = [
'core',
'authentication',
'frontend',
'oidc_provider',
'djangosaml2idp',
]
MIDDLEWARE = [
@ -148,6 +158,52 @@ OIDC_TEMPLATES = {
'authorize': 'frontend/oidc/authorize.html'
}
# SAML Configuration
SAML_IDP_CONFIG = {
'debug' : DEBUG,
'xmlsec_binary': get_xmlsec_binary(['/opt/local/bin', '/usr/bin']),
'entityid': urljoin(BASE_URL, '/saml/metadata/'),
'description': 'KumiDC',
'service': {
'idp': {
'name': 'KumiDC',
'endpoints': {
'single_sign_on_service': [
#(urljoin(BASE_URL, '/saml/sso/post/'), saml2.BINDING_HTTP_POST),
(urljoin(BASE_URL, '/saml/sso/redirect/'), saml2.BINDING_HTTP_REDIRECT),
],
"single_logout_service": [
#(urljoin(BASE_URL, "/saml/slo/post/"), saml2.BINDING_HTTP_POST),
(urljoin(BASE_URL, "/saml/slo/redirect/"), saml2.BINDING_HTTP_REDIRECT)
],
},
'name_id_format': [NAMEID_FORMAT_EMAILADDRESS, NAMEID_FORMAT_UNSPECIFIED],
'sign_response': True,
'sign_assertion': True,
'want_authn_requests_signed': True,
},
},
# Signing
'key_file': str(CERTIFICATE_DIR / 'saml.key'),
'cert_file': str(CERTIFICATE_DIR / 'saml.crt'),
# Encryption
'encryption_keypairs': [{
'key_file': str(CERTIFICATE_DIR / 'saml.key'),
'cert_file': str(CERTIFICATE_DIR / 'saml.crt'),
}],
'valid_for': 365 * 24,
}
SAML_IDP_SP_FIELD_DEFAULT_PROCESSOR = 'core.saml.processors.SAMLProcessor'
SAML_IDP_MULTIFACTOR_VIEW = "frontend.views.saml.SAMLMultiFactorView"
SAML_AUTHN_SIGN_ALG = saml2.xmldsig.SIG_RSA_SHA256
SAML_AUTHN_DIGEST_ALG = saml2.xmldsig.DIGEST_SHA256
# Session Timeouts

View file

@ -4,9 +4,12 @@ from django.views.generic import RedirectView
urlpatterns = [
re_path(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
re_path(r'^saml/', include('djangosaml2idp.urls', namespace="djangosaml2idp")),
path('admin/login/', RedirectView.as_view(url=reverse_lazy("auth:login"), query_string=True)),
path('admin/', admin.site.urls),
re_path(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
path('auth/', include(("authentication.urls", "auth"))),
path('', include(("frontend.urls", "frontend"))),
]

View file

@ -1,10 +1,14 @@
Django<4
git+https://github.com/juanifioren/django-oidc-provider
django-oidc-provider
djangosaml2idp
dbsettings
django-autosecretkey
git+https://github.com/IdentityPython/pysaml2
cryptography
pyotp
django-timezone-field
django-phonenumber-field[phonenumbers]