Projet

Général

Profil

0001-remove-auth2_ssl-33992.patch

Benjamin Dauvergne, 14 juin 2019 15:24

Télécharger (47,3 ko)

Voir les différences:

Subject: [PATCH 1/2] remove auth2_ssl (#33992)

 src/authentic2/auth2_auth/auth2_ssl/README    | 180 ------------------
 .../auth2_auth/auth2_ssl/__init__.py          |  15 --
 src/authentic2/auth2_auth/auth2_ssl/admin.py  |  25 ---
 .../auth2_auth/auth2_ssl/app_settings.py      |  51 -----
 .../auth2_auth/auth2_ssl/authentic_ssl.vhost  |  52 -----
 .../auth2_auth/auth2_ssl/authenticators.py    |  44 -----
 .../auth2_auth/auth2_ssl/backends.py          | 157 ---------------
 .../auth2_ssl/locale/fr/LC_MESSAGES/django.po |  89 ---------
 .../auth2_auth/auth2_ssl/middleware.py        |  32 ----
 .../migrations/0003_auto_20190614_1438.py     |  22 +++
 src/authentic2/auth2_auth/auth2_ssl/models.py |  40 ----
 .../templates/auth/account_linking_ssl.html   |  45 -----
 .../templates/auth/login_form_ssl.html        |  14 --
 .../auth2_ssl/templates/ssl/profile.html      |  33 ----
 src/authentic2/auth2_auth/auth2_ssl/urls.py   |  33 ----
 src/authentic2/auth2_auth/auth2_ssl/util.py   | 104 ----------
 src/authentic2/auth2_auth/auth2_ssl/views.py  | 141 --------------
 .../locale/fr/LC_MESSAGES/django.po           |  67 -------
 src/authentic2/settings.py                    |   1 -
 19 files changed, 22 insertions(+), 1123 deletions(-)
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/README
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/admin.py
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/app_settings.py
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/authentic_ssl.vhost
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/authenticators.py
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/backends.py
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/locale/fr/LC_MESSAGES/django.po
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/middleware.py
 create mode 100644 src/authentic2/auth2_auth/auth2_ssl/migrations/0003_auto_20190614_1438.py
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/models.py
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/templates/auth/account_linking_ssl.html
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/templates/auth/login_form_ssl.html
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/templates/ssl/profile.html
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/urls.py
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/util.py
 delete mode 100644 src/authentic2/auth2_auth/auth2_ssl/views.py
 delete mode 100644 src/authentic2/auth2_auth/locale/fr/LC_MESSAGES/django.po
src/authentic2/auth2_auth/auth2_ssl/README
1
================================
2
Enable SSL Client authentication
3
================================
4

  
5
Intro
6
=====
7
Tested with Apache 2 and mod_ssl.
8
Django over mod_wsgi. From http://docs.djangoproject.com/en/dev/howto/deployment/modwsgi/
9
"Deploying Django with Apache and mod_wsgi is the recommended way to get Django into production."
10

  
11
Generate Keys
12
=============
13
* Create a CA (passphrase)
14
openssl genrsa -des3 -out ca.key 2048
15
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt
16
openssl x509 -in ca.crt -text -noout
17
* Server key material (challenge)
18
openssl genrsa -des3 -out server.key 1024
19
openssl req -new -key server.key -out server.csr
20
openssl x509 -req -in server.csr -out server.crt -sha1 -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650
21
openssl x509 -in server.crt -text -noout
22
* User Key material (challenge/password)
23
openssl genrsa -des3 -out c.key 1024
24
openssl req -new -key c.key -out c.csr
25
openssl x509 -req -in c.csr -out c.crt -sha1 -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650
26
openssl pkcs12 -export -in c.crt -inkey c.key -name "Mikael Ates" -out c.p12
27
openssl pkcs12 -in c.p12 -clcerts -nokeys -info
28

  
29
Configure Apache and WSGI
30
=========================
31
Add a file django.wsgi, e.g.:
32
"""
33
import os
34
import sys
35

  
36
sys.path.append("/usr/local/lib/python2.6/site-packages/")
37
try:
38
    import lasso
39
except:
40
    print("Unable to import Lasso.", file=sys.stderr)
41

  
42
apache_configuration= os.path.dirname(__file__)
43
project = os.path.dirname(apache_configuration)
44
sys.path.append(project)
45
try:
46
    import authentic2.settings
47
    os.environ['DJANGO_SETTINGS_MODULE'] = 'authentic2.settings'
48
except:
49
    print("Unable to import settings.", file=sys.stderr)
50

  
51
import django.core.handlers.wsgi
52
application = django.core.handlers.wsgi.WSGIHandler()
53
"""
54

  
55
Activate apache2 modules:
56
* a2enmod wsgi
57
* a2enmod ssl
58

  
59
Add a Apache vhost for SSL.
60
"""
61
<IfModule mod_ssl.c>
62
<VirtualHost *:443>
63

  
64
LimitInternalRecursion 1000
65
ServerAdmin webmaster@entrouvert.org
66
ServerName localhost
67

  
68
Alias /media/admin/ /usr/local/lib/python2.6/dist-packages/django/contrib/admin/media/
69

  
70
WSGIScriptAlias / /Donnees/devs/authentic/apache/django.wsgi
71

  
72
<Directory /Donnees/devs/authentic/>
73
SSLVerifyClient optional_no_ca
74
Options Indexes MultiViews FollowSymLinks
75
AllowOverride None
76
Order deny,allow
77
Allow from all
78
</Directory>
79

  
80
SSLEngine on
81
SSLCipherSuite HIGH:MEDIUM
82
SSLProtocol all -SSLv2
83

  
84
SSLCertificateFile /Donnees/devs/authentic/apache/key_mat/server.crt
85
SSLCertificateKeyFile /Donnees/devs/authentic/apache/key_mat/server.key
86

  
87
SSLCertificateChainFile /Donnees/devs/authentic/apache/key_mat/ca.crt
88
SSLCACertificateFile /Donnees/devs/authentic/apache/key_mat/ca.crt
89

  
90
SSLOptions +StdEnvVars +ExportCertData
91

  
92
BrowserMatch "MSIE [2-6]" \
93
	nokeepalive ssl-unclean-shutdown \
94
	downgrade-1.0 force-response-1.0
95
BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown
96

  
97
</VirtualHost>
98
</IfModule>
99
"""
100

  
101
Give rights to Apache on your Authentic directory.
102
Reload Apache.
103

  
104
Configure Authentic
105
===================
106

  
107
Key                        Description
108
-------------------------- ----------------------------------------
109
ACCEPT_SELF_SIGNED         accept certificate for which the validation failed,
110
                           default: False
111
STRICT_MATCH               do a binary compare to match certificate and users,
112
                           default: False
113
SUBJECT_MATCH_KEYS         SSL information to use to match recorded
114
                           certificates. default: ('subject_dn', 'issuer_dn'),
115
                           possible values: serial, subject_dn, issuer_dn, cert.
116
CREATE_USERNAME_CALLBACK   function receiving a SSLInfo object as first
117
                           parameter and returning a username, default: None
118
CREATE_USER                function receiving a SSLInfo object as first
119
                           parameter and returning a user, default: None
120
USE_COOKIE                 to be described
121

  
122
in settings.py:
123
Set AUTH_SSL = True
124
To create a user with the mail adress as identifier:
125
SSLAUTH_CREATE_USER = True
126
To use another identifier:
127
def myusernamegen(ssl_info):
128
    import re
129
    if(ssl_info.subject_cn):
130
        return return re.sub('[^a-zA-Z0-9]', '_', ssl_info.subject_cn)
131
    else:
132
        return return re.sub('[^a-zA-Z0-9]', '_', ssl_info.serial)
133
SSLAUTH_CREATE_USERNAME_CALLBACK = myusernamegen
134

  
135

  
136
Nginx configuration
137
===================
138

  
139
You must be able to retrieve SSL environment variable, for example with the
140
SCGI backend you must add those lines to /etc/nginx/scgi_params::
141

  
142
    scgi_param SSL_CLIENT_CERT $ssl_client_cert;
143
    scgi_param SSL_CLIENT_RAW_CERT $ssl_client_raw_cert;
144
    scgi_param SSL_CLIENT_S_DN $ssl_client_s_dn;
145
    scgi_param SSL_CLIENT_I_DN $ssl_client_i_dn;
146
    scgi_param SSL_CLIENT_SERIAL $ssl_client_serial;
147
    scgi_param SSL_CLIENT_M_SERIAL $ssl_client_serial;
148
    scgi_param SSL_CLIENT_VERIFY $ssl_client_verify;
149

  
150
It would be the same with FCGI but using the fcgi_param directive in the
151
fcgi_params file. It does not currently work when using proxy_pass.
152

  
153
A virtualhost configuration example::
154

  
155
    server {
156
      listen 80;
157
      server_name authentic.localhost;
158

  
159
      rewrite ^ https://$server_name$request_uri? permanent;
160
    }
161

  
162
    server {
163
      listen 443;
164
      server_name authentic.localhost;
165

  
166
      ssl on;
167
      ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
168
      ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
169
      ssl_verify_client optional_no_ca;
170

  
171
      location / {
172
      include scgi_params;
173
            scgi_pass localhost:8000;
174
      }
175
    }
176

  
177
The serveur must be run using the SCGI protocol, with this command line for
178
example::
179

  
180
    ./manage.py runfcgi protocol=scgi method=threaded daemonize=false host=localhost port=8000
src/authentic2/auth2_auth/auth2_ssl/__init__.py
16 16

  
17 17

  
18 18
class Plugin(object):
19
    def get_before_urls(self):
20
        from . import app_settings
21
        from django.conf.urls import include, url
22
        from authentic2.decorators import setting_enabled, required
23

  
24
        return required(
25
            setting_enabled('ENABLE', settings=app_settings),
26
            [url(r'^accounts/sslauth/', include(__name__ + '.urls'))])
27

  
28 19
    def get_apps(self):
29 20
        return [__name__]
30

  
31
    def get_authentication_backends(self):
32
        return ['authentic2.auth2_auth.auth2_ssl.backends.SSLBackend']
33

  
34
    def get_authenticators(self):
35
        return ['authentic2.auth2_auth.auth2_ssl.authenticators.SSLAuthenticator']
src/authentic2/auth2_auth/auth2_ssl/admin.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.contrib import admin
18

  
19
from . import models
20

  
21

  
22
class ClientCertificateAdmin(admin.ModelAdmin):
23
    list_display = ('user', 'subject_dn', 'issuer_dn', 'serial')
24

  
25
admin.site.register(models.ClientCertificate, ClientCertificateAdmin)
src/authentic2/auth2_auth/auth2_ssl/app_settings.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import sys
18

  
19

  
20
class AppSettings(object):
21
    '''Thanks django-allauth'''
22
    __DEFAULTS = dict(
23
        # settings for TEST only, make it easy to simulate the SSL
24
        # environment
25
        ENABLE=False,
26
        FORCE_ENV={},
27
        ACCEPT_SELF_SIGNED=False,
28
        STRICT_MATCH=False,
29
        SUBJECT_MATCH_KEYS=('subject_dn', 'issuer_dn'),
30
        CREATE_USERNAME_CALLBACK=None,
31
        USE_COOKIE=False,
32
        CREATE_USER=False,
33
    )
34

  
35
    def __init__(self, prefix):
36
        self.prefix = prefix
37

  
38
    def _setting(self, name, dflt):
39
        from django.conf import settings
40
        return getattr(settings, self.prefix + name, dflt)
41

  
42
    def __getattr__(self, name):
43
        if name not in self.__DEFAULTS:
44
            raise AttributeError(name)
45
        return self._setting(name, self.__DEFAULTS[name])
46

  
47

  
48
app_settings = AppSettings('SSLAUTH_')
49
app_settings.__name__ = __name__
50
app_settings.__file__ = __file__
51
sys.modules[__name__] = app_settings
src/authentic2/auth2_auth/auth2_ssl/authentic_ssl.vhost
1
<IfModule mod_ssl.c>
2
<VirtualHost *:443>
3

  
4
LimitInternalRecursion 1000
5
ServerAdmin webmaster@entrouvert.org
6
ServerName localhost
7

  
8
#Alias /media/ /Donnees/devs/Authentic/authentic/media/
9
Alias /media/admin/ /usr/local/lib/python2.6/dist-packages/django/contrib/admin/media/
10

  
11
WSGIScriptAlias / /Donnees/devs/Authentic/authentic/apache/django.wsgi
12

  
13
<Location />
14

  
15
Options Indexes MultiViews FollowSymLinks
16
AllowOverride None
17
Order deny,allow
18
Allow from all
19

  
20
</Location>
21

  
22
<Location /sslauth/>
23

  
24
SSLVerifyClient require
25

  
26
Options Indexes MultiViews FollowSymLinks
27
AllowOverride None
28
Order deny,allow
29
Allow from all
30

  
31
</Location>
32

  
33
SSLEngine on
34
SSLCipherSuite HIGH:MEDIUM
35
SSLProtocol all -SSLv2
36

  
37
SSLCertificateFile /Donnees/devs/Authentic/authentic/apache/key_mat/server.crt
38
SSLCertificateKeyFile /Donnees/devs/Authentic/authentic/apache/key_mat/server.key
39

  
40
SSLCertificateChainFile /Donnees/devs/Authentic/authentic/apache/key_mat/ca.crt
41
SSLCACertificateFile /Donnees/devs/Authentic/authentic/apache/key_mat/ca.crt
42

  
43
SSLOptions +StdEnvVars +ExportCertData
44
#SSLProtocol all
45

  
46
	BrowserMatch "MSIE [2-6]" \
47
		nokeepalive ssl-unclean-shutdown \
48
		downgrade-1.0 force-response-1.0
49
	BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown
50

  
51
</VirtualHost>
52
</IfModule>
src/authentic2/auth2_auth/auth2_ssl/authenticators.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.utils.translation import ugettext_lazy as _
18
import django.forms
19

  
20
from . import views, app_settings
21
from authentic2.utils import redirect_to_login
22

  
23

  
24
class SSLAuthenticator(object):
25
    def enabled(self):
26
        return app_settings.ENABLE
27

  
28
    def id(self):
29
        return 'ssl'
30

  
31
    def name(self):
32
        return _('SSL with certificates')
33

  
34
    def form(self):
35
        return django.forms.Form
36

  
37
    def post(self, request, form, nonce, next_url):
38
        return redirect_to_login(request, login_url='user_signin_ssl',)
39

  
40
    def template(self):
41
        return 'auth/login_form_ssl.html'
42

  
43
    def profile(self, request, *args, **kwargs):
44
        return views.profile(request, *args, **kwargs)
src/authentic2/auth2_auth/auth2_ssl/backends.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.contrib.auth import get_user_model
18
from django.db.models import Q
19
import logging
20

  
21
from authentic2.backends import is_user_authenticable
22

  
23
from . import models, app_settings
24

  
25
logger = logging.getLogger(__name__)
26

  
27
User = get_user_model()
28

  
29

  
30
class AuthenticationError(Exception):
31
    pass
32

  
33

  
34
class SSLBackend:
35
    """
36
    authenticates a client certificate against the records stored
37
    in ClientCertificate model and looks up the corresponding django user
38

  
39
    In all methods, the ssl_info parameter is supposed to be an SSLInfo
40
    instance
41
    """
42
    supports_object_permissions = False
43
    supports_anonymous_user = False
44

  
45
    def authenticate(self, ssl_info):
46
        cert = self.get_certificate(ssl_info)
47
        if cert is None:
48
            return None
49
        else:
50
            if not is_user_authenticable(cert.user):
51
                logger.info('SSLAuth: authentication refused by user filters')
52
                return None
53
            return cert.user
54

  
55
    def get_user(self, user_id):
56
        """
57
        simply return the user object. That way, we only need top look-up the
58
        certificate once, when loggin in
59
        """
60
        try:
61
            return User.objects.get(id=user_id)
62
        except User.DoesNotExist:
63
            return None
64

  
65
    def get_certificate(self, ssl_info):
66
        """
67
        returns a ClientCertificate object for the passed
68
        cert data or None if not found
69
        """
70

  
71
        if app_settings.STRICT_MATCH:
72
            # compare complete certificate in strict match
73
            if not ssl_info.cert:
74
                logger.error('SSLAuth: strict match required but PEM encoded certificate '
75
                             'not found in environment. Check your server settings')
76
                return None
77
            query = Q(cert=ssl_info.cert)
78
        else:
79
            query_args = {}
80
            for key in app_settings.SUBJECT_MATCH_KEYS:
81
                if not ssl_info.get(key):
82
                    logger.error(u'SSLAuth: key %s is missing from ssl_info', key)
83
                    return None
84
                query_args[key] = ssl_info.get(key)
85

  
86
            query = Q(**query_args)
87
        try:
88
            cert = models.ClientCertificate.objects.select_related().get(query)
89
            return cert
90
        except models.ClientCertificate.DoesNotExist:
91
            return None
92

  
93
    def create_user(self, ssl_info):
94
        """
95
        This method creates a new django User and ClientCertificate record
96
        for the passed certificate info. It does not create an issuer record,
97
        just a subject for the ClientCertificate.
98
        """
99
        # auto creation only created a DN for the subject, not the issuer
100

  
101
        # get username and check if the user exists already
102
        if app_settings.CREATE_USERNAME_CALLBACK:
103
            build_username = app_settings.CEATE_USERNAME_CALLBACK
104
        else:
105
            build_username = self.build_username
106

  
107
        username = build_username(ssl_info)
108

  
109
        try:
110
            user = User.objects.get(username=username)
111
        except User.DoesNotExist:
112
            if app_settings.CREATE_USER_CALLBACK:
113
                build_user = app_settings.CREATE_USER_CALLBACK
114
            else:
115
                build_user = self.build_user
116
            user = build_user(username, ssl_info)
117

  
118
        # create the certificate record and save
119
        self.link_user(ssl_info, user)
120
        return user
121

  
122
    def link_user(self, ssl_info, user):
123
        """
124
        This method creates a new django User and ClientCertificate record
125
        for the passed certificate info. It does not create an issuer record,
126
        just a subject for the ClientCertificate.
127
        """
128
        # create the certificate record and save
129
        cert = models.ClientCertificate()
130
        cert.user = user
131
        cert.subject_dn = ssl_info.subject_dn
132
        cert.issuer_dn = ssl_info.issuer_dn
133
        cert.serial = ssl_info.serial
134
        cert.cert = ssl_info.cert
135
        cert.save()
136

  
137
        return user
138

  
139
    def build_user(self, username, ssl_info):
140
        """
141
        create a valid (and stored) django user to be associated with the
142
        newly created certificate record. This method can be "overwritten" by
143
        using the SSLAUTH_CREATE_USER_CALLBACK setting.
144
        """
145
        User = get_user_model()
146
        user = User()
147
        setattr(user, User.USERNAME_FIELD, username)
148
        if hasattr(User, 'set_unusable_password'):
149
            user.set_unusable_password()
150
        user.is_active = True
151
        user.save()
152
        return user
153

  
154
    @classmethod
155
    def get_saml2_authn_context(cls):
156
        from authentic2.compat_lasso import lasso
157
        return lasso.SAML2_AUTHN_CONTEXT_X509
src/authentic2/auth2_auth/auth2_ssl/locale/fr/LC_MESSAGES/django.po
1
# authentic2 auth ssl french l10n
2
# Copyright (C) 2015 Entr'ouvert
3
# This file is distributed under the same license as the Authentic package.
4
# Frederic Peters <fpeters@entrouvert.com>, 2010.
5
#
6
msgid ""
7
msgstr ""
8
"Project-Id-Version: Authentic\n"
9
"Report-Msgid-Bugs-To: \n"
10
"POT-Creation-Date: 2019-03-08 10:43+0100\n"
11
"PO-Revision-Date: 2013-07-23 17:41+0200\n"
12
"Last-Translator: Mikaël Ates <mates@entrouvert.com>\n"
13
"Language-Team: None\n"
14
"Language: fr\n"
15
"MIME-Version: 1.0\n"
16
"Content-Type: text/plain; charset=UTF-8\n"
17
"Content-Transfer-Encoding: 8bit\n"
18
"Plural-Forms: nplurals=2; plural=n>1;\n"
19

  
20
#: src/authentic2/auth2_auth/auth2_ssl/frontends.py:16
21
msgid "SSL with certificates"
22
msgstr "Certificats SSL"
23

  
24
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/account_linking_ssl.html:5
25
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/account_linking_ssl.html:9
26
msgid "Log in to link your certificate with an existing account"
27
msgstr "connectez-vous pour lier votre certificat avec un compte existant"
28

  
29
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/account_linking_ssl.html:18
30
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/account_linking_ssl.html:25
31
msgid "Username:"
32
msgstr "Nom d'utilisateur :"
33

  
34
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/account_linking_ssl.html:21
35
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/account_linking_ssl.html:29
36
msgid "Password:"
37
msgstr "Mot de passe :"
38

  
39
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/account_linking_ssl.html:34
40
msgid "Create me a new account"
41
msgstr "Me créer un nouveau compte"
42

  
43
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/account_linking_ssl.html:38
44
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/login_form_ssl.html:9
45
msgid "Log in"
46
msgstr "S'identifier"
47

  
48
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/login_form_ssl.html:4
49
msgid "Login using a certificate."
50
msgstr "Connexion par certificat."
51

  
52
#: src/authentic2/auth2_auth/auth2_ssl/templates/auth/login_form_ssl.html:11
53
msgid "Cancel"
54
msgstr "Annuler"
55

  
56
#: src/authentic2/auth2_auth/auth2_ssl/templates/ssl/profile.html:3
57
msgid "SSL Certificates"
58
msgstr "Certificats SSL"
59

  
60
#: src/authentic2/auth2_auth/auth2_ssl/templates/ssl/profile.html:21
61
msgid "Delete"
62
msgstr "Supprimer"
63

  
64
#: src/authentic2/auth2_auth/auth2_ssl/templates/ssl/profile.html:28
65
msgid "Add a certificate?"
66
msgstr "Ajouter un certificat ?"
67

  
68
#: src/authentic2/auth2_auth/auth2_ssl/templates/ssl/profile.html:30
69
msgid "Add"
70
msgstr "Ajouter"
71

  
72
#: src/authentic2/auth2_auth/auth2_ssl/views.py:28
73
msgid "SSL Client Authentication failed. No client certificate found."
74
msgstr "Echec de l'authentification cliente SSL. Aucun certificat trouvé."
75

  
76
#: src/authentic2/auth2_auth/auth2_ssl/views.py:35
77
msgid "SSL Client Authentication failed. Your client certificate is not valid."
78
msgstr ""
79
"Echec de l'authentification cliente SSL. Votre certificat n'est pas valide."
80

  
81
#: src/authentic2/auth2_auth/auth2_ssl/views.py:54
82
#: src/authentic2/auth2_auth/auth2_ssl/views.py:70
83
#: src/authentic2/auth2_auth/auth2_ssl/views.py:118
84
msgid "SSL Client Authentication failed. Internal server error."
85
msgstr "Echec de l'authentification SSL. Erreur interne du serveur."
86

  
87
#: src/authentic2/auth2_auth/auth2_ssl/views.py:140
88
msgid "Certificate deleted."
89
msgstr "Certificat supprimé."
src/authentic2/auth2_auth/auth2_ssl/middleware.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.contrib.auth import authenticate, login
18

  
19
from . import util, app_settings
20

  
21

  
22
class SSLAuthMiddleware(object):
23
    """
24
    attempts to find a valid user based on the client certificate info
25
    """
26
    def process_request(self, request):
27
        if app_settings.USE_COOKIE and request.user.is_authenticated():
28
            return
29
        ssl_info = util.SSLInfo(request)
30
        user = authenticate(ssl_info=ssl_info)
31
        if user and request.user != user:
32
            login(request, user)
src/authentic2/auth2_auth/auth2_ssl/migrations/0003_auto_20190614_1438.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.20 on 2019-06-14 12:38
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('auth2_ssl', '0002_auto_20150409_1840'),
12
    ]
