Base 5–3rd party authentication in Django
Remember the frustration of creating new accounts for every website? I certainly do. Some of us still have two or three passwords cycling between sites. When “OpenID” kicked in, I was over the moon. It took a while for Google to adopt it, and Single Sign-On revolutionized how we use credentials everywhere. Let’s build this feature into our boilerplate project.
We continue from the last article, Base 4 — Deploy Django on DigitalOcean. The code for this one is in the part-4-deploy-on-digitalocean branch.
For this implementation, we’ll use django-allauth, a robust authentication app for Django. It’s a popular choice due to its comprehensive support for over 100 authentication methods. Given its widespread use and familiarity among users, we’ll set up Google Sign-On as our primary third-party authentication option. We can add more in a parallel article. This approach will give us a solid authentication system that caters to different user preferences while keeping our codebase clean and maintainable.
Upgrading dependencies
It’s been a while since we installed the dependencies, so it would be good to check for updates. poetry
has a command poetry show --outdated
, unfortunately, it displays all installed outdated dependencies, including the ones not mentioned in the pyproject.toml
file. The shortest way to filter them is via this command:
djpoe ➜ poetry show --outdated | grep --file=<(poetry show --tree | grep '^\w' | sed 's/^\([^ ]*\).*/^\1/')
poetry show --outdated
lists all outdated dependencies in your Poetry project
poetry show --tree
shows the dependency tree of your project in a format:
django 5.1.1 A high-level Python web framework that encourages rapid development and clean, pragmatic design.
├── asgiref >=3.8.1,<4
├── sqlparse >=0.3.1
└── tzdata *
grep ‘^\w’
filters the output of the above command and returns only the lines starting with a word. Here it would be the line starting with django
sed ‘s/^\([^ ]*\).*/^\1/’)
filters the line only to keep the package name. Here — django
grep --file
takes the list of outdated packages and filters it with a list of the first-choice ones written in pyproject.toml
We will add entire command to the poethepoet
tasks for the future usage:
# pyproject.toml
# ...
[tool.poe.tasks.find_updates]
shell = "poetry show --outdated | grep --file=<(poetry show --tree | grep '^\\w' | sed 's/^\\([^ ]*\\).*/^\\1/')"
interpreter = "bash"
djpoe ➜ poe find_updates
Poe => poetry show --outdated | grep --file=<(poetry show --tree | grep '^\w' | sed 's/^\([^ ]*\).*/^\1/')
django 5.1 5.1.1 A high-level Python web framework that encourages ...
pytest-django 4.8.0 4.9.0 A Django plugin for pytest.
ruff 0.6.3 0.6.4 An extremely fast Python linter and code formatter...
Checking pyproject.toml
config for these packages:
[tool.poetry.dependencies]
django = "^5.1"
[tool.poetry.group.dev.dependencies]
pytest-django = "^4.8.0"
ruff = "^0.6"
The version defined in pypoetry
is ^5.1
for django
and ^0.6
for ruff
. We can upgrade them with plain install.
djpoe ➜ poetry install
However, we need to update the pytest-django
version to ^4
as we don’t need to be so precise.
djpoe ➜ poetry add --group dev pytest-django@^4
Since our project’s dependencies are fresh, we can proceed to the actual feature implementation.
Using django-allauth with regular accounts
django-allauth’s documentation provides a lot of configuration settings. It’s worth the read to understand how flexible this app is. I like some of them to be set to a different value than the default one.
Using email instead of a username
ACCOUNT_AUTHENTICATION_METHOD="email" # (default: "username")
ACCOUNT_EMAIL_REQUIRED=True # (default: False)
I don’t remember the last time I signed up using my username. Using email seems natural, and it’s unique to the world.
Email confirmation
ACCOUNT_EMAIL_VERIFICATION="mandatory". # (default: "optional")
Let’s make it mandatory to verify the email.
ACCOUNT_CONFIRM_EMAIL_ON_GET=True # (default: False)
Determines whether or not a GET request automatically confirms an email address. GET is not designed to modify the server state, though it is commonly used for email confirmation. To avoid requiring user interaction, consider using POST via Javascript in your email confirmation template as an alternative to setting this to True.
Notifying users about password changes
ACCOUNT_EMAIL_NOTIFICATIONS=True # (default: False)
It feels safer to receive such an email.
Lowercase usernames
ACCOUNT_PRESERVE_USERNAME_CASING=False # (default=True)
Although we no longer use the usernames to log in, the system will use them to print out on the profile page and other places. I want to keep it consistent and lowercase.
Configuration
We will add the config constants to the settings.py
file, using the above values as the default ones but leaving them to be configured with the environment variables.
# djpoe/djpoe/settings.py
# ...
env = environ.FileAwareEnv(
DEBUG=(bool, False),
DATABASE_URL=(str, "sqlite"),
SECRET_KEY=(str),
# django-allauth regular account settings
ACCOUNT_AUTHENTICATION_METHOD=(str, "email"),
ACCOUNT_EMAIL_REQUIRED=(bool, True),
ACCOUNT_CONFIRM_EMAIL_ON_GET=(bool, False),
ACCOUNT_EMAIL_VERIFICATION=(str, "mandatory"),
ACCOUNT_EMAIL_NOTIFICATIONS=(bool, True),
ACCOUNT_PRESERVE_USERNAME_CASING=(bool, False),
)
# ...
# django-allauth regular account settings
ACCOUNT_AUTHENTICATION_METHOD = env("ACCOUNT_AUTHENTICATION_METHOD")
ACCOUNT_EMAIL_REQUIRED = env("ACCOUNT_EMAIL_REQUIRED")
ACCOUNT_CONFIRM_EMAIL_ON_GET = env("ACCOUNT_CONFIRM_EMAIL_ON_GET")
ACCOUNT_EMAIL_VERIFICATION = env("ACCOUNT_EMAIL_VERIFICATION")
ACCOUNT_EMAIL_NOTIFICATIONS = env("ACCOUNT_EMAIL_NOTIFICATIONS")
ACCOUNT_PRESERVE_USERNAME_CASING = env("ACCOUNT_PRESERVE_USERNAME_CASING")
Installing django-allauth
djpoe ➜ poetry add django-allauth
• Installing django-allauth (64.2.1)
djpoe ➜ poetry export --without-hashes --format=requirements.txt > requirements.txt
Connecting to the project
Let’s implement some instructions from allauth’s Quickstart. In our case, before adding the social accounts, we should add the following:
# djpoe/djpoe/settings.py
# ...
INSTALLED_APPS = [
...
# The following apps are required:
'django.contrib.auth',
'django.contrib.messages',
'allauth',
'allauth.account',
# ...
MIDDLEWARE = (
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
# Add the account middleware:
"allauth.account.middleware.AccountMiddleware",
)
# ...
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
"django.contrib.auth.backends.ModelBackend",
# `allauth` specific authentication methods, such as login by email
"allauth.account.auth_backends.AuthenticationBackend",
]
# djpoe/djpoe/urls.py
# ...
urlpatterns = [
...
path('accounts/', include('allauth.urls')),
...
]
After saving these files, we have to migrate the database.
djpoe ➜ poe migrate
Poe => python ./djpoe/manage.py migrate
Operations to perform:
Apply all migrations: account, admin, auth, contenttypes, sessions
Running migrations:
Applying account.0001_initial... OK
Applying account.0002_email_max_length... OK
Applying account.0003_alter_emailaddress_create_unique_verified_email... OK
Applying account.0004_alter_emailaddress_drop_unique_email... OK
Applying account.0005_emailaddress_idx_upper_email... OK
Applying account.0006_emailaddress_lower... OK
Applying account.0007_emailaddress_idx_email... OK
Applying account.0008_emailaddress_unique_primary_email_fixup... OK
Applying account.0009_emailaddress_unique_primary_email... OK
djpoe ➜ poe manage dbshell
Poe => python ./djpoe/manage.py dbshell
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> .tables
account_emailaddress auth_user_groups
account_emailconfirmation auth_user_user_permissions
auth_group django_admin_log
auth_group_permissions django_content_type
auth_permission django_migrations
auth_user django_session
We will now launch the website…
djpoe ➜ poe dev
… and navigate to the login screen at http://127.0.0.1:8001/accounts/login/
We receive an error during the signup process:
...
File "/opt/homebrew/Cellar/python@3.12/3.12.4/Frameworks/Python.framework/Versions/3.12/lib/python3.12/socket.py", line 853, in create_connection
...
We want to send a confirmation email from the localhost
and it’s not yet allowed. Let’s switch this requirement off for now, and we can always add the sending email feature in the future.
Add the following to the .env
file.
# .env
DEBUG=True
SECRET_KEY=django-insecure-=@ek@$wwm7-#ee
ACCOUNT_EMAIL_VERIFICATION=none
ACCOUNT_EMAIL_NOTIFICATIONS=False
Now we can log in, but the browser redirects to http://127.0.0.1:8001/accounts/profile/, which gives us a 404
page. Several settings configure the redirect:
LOGIN_REDIRECT_URL
(default:”/accounts/profile/”
) The URL where requests are redirected for login.LOGOUT_REDIRECT_URL
(default:LOGIN_REDIRECT_URL
) The URL where requests are redirected for logout.ACCOUNT_AUTHENTICATED_LOGIN_REDIRECTS
(default:True
) Should the browser redirect toLOGIN_REDIRECT_URL
after a successful login action?ACCOUNT_SIGNUP_REDIRECT_URL
(default:LOGIN_REDIRECT_URL
)ACCOUNT_LOGOUT_REDIRECT_URL
(default:LOGOUT_REDIRECT_URL or “/”
)
The simplest way to avoid the 404
page is to override the LOGIN_REDIRECT_URL
setting and set the redirect to ”/”
:
# djpoe/djpoe/settings.py
# ...
env = environ.FileAwareEnv(
# ...
LOGIN_REDIRECT_URL=(str, "/accounts/profile/"),
)
# ...
LOGIN_REDIRECT_URL = env("LOGIN_REDIRECT_URL")
# .env
# ...
LOGIN_REDIRECT_URL = env("LOGIN_REDIRECT_URL")
We can log out by navigating to http://127.0.0.1:8001/accounts/logout/ and click on the Sign Out
button.
To test if the complete cycle is working, we can start with a clean database:
djpoe ➜ rm djpoe/sqlite
djpoe ➜ poe manage migrate
Poe => python ./djpoe/manage.py migrate
Operations to perform:
Apply all migrations: account, admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
...
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
djpoe ➜ poe dev
After filling in the Sign-up information, I was redirected to http://127.0.0.1:8001/. Since I’m logged in, the browser does the same for http://127.0.0.1:8001/accounts/login. Again, logging out on http://127.0.0.1:8001/accounts/logout.
We can check what happened in the database.
djpoe ➜ poe manage dbshell
Poe => python ./djpoe/manage.py dbshell
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> .mode column
sqlite> .headers on
sqlite> select * from account_emailaddress;
id verified primary user_id email
-- -------- ------- ------- -----------------
1 0 1 1 email@example.com
sqlite> select * from auth_user;
id password last_login is_superuser username last_name email is_staff is_active date_joined first_name
-- ----------------- ---------------- ------------ -------- --------- ----------------- -------- --------- -------------------------- ----------
1 pbkdf2_sha256$*** 2024-09-09 04:44 0 zalun email@example.com 0 1 2024-09-08 17:40:41.113042
We’ve got two new entries. One in account_emailaddress
and another in auth_user
.
We’ve configured the django-allauth
for regular accounts. Using the location bar to navigate the login and logout pages is inconvenient. Let’s create a trivial logged-in user indicator.
Accounts pages navigation
We need a link to the login page when the user is logged out and to the log-out page for an authenticated user. We will also print the current user’s username.
Our homepage is created with just the View in the djpoe/helloworld/views.py
file. We need to add a template. Let’s create a static one first. In the djpoe/helloworld/templates/helloworld/index.html
:
{# djpoe/helloworld/templates/helloworld/index.html #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello, World!</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>This is a simple Django template from the helloworld app.</p>
</body>
</html>
We need to connect it to the View.
# djpoe/helloworld/views.py
"""Views for the helloworld app."""
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
def index(request: HttpRequest) -> HttpResponse:
"""Return a simple greeting."""
return render(request, "helloworld/index.html")
We’ve got an excellent Hello World page.
Unfortunately, it breaks our tests:
We need to update it. Let’s check whether the “Hello, World!”
string exists in the rendered context.
# tests/test_helloworld.py
import pytest
from django.test import Client
@pytest.mark.django_db
def test_helloworld_page(client: Client):
response = client.get("/")
assert response.status_code == 200
assert "Hello, World!" in response.content.decode()
As our tests are working, we can add the authentication links.
{# djpoe/helloworld/templates/helloworld/index.html #}
{# ... #}
<h1>Hello, World!</h1>
{% if user.is_authenticated %}
<p>Welcome, {{ user.username }}!</p>
<a href="{% url 'account_logout' %}">Logout</a>
{% else %}
<p>Welcome, guest!</p>
<a href="{% url 'account_login' %}">Login</a>
<a href="{% url 'account_signup' %}">Sign Up</a>
{% endif %}
{# ... #}
We’ll test this behavior. For this, we will authenticate the user from the fixture with the client.login()
feature.
# tests/test_helloworld.py
import pytest
from django.contrib.auth import get_user_model
from django.test import Client
User = get_user_model()
@pytest.fixture(name="user")
def user_fixture():
return User.objects.create_user(username="testuser", password="12345")
# ... (Hello, World! test)
@pytest.mark.django_db
def test_homepage_unauthenticated(client: Client):
response = client.get("/")
assert response.status_code == 200
assert "Welcome, guest!" in response.content.decode()
assert "Login" in response.content.decode()
assert "Sign Up" in response.content.decode()
assert "Logout" not in response.content.decode()
@pytest.mark.django_db
def test_homepage_authenticated(client: Client, user):
client.login(username="testuser", password="12345")
response = client.get("/")
assert response.status_code == 200
assert f"Welcome, {user.username}!" in response.content.decode()
assert "Logout" in response.content.decode()
assert "Login" not in response.content.decode()
assert "Sign Up" not in response.content.decode()
Styling
The site is working, but it’s just a little dull. It’s intentional:
The templates that are offered out of the box are intentionally plain and without any styling. We do not want to pick a side in the multitudes of frontend styling options out there, and the look and feel typically should be adjusted to match the branding of your project.
We will copy the templates as suggested and make minimal changes to avoid going deep into styling. In the end, this is going to be just a boilerplate for future projects. I assume each one will look a little different.
I asked DuckDuckGo, and the first link was Skeleton. Adding it to the homepage is trivial:
{# djpoe/helloworld/templates/helloworld/index.html #}
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello, World!</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
The result is acceptable, and I will not waste more time.
We need to add this file to all of the django-allauth
pages. Let’s overwrite the base layout. It can be done with a script. I copied all the Allauth templates, but they are too many files. The allauth/loayout/base.html
is extended in all templates. It will be enough.
Finding the base path of the package is some fun in itself:
djpoe ➜ python
>>> import allauth
>>> print(allauth.__path__)
['/Users/piotrzalewa/Projects/Medium/djpoe/.venv/lib/python3.12/site-packages/allauth']
We can now dynamically find the directory of allauth
package and copy the file we want.
djpoe ➜ mkdir -p djpoe/templates/allauth/layouts
djpoe ➜ cp $(python -c "import allauth; print(allauth.__path__[0])")/templates/allauth/layouts/base.html djpoe/templates/allautallauth/layouts
We want Django to use the templates from the templates directory.
# djpoe/djpoe/settings.py
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
BASE_DIR / "templates",
],
# ...
},
]
Now, we will add the Skeleton CSS to the copied base.html
.
{# djpoe/templates/allauth/layout/base.html" #}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
{# ... #}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
{# ... #}
It’s slightly less eye-sore than before. And we will leave it as it is. In the actual project, I’d probably use something like django-allauth-ui.
Google Single Sign-On
Great! Let’s dive into the exciting world of social login. Now that we’ve covered the basics of django-allauth
configuration, it’s time to supercharge our authentication system with OAuth magic!
Install dependencies
django-allauth
app needs some dependencies installed for it to be integrated to Google.
djpoe ➜ poetry add "django-allauth[socialaccount,google]"
Using version ^64.2.1 for django-allauth
Updating dependencies
Resolving dependencies... (0.4s)
Package operations: 11 installs, 0 updates, 0 removals
- Installing pycparser (2.22)
- Installing certifi (2024.8.30)
- Installing cffi (1.17.1)
- Installing charset-normalizer (3.3.2)
- Installing idna (3.9)
- Installing urllib3 (2.2.3)
- Installing cryptography (43.0.1)
- Installing oauthlib (3.2.2)
- Installing requests (2.32.3)
- Installing pyjwt (2.9.0)
- Installing requests-oauthlib (2.0.0)
djpoe ➜ poetry export --without-hashes --format=requirements.txt > requirements.txt
djpoe ➜ poe migrate
Operations to perform:
Apply all migrations: account, admin, auth, contenttypes, sessions, socialaccount
Running migrations:
Applying socialaccount.0001_initial... OK
Applying socialaccount.0002_token_max_lengths... OK
Applying socialaccount.0003_extra_data_default_dict... OK
Applying socialaccount.0004_app_provider_id_settings... OK
Applying socialaccount.0005_socialtoken_nullable_app... OK
Applying socialaccount.0006_alter_socialaccount_extra_data... OK
It’s good to run the test here to check if everything is OK. Initially, I forgot to add the socialaccount
to the poetry add
command, and the failing test redirected me back to the documentation after complaining:
...
File "/Users/piotrzalewa/Projects/Medium/djpoe/.venv/lib/python3.12/site-packages/allauth/socialaccount/providers/google/provider.py", line 1, in <module>
import requests
ModuleNotFoundError: No module named 'requests'
Configuration
Again, the documentation provides us with a list of constants to configure the behavior:
Here is my initial setting:
# djpoe/djpoe/settings.py
# ...
env = environ.FileAwareEnv(
# ...
# django-allauth social account settings
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT=(bool, True),
SOCIALACCOUNT_ONLY=(bool, False),
GOOGLE_AUTH_CLIENT_ID=(str, ""),
GOOGLE_AUTH_CLIENT_SECRET=(str, ""),
)
# ...
# Application definition
INSTALLED_APPS = [
# ...
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.google",
"helloworld",
]
# ...
# django-allauth social account settings
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = env("SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT")
SOCIALACCOUNT_ONLY = env("SOCIALACCOUNT_ONLY")
SOCIALACCOUNT_PROVIDERS = {
"google": {
"APP": {
"client_id": env("GOOGLE_AUTH_CLIENT_ID"),
"secret": env("GOOGLE_AUTH_CLIENT_SECRET"),
},
"scope": ["profile", "email"],
"EMAIL_AUTHENTICATION": True,
"AUTH_PARAMS": {
"access_type": "online",
},
"OAUTH_PKCE_ENABLED": True,
"FETCH_USERINFO": True,
}
}
Let’s untangle the not-so-obvious ones.
SOCIALACCOUNT_PROVIDERS[“EMAIL_AUTHENTICATION”] = TRUE
We can configure the project to authenticate a user who already signed in as a regular account (email/password) using the social account. We can do this for all possible social accounts or each one separately. The safer option is to switch it on for each provider. I think it’s OK to allow this for Google.SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT
(default:False
) is related to theSOCIALACCOUNT_PROVIDERS[“EMAIL_AUTHENTICATION”] = TRUE
It answers the question: Should the regular account be added to social accounts after logging in with such a thing? I think it’s sane, but I leave it open to be configured with the environment variable.SOCIALACCOUNT_ONLY
(default:False
) Regular account login is switched off if set toTrue
. I’m not changing the default, but I want it configured with the environment variable.SOCIALACCOUNT_PROVIDERS
The standard workflow sets the Google account configuration using the Django admin panel. I prefer the config to be written in code.SOCIALACCOUNT_PROVIDERS[“google”][“scope”] = ["profile", "email"]
We need access to the profile to retrieve essential details like the user’s name and profile picture. We also want email to be stored with the user’s data.OAUTH_PKCE_ENABLED
It’s a security feature that enables the use of Proof Key for Code Exchange (PKCE) in the OAuth 2.0 flow. PKCE adds an extra layer of security for OAuth authorization, which is particularly beneficial for mobile and single-page applications because it prevents specific authorization code interception attacks.SOCIALACCOUNT_PROVIDERS["FETCH_USERINFO"] = True
user info endpoint will be used to populate theavatar_url
for those users who have a private style ofavatar_url
.
Configuring Google Cloud side
Log in and create a new project. You’ll be notified when it’s created. Please select it and navigate to the Credentials page.
There is a warning icon about configuring the consent screen. Start with that. Depending on the project requirements, you might specify the internal
(you need to be a Google Workspace user) or external
option. Confirm the form and edit the App information. We only need to add the app name, user support email, and developer’s contact information for now. After confirming with SAVE AND CONTINUE
button you will be asked to define the scopes. Choose “email” and “profile” and continue.
The publishing status is set to “Testing.” Google only allows test users to access the app. You need to provide a few. Again, continue, review the settings, and return to the dashboard.
Now, we need to create credentials. We’re looking into the OAuth 2.0 Client IDs
section. It will be empty if no connection to Google Sign-on has been made.
Click on CREATE CREDENTIALS
and choose the OAuth client ID.
Fill in the details. I’ve chosen to create a separate one for localhost.
After creating the credentials, Google will display the Client ID and secret in the modal window. Copy or download and add them to your local .env
file.
# djpoe/.env
DEBUG=True
SECRET_KEY=django-insecure-=abcde
GOOGLE_AUTH_CLIENT_ID=12345678-abcde.apps.googleusercontent.com
GOOGLE_AUTH_CLIENT_SECRET=GOCSPX-ABCdefGHIjkl
Start the dev server (poe dev
) and navigate to http://127.0.0.1:8001/accounts/login. There is a new section for third-party logins.
This link will move you to http://127.0.0.1:8001/accounts/google/login/?process=login and display the Google login page.
Continue will bring you to the standard Google Sign-in page. Everything is working as expected. If the LOGIN_REDIRECT_URL
is set to /
, the user is redirected to the homepage.
The database has the details retrieved from Google's profile. Here, it is translated to JSON.
{
"id": 1,
"provider": "google",
"uid": "123456789012345678",
"last_login": "2024-09-15 19:16:17.835128",
"date_joined": "2024-09-15 18:53:25.745574",
"user_id": 2,
"extra_data": {
"iss": "https://accounts.google.com",
"azp": "123456789-abcdef.apps.googleusercontent.com",
"aud": "123456789-abcdef.apps.googleusercontent.com",
"sub": "98765432123456789",
"email": "example@gmail.com",
"email_verified": true,
"at_hash": "aBCDEf_vabcdeFG",
"name": "Piotr \"zalun\" Zalewa",
"picture": "https://lh3.googleusercontent.com/a/ACg8ocL7QdEUBRFS9CcUoMZPUavy7VOAjlquJGIRfrUoIHkeVhhUds_T8g=s96-c",
"given_name": "Piotr",
"family_name": "Zalewa",
"iat": 1726427777,
"exp": 1726431377
}
}
django-allauth
created the entry in the auth_user
as well. The username
is the lowercased given_name
, in this case “piotr”
. If another Piotr would sign up using a social account, a number will be attached. In my case, it was “piotr7”
.
Tests
How can we test it? The signal mechanism has already been tested in django-allauth
. We might test the pages in a similar way we did before.
A small refactoring… We do not need the user
fixture. We used it only to get username
. I’ll create the username
and password
fixtures in tests/conftest.py
so these can be used in all tests.
# tests/conftest.py
import pytest
@pytest.fixture(name="username")
def username_fixture():
return "testuser"
@pytest.fixture(name="password")
def password_fixture():
return "12345"
We can now simplify the test_helloworld.py
# tests/test_helloworld.py
# ...
@pytest.mark.django_db
def test_homepage_authenticated(client: Client, username: str, password: str):
client.login(username=username, password=password)
response = client.get("/")
assert response.status_code == 200
assert f"Welcome, {username}!" in response.content.decode()
assert "Logout" in response.content.decode()
assert "Login" not in response.content.decode()
assert "Sign Up" not in response.content.decode()
And write the test to see if the link to Google appears in the login page.
# tests/test_accounts_pages.py
import pytest
from django.test import Client
from django.urls import reverse
@pytest.mark.django_db
def test_login_page_not_authenticated(client: Client):
response = client.get(reverse("account_login"))
assert response.status_code == 200
assert "third-party" in response.content.decode()
assert '<a title="Google" href="/accounts/google/login/?process=login">Google</a>' in response.content.decode()
Connect the deployed project
Repeat the process of creating credentials. This time, use the https://djpoe-app-kbmxj.ondigitalocean.app
instead of http://127.0.0.1:8001
. Apply credentials to the App’s Environment Variables setting. Check in the Encrypt option.
We will rebase the main
branch on part-5-auth
one, and push to the repository.
djpoe ➜ git checkout main
djpoe ➜ git rebase part-5-auth
Successfully rebased and updated refs/heads/main.
djpoe ➜ git push -f
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:zalun/djpoe.git
0f026da..18dd529 main -> main
After the deployment, navigate to the Console and migrate the database.
> python djpoe/manage.py migrate
All is done; we’re ready to log in using the Google Oauth!
Deployment
After a successful build, go into the app console and run:
$ python ./djpoe/manage.py migrate
$ python ./djpoe/manage.py loaddata homepage
Unfortunately, Wagtail is loaded without images. We need to fix this bit.
Final words
There are plenty of other social accounts to connect using django-allauth
. If you’d like to see others, please leave a comment.
Feel free to follow me here. Do not hesitate to clap, either. Funny, I know, but I enjoy them.
You will find the code used in the article in the GitHub djpoe
project in part-5-auth branch.
This is continued — Wagtail CMS for the content