login ***** .. highlight:: python https://gitlab.com/kb/login - :doc:`dev-ember-auth` - :doc:`sys-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-azure` Portal. Add the following to ``requirements/base.txt``:: mozilla-django-oidc== .. tip:: See :doc:`dev-requirements` for the current version... 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")``. 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.fish``:: set -x USE_OPENID_CONNECT "True" set -x KB_TEST_EMAIL_USERNAME "admin" Set-up your ``.private`` file using the information from :doc:`sys-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-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<pk>\d+)/username/$', view=MyUpdateUserNameView.as_view(), name='update_user_name', ), url(regex=r'^accounts/user/(?P<pk>\d+)/password/$', view=MyUpdateUserPasswordView.as_view(), name='update_user_password', ), You can use these views in your project as follows: .. code-block:: html <td> <a href="{% url 'update_user_name' u.pk %}"> <i class="fa fa-edit"></i> {{ u.username }} </a> </td> <td> <a href="{% url 'update_user_password' u.pk %}"> <i class="fa fa-edit"></i> ******** </a> </td> 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