login
*****
.. highlight:: python
https://gitlab.com/kb/login
- :doc:`dev-ember-auth`
- :doc:`sys-microsoft-azure`
Management Commands
===================
create-user-system-generated
----------------------------
Create the ``SYSTEM_GENERATED`` user:
.. code-block:: bash
django-admin create-user-system-generated
.. tip:: If you prefer to auto-create the ``SYSTEM_GENERATED`` user in code,
then just call ``get_system_generated_user`` directly e.g.
::
from login.util import get_system_generated_user
system_generated_user = get_system_generated_user()
list-of-users
-------------
To create a CSV file containing the ``username`` and full name of all the active
users on your site:
.. code-block:: bash
django-admin.py list-of-users out.csv
.. _app_login_openid_connect:
OpenID Connect
==============
.. note:: If you don't want to use OpenID Connect, then just set the following
in ``settings/base.py`` (``USE_OPENID_CONNECT = False``).
Register your application in the :doc:`sys-microsoft-azure` Portal.
Add the following to ``requirements/base.txt``::
kb-mozilla-django-oidc==
.. tip:: For more information, see `README.rst`_
in https://github.com/pkimber/kb-mozilla-django-oidc
.. tip:: See :doc:`dev-requirements` for the current version...
.. warning:: If the source code for ``mozilla-django-oidc`` is installed
instead of ``kb-mozilla-django-oidc``
(``head -50 .venv/lib/python3.10/site-packages/mozilla_django_oidc/utils.py``)
then ``uv clean cache kb-mozilla-django-oidc``
and ``uv clean cache mozilla-django-oidc``
Add the following to ``project/urls.py``::
re_path(r"^oidc/", view=include("mozilla_django_oidc.urls")),
Add the following to ``settings/base.py``::
MIDDLEWARE = (
# ...
"mozilla_django_oidc.middleware.SessionRefresh",
"reversion.middleware.RevisionMiddleware",
)
THIRD_PARTY_APPS = (
"mozilla_django_oidc",
.. note:: Probably best not to use ``SessionRefresh`` middleware on example
apps (it will log you out every 15 minutes).
::
# 17/01/2020, We are getting a redirect loop when we use this 'LOGIN_URL'
# LOGIN_URL = reverse_lazy("oidc_authentication_init")
LOGIN_URL = reverse_lazy("login")
# https://mozilla-django-oidc.readthedocs.io/
USE_OPENID_CONNECT = get_env_variable_bool("USE_OPENID_CONNECT")
AUTHENTICATION_BACKENDS = ("login.service.KBSoftwareOIDCAuthenticationBackend",)
LOGIN_REDIRECT_URL_FAILURE = reverse_lazy("login")
OIDC_CREATE_USER = False
OIDC_OP_AUTHORIZATION_ENDPOINT = get_env_variable(
"OIDC_OP_AUTHORIZATION_ENDPOINT"
)
OIDC_OP_JWKS_ENDPOINT = get_env_variable("OIDC_OP_JWKS_ENDPOINT")
OIDC_OP_TOKEN_ENDPOINT = get_env_variable("OIDC_OP_TOKEN_ENDPOINT")
OIDC_OP_USER_ENDPOINT = "NOT_USED_BY_KB_LOGIN_SERVICE"
OIDC_RP_CLIENT_ID = get_env_variable("OIDC_RP_CLIENT_ID")
OIDC_RP_CLIENT_SECRET = get_env_variable("OIDC_RP_CLIENT_SECRET")
OIDC_RP_SIGN_ALGO = get_env_variable("OIDC_RP_SIGN_ALGO")
OIDC_USE_NONCE = get_env_variable_bool("OIDC_USE_NONCE")
.. warning:: Double check your settings files to make sure you don't have other
``AUTHENTICATION_BACKENDS`` configured
i.e. The only ``AUTHENTICATION_BACKENDS`` in your project should
be the one in the section shown above.
.. warning:: Don't forget to set ``LOGIN_REDIRECT_URL_FAILURE`` for Django
projects which are the API for an Ember app.
If you do forget, the default URL is ``/`` which will not exist
for a Django *backend* project.
.. tip:: You may prefer to set ``LOGIN_REDIRECT_URL_FAILURE`` to
``reverse_lazy("project.home")``.
.. tip:: If using OIDC for login via Vue and Django for the same development
project, then your ``LOGIN_REDIRECT_URL``s will be for the Vue app.
To get the Django login to go back to a Django URL, then I added a
``next`` parameter to the Django URL e.g.
``Login``
Add the following to ``settings/local.py``::
KB_TEST_EMAIL_FOR_OIDC = get_env_variable("KB_TEST_EMAIL_FOR_OIDC")
KB_TEST_EMAIL_USERNAME = get_env_variable("KB_TEST_EMAIL_USERNAME")
OPEN_ID_CONNECT_EMBER_REDIRECT_URI = "http://localhost:4200"
.. note:: The ``KB_TEST_EMAIL_`` settings are used by the
``demo_data_login_oidc`` management command.
.. note:: The ``OPEN_ID_CONNECT_EMBER_REDIRECT_URI`` needs to match your
``ember`` apps host name and port.
Add the following to ``settings/production.py``::
OPEN_ID_CONNECT_EMBER_REDIRECT_URI = get_env_variable("HOST_NAME")
.. note:: Using ``HOST_NAME`` will work as long as the Django site is on the
same host as the Ember site.
Add the following to ``.gitlab.ci``::
test:
script:
- export KB_TEST_EMAIL_FOR_OIDC="patrick@kbsoftware.co.uk"
- export KB_TEST_EMAIL_USERNAME="admin"
- export OIDC_CREATE_USER=False
- export OIDC_OP_AUTHORIZATION_ENDPOINT="http://localhost:1235/"
- export OIDC_OP_JWKS_ENDPOINT="http://localhost:1236/"
- export OIDC_OP_TOKEN_ENDPOINT="http://localhost:1237/"
- export OIDC_OP_USER_ENDPOINT="http://localhost:1238/"
- export OIDC_RP_CLIENT_ID="my-oidc-client-id"
- export OIDC_RP_CLIENT_SECRET="my-oidc-client-secret"
- export OIDC_RP_SIGN_ALGO="RS256"
- export OIDC_USE_NONCE=False
- export USE_OPENID_CONNECT=True
Add the following to your environment e.g. ``.env``::
USE_OPENID_CONNECT="True"
KB_TEST_EMAIL_USERNAME="admin"
Set-up your ``.private`` file using the information from
:doc:`sys-microsoft-azure`.
How does it work?
-----------------
.. tip:: For Ember ``oidc`` auth, see :doc:`dev-ember-auth`.
Start by clicking on the link:
.. image:: ./misc/2022-02-07-oidc-login-verify-its-you.png
To browse to ``/oidc/authenticate/``::
# site-packages/mozilla_django_oidc/
url(r'^authenticate/$',
OIDCAuthenticationRequestView.as_view(),
name='oidc_authentication_init'),
``OIDCAuthenticationRequestView`` will return a redirect URL to the third
party authentication provider using the ``OIDC_OP_AUTHORIZATION_ENDPOINT``
e.g.
https://login.microsoftonline.com/3ab1a2b2/oauth2/v2.0/authorize?response_type=code&scope=openid+email&client_id=386e3c7&redirect_uri=https://www.hatherleigh.info/oidc/callback/&state=YdIhnM3
The browser will make an HTTP request to the URL of the third party
authentication provider, passing a ``client_id`` and ``redirect_uri``
(the ``redirect_uri`` includes a randomly generated ``state``)
The third part authentication provider will authenticate the user before
calling the ``redirect_uri``::
# site-packages/mozilla_django_oidc/
url(r'^callback/$',
OIDCAuthenticationCallbackView.as_view(),
name='oidc_authentication_callback'),
The ``OIDCAuthenticationCallbackView`` will use our
``login.service.KBSoftwareOIDCAuthenticationBackend`` to log the user in
before returning a redirect to the ``LOGIN_REDIRECT_URL``.
The browser will redirect to the ``LOGIN_REDIRECT_URL``.
.. tip:: For Ember ``oidc`` auth, see :doc:`dev-ember-auth`.
Diagostics
----------
.. warning:: Ask the user which app they are trying to log into.
This will make sure we check the correct log files!!
Check the log file e.g::
[07/Feb/2023 17:11:04] DEBUG [mozilla_django_oidc.auth:340]
Login failed: No user with email patrick.kimber@hatherleigh.onmicrosoft.com found, and OIDC_CREATE_USER is False
OIDC Login will only work if one of your users has a matching email address.
If you have trouble logging a user in, then you can display the email address
by adding a ``print`` statement to::
venv/lib/python3.10/site-packages/mozilla_django_oidc/auth.py
e.g::
def get_or_create_user(self, access_token, id_token, payload):
user_info = self.get_userinfo(access_token, id_token, payload)
email = user_info.get("email")
print(email)
Check the user ``is_active``:
If the user is no longer active, then this may be the reason for a login fail.
Testing / Debug
---------------
If your ``AUTHENTICATION_BACKENDS`` are set to use OIDC, then tests using the
``ModelBackend`` for authentication will fail. To fix this, add the following
to the beginning of each test::
@pytest.mark.django_db
def test_create(client, settings):
settings.AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend"
]
.. note:: This fix will also allow our ``perm_check`` fixture to work.
When *testing* (**on your laptop**) you can use the ``demo_data_login_oidc``
management command to update the email address for the ``staff`` user:
1. Use ``.env.fish`` to set the ``KB_TEST_EMAIL_FOR_OIDC``
and ``KB_TEST_EMAIL_USERNAME`` environment variables.
This will need to match the email address of the user in the Azure portal.
2. Run the ``django-admin.py demo_data_login_oidc`` management command to update
the email address for the ``KB_TEST_EMAIL_USERNAME`` user to match the
``KB_TEST_EMAIL_FOR_OIDC`` environment variable:
Deployment
----------
Using the information from your ``.private`` file
(see above and :doc:`sys-microsoft-azure` for more information),
update the Salt ``pillar`` file for your site e.g:
.. code-block:: yaml
sites:
my_site:
package: bpm
profile: django
env:
use_openid_connect: True
oidc_op_authorization_endpoint: "https://login.microsoftonline.com/fcee251/oauth2/v2.0/authorize"
oidc_op_jwks_endpoint: "https://login.microsoftonline.com/common/discovery/v2.0/keys"
oidc_op_token_endpoint: "https://login.microsoftonline.com/fcee251/oauth2/v2.0/token"
oidc_rp_client_id: "36ad9"
oidc_rp_client_secret: "aead6"
oidc_rp_sign_algo: "RS256"
oidc_use_nonce: False
Password
========
Brute Force
-----------
`django-axes`_ will lock out repeated attempts from the same IP address.
To configure:
Add ``django-axes`` to ``requirements/base.txt``
Add ``axes`` to ``THIRD_PARTY_APPS`` in ``settings/base.py``::
THIRD_PARTY_APPS = (
'axes',
Configure in ``settings/base.py``::
from datetime import timedelta
AXES_COOLOFF_TIME = timedelta(minutes=15)
AXES_FAILURE_LIMIT = 5
AXES_LOCKOUT_TEMPLATE = 'login/axes_lockout_template.html',
AXES_PASSWORD_FORM_FIELD = 'password1'
.. note:: ``AXES_COOLOFF_TIME`` configures a 15 minute cooling off period before
the next login attempt can be made.
.. note:: The ``axes_lockout_template.html`` is in the ``login`` app.
To administer Axes, the ``admin`` app has a list of *Access attempts* and
*Access logs* at ``/admin/axes/``.
Reset all lockouts and access records::
django-admin.py axes_reset
Clear lockout/records for an ip address::
django-admin.py axes_reset ip
Reset
-----
When a user (or non-user) attemps to reset their password, the ``Notify`` users
are emailed. For logic, see:
https://gitlab.com/kb/login/blob/master/login/forms.py
To use Google ReCaptcha on the password reset form,
use the ``RECAPTCHA_PRIVATE_KEY`` settings.
For details, see :doc:`dev-captcha`.
I think this is a potential risk for a DOS attack. If we get a DOS attack
then we could use the ``PasswordResetAudit`` model to limit the number of
notification emails we send:
https://gitlab.com/kb/login/blob/master/login/models.py
To see an audit of password reset attempts, browse to
``/accounts/password/reset/audit/report/``
Validation
----------
.. tip:: Unit tests for this feature are in the ``login`` app.
To add password validators, just add them to this list in ``settings/base.py``.
We have these validators working on a live project::
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 8,
}
},
]
For documentation, see `Enabling password validation`_...
Register
========
If you want to allow users to register on your site, add the following to
``urls.py``::
from login.views import RegisterCreateView
url(regex=r'^accounts/register/$',
view=RegisterCreateView.as_view(),
name='register'
),
Staff
=====
If you want a member of staff to be able to update user names and passwords for
other users:
Create a couple of views in ``views.py``. This will allow you to set the
success URL for your project::
from login.views import (
UpdateUserNameView,
UpdateUserPasswordView,
)
class MyUpdateUserNameView(UpdateUserNameView):
def get_success_url(self):
return reverse('example.test')
class MyUpdateUserPasswordView(UpdateUserPasswordView):
def get_success_url(self):
return reverse('example.test')
Add the views to ``urls.py``::
from .views import (
MyUpdateUserNameView,
MyUpdateUserPasswordView,
)
url(regex=r'^accounts/user/(?P\d+)/username/$',
view=MyUpdateUserNameView.as_view(),
name='update_user_name',
),
url(regex=r'^accounts/user/(?P\d+)/password/$',
view=MyUpdateUserPasswordView.as_view(),
name='update_user_password',
),
You can use these views in your project as follows:
.. code-block:: html
{{ u.username }}
|
********
|
Templates
=========
If you want to override the ``login`` templates with your own versions...
Update ``settings/base.py`` to search the ``project/templates/`` folder before
the app folders::
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "project" / "templates"],
"APP_DIRS": True,
"OPTIONS": {
e.g::
project/templates/login/password_reset.html
project/templates/login/login.html
For more details, see
https://docs.djangoproject.com/en/3.2/howto/overriding-templates/
.. _`django-axes`: https://django-axes.readthedocs.io/en/latest/configuration.html
.. _`Enabling password validation`: https://docs.djangoproject.com/en/2.0/topics/auth/passwords/#enabling-password-validation
.. _`README.rst`: https://github.com/pkimber/kb-mozilla-django-oidc/blob/main/README.rst#kb-software-ltd