login
Management Commands
create-user-system-generated
Create the SYSTEM_GENERATED
user:
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:
django-admin.py list-of-users out.csv
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 Microsoft Azure Portal.
Add the following to requirements/base.txt
:
mozilla-django-oidc==
Tip
See 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 Microsoft Azure.
How does it work?
Tip
For Ember oidc
auth, see Ember Authentication.
Start by clicking on the link:
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.
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 Ember Authentication.
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:
Use
.env.fish
to set theKB_TEST_EMAIL_FOR_OIDC
andKB_TEST_EMAIL_USERNAME
environment variables. This will need to match the email address of the user in the Azure portal.Run the
django-admin.py demo_data_login_oidc
management command to update the email address for theKB_TEST_EMAIL_USERNAME
user to match theKB_TEST_EMAIL_FOR_OIDC
environment variable:
Deployment
Using the information from your .private
file
(see above and Microsoft Azure for more information),
update the Salt pillar
file for your site e.g:
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 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:
<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/