Reusable 'Sign in with VisualEyes' passwordless photo-login for Django.
Project description
django-visualeyes
Reusable "Sign in with VisualEyes" — AQA's passwordless photo login — for any
Django project. Instead of copy-pasting the client, views and template glue into
each site, pip install django-visualeyes, add a few settings, and wire three
URLs.
Targets Django 4.2 LTS through 6.x, Python 3.10+. Zero runtime
dependencies beyond Django (the API client uses the stdlib urllib).
Install
pip install django-visualeyes
Configure
Add the app (and, if you use multiple auth backends, the VisualEyes backend):
INSTALLED_APPS = [
# ...
"visualeyes",
]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"visualeyes.backends.VisualEyesBackend", # records VE logins distinctly
]
Include the URLs (the app namespaces itself as visualeyes):
# project urls.py
urlpatterns = [
path("accounts/", include("visualeyes.urls")),
# ...
]
This exposes:
| URL | name | method |
|---|---|---|
accounts/ve/start |
visualeyes:visualeyes_start |
POST |
accounts/ve/callback |
visualeyes:visualeyes_callback |
GET |
Settings
All settings are prefixed VISUALEYES_:
| Setting | Required | Default | Purpose |
|---|---|---|---|
VISUALEYES_CLIENT_ID |
yes | — | API client id (sent as X-VE-Client-Id). |
VISUALEYES_CLIENT_SECRET |
yes | — | HMAC signing secret. Keep it out of source control. |
VISUALEYES_API_BASE |
no | "https://aqa.com" |
Base URL of the VisualEyes service. |
VISUALEYES_TIMEOUT |
no | 15 |
Per-request timeout, seconds. |
VISUALEYES_ATTEST_LOCAL_ACCOUNTS |
no | False |
True → ve_start vouches for the account (local_account: true) and forwards the typed name; False → forwards the account email un-attested. Generalizes the portals' VE_ALIAS_PROTOCOL. |
VISUALEYES_LOGIN_REDIRECT |
no | settings.LOGIN_REDIRECT_URL or "/" |
Where to land after a successful login (when no safe next). |
VISUALEYES_CALLBACK_URL_NAME |
no | "visualeyes_callback" |
URL name reversed to build the callback URL. |
VISUALEYES_SESSION_FLAG |
no | "via_ve" |
Session key set True after a VE login. |
A manage.py check warning (visualeyes.W001) fires if the required
credentials are unset.
Login template
Load the tag library and drop the button inside your existing login <form>
(the one with the username field and {% csrf_token %}). The button re-submits
that form — including the username — to ve_start via formaction +
formnovalidate, so the blank password field doesn't block it. No JavaScript.
{% load visualeyes %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
{{ form.username.label_tag }} {{ form.username }}
{{ form.password.label_tag }} {{ form.password }}
<button type="submit">Sign in</button>
{% visualeyes_button %}
{# custom username field id: {% visualeyes_button username_field_id="id_login" %} #}
</form>
The default assumes Django's id_username; pass username_field_id if your
form differs.
Showing errors in your login card
The views report failures through Django's messages framework
(messages.error), so render messages where your form errors already appear
-- inside your login card, not at the top of the page. Otherwise a VisualEyes
error lands wherever your base template renders messages (often top-left), away
from the password-error box.
If your base.html renders messages site-wide, make that block overridable and
suppress it on the login page:
{# base.html #}
{% block messages %}
{% for message in messages %}
<div class="flash flash-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endblock %}
{# registration/login.html #}
{% block messages %}{% endblock %} {# don't render them at the top here #}
{% block content %}
<div class="card">
{% for message in messages %}
<div class="flash flash-error">{{ message }}</div>
{% endfor %}
{{ form.non_field_errors }}
{# ... your login form, including {% visualeyes_button %} ... #}
</div>
{% endblock %}
Hiding "change password" for passwordless users
VisualEyes-only accounts have no usable password, and a session that authenticated via VisualEyes shouldn't offer a password change either. A template tag guards a link:
{% load visualeyes %}
{% visualeyes_can_change_password as can_change %}
{% if can_change %}
<a href="{% url 'password_change' %}">Change password</a>
{% endif %}
It returns user.has_usable_password and not request.session.via_ve.
Django 6.2+ additionally offers a URL-level guard:
PasswordChangeView.usable_password_url (and the accompanying
SetPasswordMixin support), which redirects users with no usable password away
from the change-password form. Prefer that at the view layer when you're on 6.2+;
the template tag remains the portable option for 4.2–6.1.
How it works
ve_start(POST) — validates the typed username/email against the localUsertable first (Q(username__iexact) | Q(email__iexact), active only). Unknown ⇒ error + redirect to login with no VisualEyes call (anti-enumeration). Otherwise it opens a challenge (attested or not perATTEST_LOCAL_ACCOUNTS) and redirects to the register or challenge URL the service returns, stashing a safenextin the session.- The user completes the photo challenge on VisualEyes, which redirects back to
ve_callback(GET) with a single-use result token in?result=. ve_callbackverifies the token. On pass it binds the echoedalias(an active local username) or else get-or-creates an email account with an unusable password, callslogin(..., backend="visualeyes.backends.VisualEyesBackend"), sets thevia_vesession flag, and redirects to the safenextorLOGIN_REDIRECT. A duress signal is logged, never surfaced.
Retry / safety notes
start_challengeretries once on connection-level failure — creating a challenge is safe to repeat.verifynever retries — the result token is single-use, so a retry could double-consume it. It fails closed.- HTTP error statuses (4xx/5xx) are never retried; they are returned to the
caller as
(status, body).
Development / tests
The suite mocks the HTTP layer — it never contacts the live VisualEyes service.
python -m venv .venv && . .venv/bin/activate
pip install -e .
python runtests.py # or: python runtests.py tests.test_views_start
License
GNU Lesser General Public License v3.0 or later (LGPL-3.0-or-later). See COPYING.LESSER and COPYING.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_visualeyes-0.1.1.tar.gz.
File metadata
- Download URL: django_visualeyes-0.1.1.tar.gz
- Upload date:
- Size: 33.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da6d495ee868134b9253822cb78b8c4f952e47ad6cb82d9db00640aea370c3bf
|
|
| MD5 |
e750d7f4e75b46352ca755c501dfe90b
|
|
| BLAKE2b-256 |
89a33d983c831e3d62ddc22d998c653f5290d5143e76f4618ed2a9660255e5bf
|
File details
Details for the file django_visualeyes-0.1.1-py3-none-any.whl.
File metadata
- Download URL: django_visualeyes-0.1.1-py3-none-any.whl
- Upload date:
- Size: 31.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
697849d1dbdb9b4e48912acdb509af8a7b18b5abc726dd8d3d3a6d781b8e363a
|
|
| MD5 |
bc9fac25d79cf36674f8103e35c060b1
|
|
| BLAKE2b-256 |
c47deabe464431af4a7ad1b64ce4eb439ac41767b7dfe19de3b130879c935619
|