13

  
14
    operations = [
15
        migrations.RemoveField(
16
            model_name='clientcertificate',
17
            name='user',
18
        ),
19
        migrations.DeleteModel(
20
            name='ClientCertificate',
21
        ),
22
    ]
src/authentic2/auth2_auth/auth2_ssl/models.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.db import models
18
from django.conf import settings
19
from django.utils import six
20

  
21
from . import util
22

  
23

  
24
@six.python_2_unicode_compatible
25
class ClientCertificate(models.Model):
26
    serial = models.CharField(max_length=255, blank=True)
27
    subject_dn = models.CharField(max_length=255)
28
    issuer_dn = models.CharField(max_length=255)
29
    cert = models.TextField()
30
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
31

  
32
    def __str__(self):
33
        return self.subject_dn
34

  
35
    def explode_subject_dn(self):
36
        return util.explode_dn(self.subject_dn)
37

  
38
    def explode_issuer_dn(self):
39
        return util.explode_dn(self.issuer_dn)
40

  
src/authentic2/auth2_auth/auth2_ssl/templates/auth/account_linking_ssl.html
1
{% extends "authentic2/base-page.html" %}
2
{% load i18n %}
3

  
4
{% block title %}
5
{% trans "Log in to link your certificate with an existing account" %}
6
{% endblock %}
7

  
8
{% block content %}
9
<p>* {% trans "Log in to link your certificate with an existing account" %}</p>
10
<div id="login-actions">
11
  <form id="login-form" method="post" action="{% url "post_account_linking" %}">
