Compare commits

..

6 commits
radius ... main

2178 changed files with 133 additions and 448 deletions

4
.gitignore vendored
View file

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

View file

@ -4,10 +4,10 @@ KumiDC is a simple Django-based OpenID Connect identity provider.
At its core, it uses [Django OpenID Connect Provider](https://github.com/juanifioren/django-oidc-provider) by [Juan Ignacio Fiorentino](https://github.com/juanifioren) to provide the actual OIDC functionality, and adds a few fancy things on top. At its core, it uses [Django OpenID Connect Provider](https://github.com/juanifioren/django-oidc-provider) by [Juan Ignacio Fiorentino](https://github.com/juanifioren) to provide the actual OIDC functionality, and adds a few fancy things on top.
* "Pretty" [AdminLTE](https://github.com/ColorlibHQ/AdminLTE) user interface - "Pretty" [AdminLTE](https://github.com/ColorlibHQ/AdminLTE) user interface
* Time-based One-Time Passwords for Two Factor Authentication - Time-based One-Time Passwords for Two Factor Authentication
* Requirement to re-authenticate or enter 2FA token every five minutes - Requirement to re-authenticate or enter 2FA token every five minutes
As it stands, this project is not complete. It works as an OIDC provider, although its security has not been tested to any extent. As it stands, this project is not complete. It works as an OIDC provider, although its security has not been tested to any extent.
We currently use it, in conjunction with [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy), to add an authentication layer to applications on our internal network where protection against unauthorized access is not directly implemented, and not critical. We currently use it, in conjunction with [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy), to add an authentication layer to applications on our internal network where protection against unauthorized access is not directly implemented, and not critical.

View file

@ -1,104 +0,0 @@
from django.db import models
from django.contrib.auth import get_user_model
from annoying.fields import AutoOneToOneField
from pyotp import TOTP, random_base32
from datetime import datetime
class TOTPSecret(models.Model):
"""A secret for a user's TOTP device
Attributes:
user: The user to whom this secret belongs
secret: The secret
active: Whether this secret is currently active
"""
user = AutoOneToOneField(get_user_model(), models.CASCADE, primary_key=True)
secret = models.CharField(max_length=32, default=random_base32)
active = models.BooleanField(default=False)
def verify(self, token: str, window: int = 1, set_used=True) -> bool:
"""Verify a TOTP token
Args:
token (str): The token to verify
window (int, optional): The number of tokens to check before and after the current token. Defaults to 1.
Returns:
bool: Whether the token was valid
"""
if not self.active:
return False
if UsedTOTPToken.is_used(self, token):
return False
if TOTP(self.secret).verify(token, valid_window=window):
if set_used:
UsedTOTPToken.objects.create(secret=self, token=token)
return True
return False
def get_uri(self) -> str:
"""Get the provisioning URI for this secret
This is used to generate a QR code for the user to scan with their TOTP device.
Returns:
str: The provisioning URI
"""
return TOTP(self.secret).provisioning_uri(
name=self.user.email, issuer_name="KumiDC"
)
def change_secret(self) -> str:
"""Change the secret for this user
This generates a new secret and saves it to the database.
Returns:
str: The new secret
"""
self.secret = random_base32()
self.save()
return self.secret
class UsedTOTPToken(models.Model):
"""A TOTP token that has been used
We log these so that we can prevent replay attacks.
Attributes:
secret: The secret that was used
token: The token that was used
timestamp: When the token was used
expiry: How long we should keep this token for
"""
secret = models.ForeignKey(TOTPSecret, on_delete=models.CASCADE)
token = models.CharField(max_length=6)
timestamp = models.DateTimeField(auto_now_add=True)
expiry = models.DateTimeField()
def save(self, *args, **kwargs):
self.expiry = self.timestamp + timedelta(seconds=60)
super().save(*args, **kwargs)
@classmethod
def is_used(cls, secret: TOTPSecret, token: str) -> bool:
"""Check whether a token has been used
Args:
secret (TOTPSecret): The secret that was used
token (str): The token that was used
Returns:
bool: Whether the token has recently been used
"""
return cls.objects.filter(secret=secret, token=token, expiry__gte=datetime.now()).exists()

View file

@ -3,4 +3,4 @@ from django.apps import AppConfig
class AuthConfig(AppConfig): class AuthConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'authentication' name = 'kumidc.authentication'

View file

@ -1,6 +1,6 @@
# Generated by Django 3.2.14 on 2022-08-02 16:31 # Generated by Django 3.2.14 on 2022-08-02 16:31
import authentication.helpers.session from ..helpers import session
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -30,7 +30,7 @@ class Migration(migrations.Migration):
name='AuthSession', name='AuthSession',
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('expiry', models.DateTimeField(default=authentication.helpers.session.session_expiry)), ('expiry', models.DateTimeField(default=session.session_expiry)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
), ),

View file

@ -0,0 +1,22 @@
from django.db import models
from django.contrib.auth import get_user_model
from annoying.fields import AutoOneToOneField
from pyotp import TOTP, random_base32
class TOTPSecret(models.Model):
user = AutoOneToOneField(get_user_model(), models.CASCADE, primary_key=True)
secret = models.CharField(max_length=32, default=random_base32)
active = models.BooleanField(default=False)
def verify(self, token):
return TOTP(self.secret).verify(token)
def get_uri(self):
return TOTP(self.secret).provisioning_uri(name=self.user.email, issuer_name="KumiDC")
def change_secret(self):
self.secret = random_base32()
self.save()
return self.secret

View file

@ -7,6 +7,6 @@ from ..helpers.session import session_expiry
class AuthSession(models.Model): class AuthSession(models.Model):
id = models.UUIDField(default=uuid4, primary_key=True) id = models.CharField(max_length=36, default=uuid4, primary_key=True)
user = models.ForeignKey(get_user_model(), models.CASCADE) user = models.ForeignKey(get_user_model(), models.CASCADE)
expiry = models.DateTimeField(default=session_expiry) expiry = models.DateTimeField(default=session_expiry)

View file

@ -4,7 +4,7 @@ from django.urls import reverse_lazy
from django.contrib import messages from django.contrib import messages
from django.utils import timezone from django.utils import timezone
from frontend.mixins.views import TitleMixin from ...frontend.mixins.views import TitleMixin
from ..mixins.session import OnlyLoggedOutMixin from ..mixins.session import OnlyLoggedOutMixin
from ..models.session import AuthSession from ..models.session import AuthSession
from ..helpers.otp import has_otp from ..helpers.otp import has_otp
@ -26,4 +26,4 @@ class LoginView(OnlyLoggedOutMixin, TitleMixin, DjangoLoginView):
def form_invalid(self, form): def form_invalid(self, form):
messages.error(self.request, "Could not log you in. Please check your email address and password, and try again.") messages.error(self.request, "Could not log you in. Please check your email address and password, and try again.")
return super().form_invalid(form) return super().form_invalid(form)

View file

@ -3,7 +3,7 @@ from django.utils import timezone
from ..forms.otp import TOTPLoginForm from ..forms.otp import TOTPLoginForm
from ..mixins.session import AuthSessionRequiredMixin from ..mixins.session import AuthSessionRequiredMixin
from frontend.mixins.views import TitleMixin from ...frontend.mixins.views import TitleMixin
class TOTPLoginView(TitleMixin, AuthSessionRequiredMixin, LoginView): class TOTPLoginView(TitleMixin, AuthSessionRequiredMixin, LoginView):
@ -13,4 +13,4 @@ class TOTPLoginView(TitleMixin, AuthSessionRequiredMixin, LoginView):
def form_valid(self, form): def form_valid(self, form):
self.request.session["LastActivity"] = timezone.now().timestamp() self.request.session["LastActivity"] = timezone.now().timestamp()
return super().form_valid(form) return super().form_valid(form)

View file

@ -5,7 +5,7 @@ from django.urls import reverse_lazy
from ..forms.otp import TOTPLoginForm from ..forms.otp import TOTPLoginForm
from ..models.app import AppSession from ..models.app import AppSession
from frontend.mixins.views import TitleMixin from ...frontend.mixins.views import TitleMixin
class ReverifyView(TitleMixin, LoginView): class ReverifyView(TitleMixin, LoginView):
@ -26,4 +26,4 @@ class ReverifyView(TitleMixin, LoginView):
except (AppSession.DoesNotExist, KeyError): except (AppSession.DoesNotExist, KeyError):
pass pass
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())

View file

@ -3,7 +3,7 @@ from django.apps import AppConfig
class CoreConfig(AppConfig): class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'core' name = 'kumidc.core'
def ready(self): def ready(self):
from . import receivers from . import receivers # noqa: F401

View file

@ -3,4 +3,4 @@ from django.apps import AppConfig
class FrontendConfig(AppConfig): class FrontendConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'frontend' name = 'kumidc.frontend'

View file

Before

Width:  |  Height:  |  Size: 953 B

After

Width:  |  Height:  |  Size: 953 B

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more