Start SAML implementation
This commit is contained in:
parent
8359a98fdd
commit
860c497c86
10 changed files with 129 additions and 3 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,4 +4,5 @@ db.sqlite3
|
||||||
db.sqlite3-journal
|
db.sqlite3-journal
|
||||||
venv/
|
venv/
|
||||||
config.ini
|
config.ini
|
||||||
/static/
|
/static/
|
||||||
|
/certificates/
|
|
@ -1,7 +1,9 @@
|
||||||
[App]
|
[App]
|
||||||
Debug = 0
|
Debug = 0
|
||||||
Hosts = ["kumidc.local"]
|
Hosts = ["kumidc.local"]
|
||||||
|
BaseURL = "https://kumidc.local/"
|
||||||
# StaticDir = /var/www/html/kumidc/static
|
# StaticDir = /var/www/html/kumidc/static
|
||||||
|
# CertificateDir = /etc/ssl/kumidc/
|
||||||
|
|
||||||
# [MySQL]
|
# [MySQL]
|
||||||
# Database = kumidc
|
# Database = kumidc
|
||||||
|
|
0
core/management/commands/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
47
core/management/commands/createsamlcert.py
Normal file
47
core/management/commands/createsamlcert.py
Normal 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
0
core/saml/__init__.py
Normal file
6
core/saml/processors.py
Normal file
6
core/saml/processors.py
Normal 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
7
frontend/views/saml.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from djangosaml2idp.views import ProcessMultiFactorView
|
||||||
|
|
||||||
|
from authentication.mixins.timeout import TimeoutMixin
|
||||||
|
|
||||||
|
|
||||||
|
class SAMLMultiFactorView(TimeoutMixin, ProcessMultiFactorView):
|
||||||
|
pass
|
|
@ -1,9 +1,15 @@
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
import json
|
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
|
from autosecretkey import AutoSecretKey
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +22,9 @@ SECRET_KEY = CONFIG_FILE.secret_key
|
||||||
DEBUG = CONFIG_FILE.config.getboolean("App", "Debug", fallback=False)
|
DEBUG = CONFIG_FILE.config.getboolean("App", "Debug", fallback=False)
|
||||||
|
|
||||||
ALLOWED_HOSTS = json.loads(CONFIG_FILE.config["App"]["Hosts"])
|
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
|
# Application definition
|
||||||
|
|
||||||
|
@ -35,7 +43,9 @@ INSTALLED_APPS = [
|
||||||
'core',
|
'core',
|
||||||
'authentication',
|
'authentication',
|
||||||
'frontend',
|
'frontend',
|
||||||
|
|
||||||
'oidc_provider',
|
'oidc_provider',
|
||||||
|
'djangosaml2idp',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -148,6 +158,52 @@ OIDC_TEMPLATES = {
|
||||||
'authorize': 'frontend/oidc/authorize.html'
|
'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
|
# Session Timeouts
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,12 @@ from django.views.generic import RedirectView
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
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/login/', RedirectView.as_view(url=reverse_lazy("auth:login"), query_string=True)),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
re_path(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
|
|
||||||
path('auth/', include(("authentication.urls", "auth"))),
|
path('auth/', include(("authentication.urls", "auth"))),
|
||||||
path('', include(("frontend.urls", "frontend"))),
|
path('', include(("frontend.urls", "frontend"))),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
Django<4
|
Django<4
|
||||||
|
|
||||||
git+https://github.com/juanifioren/django-oidc-provider
|
django-oidc-provider
|
||||||
|
djangosaml2idp
|
||||||
|
|
||||||
dbsettings
|
dbsettings
|
||||||
django-autosecretkey
|
django-autosecretkey
|
||||||
|
|
||||||
|
git+https://github.com/IdentityPython/pysaml2
|
||||||
|
|
||||||
|
cryptography
|
||||||
pyotp
|
pyotp
|
||||||
django-timezone-field
|
django-timezone-field
|
||||||
django-phonenumber-field[phonenumbers]
|
django-phonenumber-field[phonenumbers]
|
||||||
|
|
Loading…
Reference in a new issue