12
  {% csrf_token %}
13
  <ul class="errorlist">
14
    {% for error in form.non_field_errors %}
15
      <li>{{ error|escape }}</li>
16
    {% endfor %}
17
    {% for error in form.username.errors %}
18
      <li>{% trans "Username:" %} {{ error|escape }}</li>
19
    {% endfor %}
20
    {% for error in form.password.errors %}
21
      <li>{% trans "Password:" %} {{ error|escape }}</li>
22
    {% endfor %}
23
  </ul>
24
  <p>
25
    <label for="id_username">{% trans "Username:" %}</label>
26
    <input id="id_username" type="text" name="username" maxlength="30" />
27
  </p>
28
  <p>
29
    <label for="id_password">{% trans "Password:" %}</label>
30
    <input type="password" name="password" id="id_password" />
31
  </p>
32

  
33
  <p>
34
    <label for="id_do_creation">{% trans "Create me a new account" %}</label>
35
    <input type="checkbox" name="do_creation" id="id_do_creation" />
36
  </p>
37

  
38
  <button class="submit-button">{% trans 'Log in' %}</button>
39
  <input type="hidden" name="next" value="{{ next_url }}" />
40
</form>
41
</div>
42
<script type="text/javascript">
43
document.getElementById('id_username').focus();
44
</script>
45
{% endblock %}
src/authentic2/auth2_auth/auth2_ssl/templates/auth/login_form_ssl.html
1
{% load i18n %}
2
<div id="login-ssl">
3
<p>
4
{% trans "Login using a certificate." %}
5
</p>
6
<form method="post" action="">
7
{% csrf_token %}
8
{{ form.as_p }}
9
<button class="submit-button" name="{{ submit_name }}">{% trans "Log in" %}</button>
10
{% if cancel %}
11
<button class="cancel-button" name="cancel" formnovalidate>{% trans 'Cancel' %}"</button>
12
{% endif %}
13
</form>
14
</div>
src/authentic2/auth2_auth/auth2_ssl/templates/ssl/profile.html
1
{% load i18n %}
2
<h4 id="a2-ssl-certificate-profile" class="a2-ssl-certificate-profile-title">
3
  {% trans "SSL Certificates" %}
