On branche authens
This commit is contained in:
parent
c70fcefa86
commit
6a59163dea
12 changed files with 280 additions and 66 deletions
|
@ -116,5 +116,8 @@ class User(AbstractUser):
|
|||
|
||||
def connection_method(self):
|
||||
if self.election is None:
|
||||
if self.username.split("__")[0] == "pwd":
|
||||
return _("mot de passe")
|
||||
return _("CAS")
|
||||
|
||||
return _("identifiants spécifiques")
|
||||
|
|
|
@ -12,6 +12,8 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
|
|||
|
||||
import os
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
@ -35,6 +37,7 @@ INSTALLED_APPS = [
|
|||
"django.contrib.messages",
|
||||
"kadenios.apps.IgnoreSrcStaticFilesConfig",
|
||||
"elections",
|
||||
"authens",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -68,7 +71,7 @@ TEMPLATES = [
|
|||
WSGI_APPLICATION = "kadenios.wsgi.application"
|
||||
|
||||
|
||||
# Password validation
|
||||
# Password validation and authentication
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
|
@ -88,12 +91,15 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
|
||||
AUTH_USER_MODEL = "elections.User"
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"shared.auth.backends.ENSCASBackend",
|
||||
"shared.auth.backends.PwdBackend",
|
||||
"shared.auth.backends.CASBackend",
|
||||
"shared.auth.backends.ElectionBackend",
|
||||
]
|
||||
|
||||
LOGIN_URL = "auth.cas"
|
||||
LOGIN_REDIRECT_URL = "kadenios"
|
||||
LOGIN_URL = reverse_lazy("authens:login")
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
|
||||
AUTHENS_USE_OLDCAS = False # On n'utilise que le CAS normal pour l'instant
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||
|
|
|
@ -9,6 +9,7 @@ urlpatterns = [
|
|||
path("admin/", admin.site.urls),
|
||||
path("elections/", include("elections.urls")),
|
||||
path("auth/", include("shared.auth.urls")),
|
||||
path("authens/", include("authens.urls")),
|
||||
]
|
||||
|
||||
if "debug_toolbar" in settings.INSTALLED_APPS:
|
||||
|
|
|
@ -1,62 +1,37 @@
|
|||
from authens.backends import ENSCASBackend
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
from .utils import get_cas_client
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class ENSCASBackend:
|
||||
"""ENS CAS authentication backend.
|
||||
|
||||
Implement standard CAS v3 authentication
|
||||
"""
|
||||
|
||||
def authenticate(self, request, ticket=None):
|
||||
cas_client = get_cas_client(request)
|
||||
cas_login, attributes, _ = cas_client.verify_ticket(ticket)
|
||||
|
||||
if cas_login is None:
|
||||
# Authentication failed
|
||||
return None
|
||||
cas_login = self.clean_cas_login(cas_login)
|
||||
|
||||
if request:
|
||||
request.session["CASCONNECTED"] = True
|
||||
|
||||
return self._get_or_create(cas_login, attributes)
|
||||
class CASBackend(ENSCASBackend):
|
||||
"""ENS CAS authentication backend, customized to get the full name at connection."""
|
||||
|
||||
def clean_cas_login(self, cas_login):
|
||||
return cas_login.strip().lower()
|
||||
|
||||
def _get_or_create(self, cas_login, attributes):
|
||||
"""Handles account retrieval and creation for CAS authentication.
|
||||
|
||||
- If no CAS account exists, create one;
|
||||
- If a matching CAS account exists, retrieve it.
|
||||
"""
|
||||
return f"cas__{cas_login.strip().lower()}"
|
||||
|
||||
def create_user(self, username, attributes):
|
||||
email = attributes.get("email")
|
||||
name = attributes.get("name")
|
||||
|
||||
try:
|
||||
user = UserModel.objects.get(username=cas_login)
|
||||
except UserModel.DoesNotExist:
|
||||
user = None
|
||||
return UserModel.objects.create_user(
|
||||
username=username, email=email, full_name=name
|
||||
)
|
||||
|
||||
if user is None:
|
||||
user = UserModel.objects.create_user(
|
||||
username=cas_login, email=email, full_name=name
|
||||
)
|
||||
return user
|
||||
|
||||
# Django boilerplate.
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
return UserModel.objects.get(pk=user_id)
|
||||
except UserModel.DoesNotExist:
|
||||
class PwdBackend(ModelBackend):
|
||||
"""Password authentication"""
|
||||
|
||||
def authenticate(self, request, username=None, password=None):
|
||||
if username is None or password is None:
|
||||
return None
|
||||
|
||||
return super().authenticate(
|
||||
request, username=f"pwd__{username}", password=password
|
||||
)
|
||||
|
||||
|
||||
class ElectionBackend(ModelBackend):
|
||||
"""Authentication for a specific election.
|
||||
|
@ -70,17 +45,12 @@ class ElectionBackend(ModelBackend):
|
|||
return None
|
||||
|
||||
try:
|
||||
user = UserModel.objects.get(username=f"{election_id}__{login}")
|
||||
user = UserModel.objects.get(
|
||||
username=f"{election_id}__{login}", election=election_id
|
||||
)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
|
||||
if user.check_password(password):
|
||||
return user
|
||||
return None
|
||||
|
||||
# Django boilerplate.
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
return UserModel.objects.get(pk=user_id)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from django import forms
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth import forms as auth_forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class ElectionAuthForm(forms.Form):
|
||||
"""Adapts Django's AuthenticationForm to allow for an election specific login."""
|
||||
|
@ -50,3 +53,11 @@ class ElectionAuthForm(forms.Form):
|
|||
),
|
||||
code="invalid_login",
|
||||
)
|
||||
|
||||
|
||||
class PwdResetForm(auth_forms.PasswordResetForm):
|
||||
"""Restricts the search for password users, i.e. whose username starts with pwd__."""
|
||||
|
||||
def get_users(self, email):
|
||||
users = super().get_users(email)
|
||||
return (u for u in users if u.username.split("__")[0] == "pwd")
|
||||
|
|
53
shared/templates/authens/login_switch.html
Normal file
53
shared/templates/authens/login_switch.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Choisissez la méthode de connexion" %}</h1>
|
||||
<hr>
|
||||
|
||||
{# Indications de connexion #}
|
||||
{% comment %}
|
||||
{% if method %}
|
||||
<div class="message is-warning">
|
||||
<div class="message-body">
|
||||
{% if method == "PWD" %}
|
||||
{% trans "Pour voter lors de cette élection, vous devez vous connecter à l'aide des identifiants reçus par mail. Choisissez la connexion par mot de passe." %}
|
||||
{% elif method == "CAS" %}
|
||||
{% trans "Pour voter lors de cette élection, vous devez vous connecter à l'aide du CAS élève." %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
|
||||
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-parent">
|
||||
<a class="tile is-child notification is-primary" href="{% url "authens:login.cas" %}?next={{ next }}">
|
||||
<div class="subtitle has-text-centered mb-2">
|
||||
<span class="icon has-text-white">
|
||||
<i class="fas fa-school"></i>
|
||||
</span>
|
||||
<span class="ml-3">{% trans "Connexion via CAS" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="tile is-parent">
|
||||
<a class="tile is-child notification" href="{% url "authens:login.pwd" %}?next={{ next }}">
|
||||
<div class="subtitle has-text-centered mb-2">
|
||||
<span class="icon">
|
||||
<i class="fas fa-key"></i>
|
||||
</span>
|
||||
<span class="ml-3">{% trans "Connexion par mot de passe" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
56
shared/templates/authens/pwd_login.html
Normal file
56
shared/templates/authens/pwd_login.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Connexion par mot de passe" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=True %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-outlined is-primary is-light">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{% trans "Enregistrer" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<a class="button is-primary" href="{% url 'authens:login' %}?next={{ next }}">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-undo-alt"></i>
|
||||
</span>
|
||||
<span>{% trans "Retour" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-centered">
|
||||
<div class="control">
|
||||
<div class="help has-text-centered">
|
||||
<span>{% trans "Mot de passe oublié :" %}</span>
|
||||
<a class="tag has-text-primary" href="{% url 'authens:reset.pwd' %}">
|
||||
<span>{% trans "Réinitialiser mon mot de passe." %}</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-lock-open"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
42
shared/templates/authens/pwd_reset.html
Normal file
42
shared/templates/authens/pwd_reset.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Réinitialisation du mot de passe" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=True %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-outlined is-primary is-light">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{% trans "Envoyer un mail" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<a class="button is-primary" href="{% url 'authens:login.pwd' %}?next={{ next }}">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-undo-alt"></i>
|
||||
</span>
|
||||
<span>{% trans "Retour" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
33
shared/templates/authens/pwd_reset_confirm.html
Normal file
33
shared/templates/authens/pwd_reset_confirm.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Réinitialisation du mot de passe" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=True %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-outlined is-primary is-light">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{% trans "Enregistrer" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -55,22 +55,19 @@
|
|||
</div>
|
||||
|
||||
<div class="level-item ml-3">
|
||||
<a class="icon is-size-1 has-text-white" href="{% url 'auth.logout' %}?next={% if view.get_next_url %}{{ view.get_next_url }}{% else %}/{% endif %}">
|
||||
<a class="icon is-size-1 has-text-white" href="{% url 'authens:logout' %}?next={% if view.get_next_url %}{{ view.get_next_url }}{% else %}/{% endif %}">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="level-item">
|
||||
{% if election %}
|
||||
<a class="icon is-size-1 has-text-white" href="{% url 'auth.select' %}?next={{ request.path }}&election_id={{ election.id }}&method={{ election.preferred_method }}">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
<a class="tag has-text-primary is-size-5" href="{% url 'authens:login' %}?next={{ request.path }}">
|
||||
<span>{% trans "Se connecter" %}</span>
|
||||
<span class="icon">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="icon is-size-1 has-text-white" href="{% url 'auth.cas' %}?next={{ request.path }}">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
{% endfor %}
|
||||
|
||||
{% if field.help_text %}
|
||||
<p class="help">
|
||||
<div class="help">
|
||||
{{ field.help_text|safe }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
42
shared/templates/registration/logged_out.html
Normal file
42
shared/templates/registration/logged_out.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Déconnexion réussie" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=True %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<a class="button is-fullwidth is-outlined is-primary is-light" href="{% url 'authens:login' %}">
|
||||
<span>{% trans "Se reconnecter" %}</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-unlock"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<a class="button is-primary" href="{% url 'kadenios' %}">
|
||||
<span>{% trans "Accueil" %}</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-home"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
Loading…
Reference in a new issue