4
</h4>
5

  
6
<div class="a2-ssl-certificate-profile-body">
7
  <ul class="a2-ssl-certificate-list">
8
  {% for certificate in certificates %}
9
    <li class="a2-ssl-certificate-item">
10
      <form action="{% url "delete_certificate" certificate_pk=certificate.pk %}"
11
            method="post">
12
        {% csrf_token %}
13
        <p class="a2-ssl-certificate-dn">
14
          <dl class="a2-ssl-certificate-dn-parts">
15
            {% for k, v in certificate.explode_subject_dn %}
16
              <dt class="a2-ssl-certificate-dn-part-name">{{ k }}</dt>
17
              <dd class="a2-ssl-certificate-dn-part-value">{{ v }}</dd>
18
            {% endfor %}
19
          </dl>
20
        </p>
21
        <button class="submit-button a2-ssl-certificate-submit-button">{% trans "Delete" %}</button>
22
      </form>
23
    </p>
24
  {% endfor %}
25
  </ul>
26
  <p>
27
    <form action="{% url "user_signin_ssl" %}" method="get">
28
      <label for="id_del_cert">{% trans "Add a certificate?" %}</label>
29
      <input type="hidden" name="next" value="{% url "account_management" %}#a2-ssl-certificate-profile" />
30
      <button class="submit-button a2-ssl-certificate-submit-button">{% trans "Add" %}</button>
31
    </form>
32
  </p>
33
</div>
src/authentic2/auth2_auth/auth2_ssl/urls.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url
18
from .views import (handle_request, post_account_linking, delete_certificate, error_ssl)
19

  
20
urlpatterns = [
21
    url(r'^$',
22
        handle_request,
23
        name='user_signin_ssl'),
24
    url(r'^post_account_linking/$',
25
        post_account_linking,
26
        name='post_account_linking'),
27
    url(r'^delete_certificate/(?P<certificate_pk>\d+)/$',
28
        delete_certificate,
29
        name='delete_certificate'),
30
    url(r'^error_ssl/$',
31
        error_ssl,
32
        name='error_ssl'),
33
]
src/authentic2/auth2_auth/auth2_ssl/util.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import base64
18
import six
19

  
20
from . import app_settings
21

  
22
X509_KEYS = {
23
    'subject_dn': 'SSL_CLIENT_S_DN',
24
    'issuer_dn': 'SSL_CLIENT_I_DN',
25
    'serial': ('SSL_CLIENT_M_SERIAL', 'SSL_CLIENT_SERIAL'),
26
    'cert': 'SSL_CLIENT_CERT',
27
    'verify': 'SSL_CLIENT_VERIFY',
28
}
29

  
30

  
31
def normalize_cert(certificate_pem):
32
    '''Normalize content of the certificate'''
33
    base64_content = ''.join(certificate_pem.splitlines()[1:-1])
34
    content = base64.b64decode(base64_content)
35
    return base64.b64encode(content)
36

  
37

  
38
def explode_dn(dn):
39
    '''Extract sub element of a DN as displayed by mod_ssl or nginx_ssl'''
40
    dn = dn.strip('/')
41
    parts = dn.split('/')
42
    parts = [part.split('=') for part in parts]
43
    parts = [(part[0], part[1].decode('string_escape').decode('utf-8')) for part in parts]
44
    return parts
45

  
46

  
47
TRANSFORM = {
48
    'cert': normalize_cert,
49
}
50

  
51

  
52
class SSLInfo(object):
53
    """
54
    Encapsulates the SSL environment variables in a read-only object. It
55
    attempts to find the ssl vars based on the type of request passed to the
56
    constructor. Currently only WSGIRequest and ModPythonRequest are
57
    supported.
58
    """
59
    def __init__(self, request):
60
        name = request.__class__.__name__
61
        if app_settings.FORCE_ENV:
62
            env = app_settings.FORCE_ENV
63
        elif name == 'WSGIRequest':
64
            env = request.environ
65
        elif name == 'ModPythonRequest':
66
            env = request._req.subprocess_env
67
        else:
68
            raise EnvironmentError('The SSL authentication currently only \
69
                works with mod_python or wsgi requests')
70
        self.read_env(env)
71
        pass
72

  
73
    def read_env(self, env):
74
        for attr, keys in X509_KEYS.items():
75
            if isinstance(keys, six.string_types):
76
                keys = [keys]
77
            for key in keys:
78
                if key in env and env[key]:
79
                    v = env[key]
80
                    if attr in TRANSFORM:
81
                        v = TRANSFORM[attr](v)
82
                    self.__dict__[attr] = v
83
                else:
84
                    self.__dict__[attr] = None
85

  
86
        if self.__dict__['verify'] == 'SUCCESS':
87
            self.__dict__['verify'] = True
88
        else:
89
            self.__dict__['verify'] = False
90

  
91
    def get(self, attr):
92
        return self.__getattr__(attr)
93

  
94
    def __getattr__(self, attr):
95
        if attr in self.__dict__:
96
            return self.__dict__[attr]
97
        else:
98
            raise AttributeError('SSLInfo does not contain key %s' % attr)
99

  
100
    def __setattr__(self, attr, value):
101
        raise AttributeError('SSL vars are read only!')
102

  
103
    def __repr__(self):
104
        return '<SSLInfo %s>' % self.__dict__
src/authentic2/auth2_auth/auth2_ssl/views.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import logging
18

  
19
from django.utils.translation import ugettext as _
20
from django.shortcuts import render
21
from django.views.decorators.csrf import csrf_exempt
22
from django.views.generic.base import TemplateView
23
from django.template.loader import render_to_string
24
from django.contrib import messages
25
from django.contrib.auth.forms import AuthenticationForm
26
from django.contrib.auth import authenticate, login
27

  
28

  
29
from authentic2.utils import continue_to_next_url, record_authentication_event, redirect, redirect_to_login
30

  
31
from . import models, util, app_settings
32

  
33
logger = logging.getLogger(__name__)
34

  
35

  
36
def handle_request(request):
37
    # Check certificate validity
38
    ssl_info = util.SSLInfo(request)
39
    accept_self_signed = app_settings.ACCEPT_SELF_SIGNED
40

  
41
    if not ssl_info.cert:
42
        logger.error('SSL Client Authentication failed: SSL CGI variable CERT is missing')
43
        messages.add_message(request, messages.ERROR,
44
                             _('SSL Client Authentication failed. No client certificate found.'))
45
        return redirect_to_login(request)
46
    elif not accept_self_signed and not ssl_info.verify:
47
        logger.error('SSL Client Authentication failed: SSL CGI variable VERIFY is not SUCCESS')
48
        messages.add_message(request, messages.ERROR,
49
                             _('SSL Client Authentication failed. Your client certificate is not valid.'))
50
        return redirect_to_login(request)
51

  
52
    # SSL entries for this certificate?
53
    user = authenticate(ssl_info=ssl_info)
54

  
55
    # If the user is logged in, no need to create an account
56
    # If there is an SSL entries, no need for account creation,
57
    # just need to login, treated after
58
    if 'do_creation' in request.session and not user \
59
            and not request.user.is_authenticated():
60
        from backends import SSLBackend
61
        if SSLBackend().create_user(ssl_info):
62
            user = authenticate(ssl_info=ssl_info)
63
            logger.info(u'account created for %s', user)
64
        else:
65
            logger.error('account creation failure')
66
            messages.add_message(request, messages.ERROR,
67
                                 _('SSL Client Authentication failed. Internal server error.'))
68
            return redirect_to_login(request)
69

  
70
    # No SSL entries and no user session, redirect account linking page
71
    if not user and not request.user.is_authenticated():
72
        return render(request, 'auth/account_linking_ssl.html')
73

  
74
    # No SSL entries but active user session, perform account linking
75
    if not user and request.user.is_authenticated():
76
        from backend import SSLBackend
77
        if not SSLBackend().link_user(ssl_info, request.user):
78
            logger.error('login() failed')
79
            messages.add_message(request, messages.ERROR,
80
                                 _('SSL Client Authentication failed. Internal server error.'))
81
            return redirect_to_login(request)
82
        logger.info('Successful linking of the SSL Certificate to an account')
83

  
84
    # SSL Entries found for this certificate,
85
    # if the user is logged out, we login
86
    if not request.user.is_authenticated():
87
        login(request, user)
88
        record_authentication_event(request, how='ssl')
89
        return continue_to_next_url(request)
90

  
91
    # SSL Entries found for this certificate, if the user is logged in, we
92
    # check that the SSL entry for the certificate is this user.
93
    # else, we make this certificate point on that user.
94
    if user.username != request.user.username:
95
        logger.warning(u'The certificate belongs to %s, but %s is logged with, we change the association!',
96
                       user, request.user)
97
        from backends import SSLBackend
98
        cert = SSLBackend().get_certificate(ssl_info)
99
        cert.user = request.user
100
        cert.save()
101
    return continue_to_next_url(request)
102

  
103

  
104
@csrf_exempt
105
def post_account_linking(request):
106
    if request.method == "POST":
107
        if 'do_creation' in request.POST and request.POST['do_creation'] == 'on':
108
            request.session['do_creation'] = 'do_creation'
109
            return redirect_to_login(request, login_url='user_signin_ssl')
110
        form = AuthenticationForm(data=request.POST)
111
        if form.is_valid():
112
            user = form.get_user()
113
            login(request, user)
114
            record_authentication_event(request, how='password')
115
            return redirect_to_login(request, login_url='user_signin_ssl')
116
        else:
117
            return render(request, 'auth/account_linking_ssl.html')
118
    else:
119
        return render(request, 'auth/account_linking_ssl.html')
120

  
121

  
122
def profile(request, template_name='ssl/profile.html', *args, **kwargs):
123
    context = kwargs.pop('context', {})
124
    certificates = models.ClientCertificate.objects.filter(user=request.user)
125
    context.update({'certificates': certificates})
126
    return render_to_string(template_name, context, request=request)
127

  
128

  
129
def delete_certificate(request, certificate_pk):
130
    qs = models.ClientCertificate.objects.filter(pk=certificate_pk)
131
    count = qs.count()
132
    qs.delete()
133
    if count:
134
        logger.info('client certificate %s deleted', certificate_pk)
135
        messages.info(request, _('Certificate deleted.'))
136
    return redirect(request, 'account_management', fragment='a2-ssl-certificate-profile')
137

  
138

  
139
class SslErrorView(TemplateView):
140
    template_name = 'error_ssl.html'
141
error_ssl = SslErrorView.as_view()
src/authentic2/auth2_auth/locale/fr/LC_MESSAGES/django.po
1
# French translation of Authentic
2
# Copyright (C) 2010, 2011 Entr'ouvert
3
# This file is distributed under the same license as the Authentic package.
4
# Frederic Peters <fpeters@entrouvert.com>, 2010.
5
#
6
msgid ""
7
msgstr ""
8
"Project-Id-Version: Authentic\n"
9
"Report-Msgid-Bugs-To: \n"
10
"POT-Creation-Date: 2013-07-23 18:01+0200\n"
11
"PO-Revision-Date: 2013-07-23 18:01+0200\n"
12
"Last-Translator: Mikaël Ates <mates@entrouvert.com>\n"
13
"Language-Team: None\n"
14
"Language: fr\n"
15
"MIME-Version: 1.0\n"
16
"Content-Type: text/plain; charset=UTF-8\n"
17
"Content-Transfer-Encoding: 8bit\n"
18
"Plural-Forms: nplurals=2; plural=n>1;\n"
19

  
20
#: backend.py:17 templates/auth/login_password_profile.html:2
21
msgid "Password"
22
msgstr "Mot de passe"
23

  
24
#: models.py:23
25
#, python-format
26
msgid "Authentication of %(who)s by %(how)s at %(when)s"
27
msgstr "Authentification de %(who)s par la méthode %(how)s à %(when)s"
28

  
29
#: templates/error_ssl.html:4
30
msgid "Error: authentication failure"
31
msgstr "Erreur: Echec de l'authentification"
32

  
33
#: templates/error_ssl.html:8
34
msgid "Authentication failure"
35
msgstr "Échec d'authentification"
36

  
37
#: templates/error_ssl.html:10
38
msgid "The SSL authentication has failed"
39
msgstr "L'authentification par certificat électronique a échouée."
40

  
41
#: templates/auth/login.html:5 templates/auth/login_form.html:6
42
msgid "Log in"
43
msgstr "S'identifier"
44

  
45
#: templates/auth/login_form.html:8
46
msgid "Cancel"
47
msgstr "Annuler"
48

  
49
#: templates/auth/login_form.html:14
50
msgid "Forgot password?"
51
msgstr "Mot de passe oublié ?"
52

  
53
#: templates/auth/login_form.html:14
54
msgid "Reset it!"
55
msgstr "Le réinitialiser !"
56

  
57
#: templates/auth/login_form.html:15
58
msgid "Not a member?"
59
msgstr "Pas un membre ?"
60

  
61
#: templates/auth/login_form.html:15
62
msgid "Register!"
63
msgstr "S'inscrire !"
64

  
65
#: templates/auth/login_password_profile.html:6
66
msgid "Change password"
67
msgstr "Modifier votre mot de passe"
src/authentic2/settings.py
132 132
    'authentic2.saml',
133 133
    'authentic2.idp',
134 134
    'authentic2.idp.saml',
135
    'authentic2.auth2_auth',
136 135
    'authentic2.attribute_aggregator',
137 136
    'authentic2.disco_service',
138 137
    'authentic2.manager',
139
-