Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,36 @@ jobs:
linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install virtualenv from poetry
uses: 20c/workflows/poetry@v1
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: install project
run: uv sync --extra dev
- name: Run linters
run: |
poetry run pre-commit run --all-files
uv run pre-commit run --all-files

test:
needs: linting
strategy:
fail-fast: false
matrix:
os: [ "ubuntu-latest", "macos-latest" ]
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
python-version: [ "3.10", "3.11", "3.12" ]
runs-on: ${{ matrix.os }}
steps:
- name: Check out repository
uses: actions/checkout@v2
- name: Install virtualenv from poetry
uses: 20c/workflows/poetry@v1
with:
python-version: ${{ matrix.python-version }}
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Install python
run: uv python install ${{ matrix.python-version }}
- name: install tox
run: uv sync --extra dev
- name: Run tests
run: poetry run tox
run: uv run tox
- name: Upload coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ build/
.tox
.vscode
coverage.xml
venv/
.venv/
36 changes: 12 additions & 24 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
repos:
- repo: local
hooks:
- id: system
name: isort
entry: poetry run isort .
language: system
pass_filenames: false
- repo: local
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.6
hooks:
- id: pyupgrade
name: pyupgrade
entry: poetry run pyupgrade --py37-plus
language: python
types: [python]
pass_filenames: true
# Run the linter
- id: ruff
args: [--fix]
# Run the formatter
- id: ruff-format
- repo: local
hooks:
- id: system
name: Black
entry: poetry run black .
- id: mypy
name: mypy
entry: uv run mypy
language: system
types: [python]
pass_filenames: false
- repo: local
hooks:
- id: system
name: flake8
entry: poetry run flake8 .
language: system
pass_filenames: false
args: [src/]
2,099 changes: 0 additions & 2,099 deletions poetry.lock

This file was deleted.

136 changes: 84 additions & 52 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,70 +1,102 @@
[tool.poetry]
[project]
name = "django-security-keys"
version = "1.1.0"
description = "Django webauthn security key integration"
readme = "README.md"
repository = "https://github.com/fullctl/django-security-keys"
authors = [ "20C <code@20c.com>",]
license = "Apache-2"
license = { text = "Apache-2.0" }
authors = [
{ name = "20C", email = "code@20c.com" }
]
classifiers = [
"Topic :: Software Development",
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Software Development",
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]

packages = [
{ include = "django_security_keys", from = "src" },
requires-python = ">=3.10"
dependencies = [
"webauthn>=1.0.0",
"django-two-factor-auth>=1.13.1",
"phonenumbers>=8.12.47",
]

[tool.poetry.dependencies]
python = "^3.8"
webauthn = "^1"
[project.optional-dependencies]
dev = [
# testing
"pytest>=6.0.1",
"pytest-django>=3.8.0",
"pytest-cov",
"pytest-pythonpath",
"tox>=3.24",
"tox-gh-actions>=2.9.1",

# linting - modernized to use ruff
"ruff>=0.1.0",
"mypy>=0.950",
"django-stubs>=5.0.0",
"pre-commit>=2.13",

# ctl
"ctl>=1.0.0",
"jinja2>=3.1.2",
"tmpl>=1.0.0",
"twine>=3.3.0",

# docs
"markdown-include>=0.5",
"mkdocs>=1.2.3",
"pymdgen>=1.0.0",
]

# requirements for 2FA
django-two-factor-auth = "^1.13.1"
phonenumbers = "^8.12.47"
[project.urls]
Repository = "https://github.com/fullctl/django-security-keys"

[tool.poetry.dev-dependencies]
# testing
pytest = ">=6.0.1"
pytest-django = ">=3.8.0"
pytest-cov = "*"
pytest-pythonpath = "*"
tox = ">=3.24"
tox-gh-actions = ">=2.9.1"
[project.entry-points."markdown.extensions"]
pymdgen = "pymdgen.md:Extension"

# linting
black = { version = ">=20", allow-prereleases = true }
isort = "^5.7.0"
flake8 = "^3.8.4"
mypy = ">=0.950"
pre-commit = "^2.13"
pyupgrade = "^2.19.4"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

# ctl
ctl = "^1"
jinja2 = "^3.1.2"
tmpl = "^1"
twine = "^3.3.0"
[tool.hatch.build.targets.wheel]
packages = ["src/django_security_keys"]

# docs
markdown-include = ">=0.5"
mkdocs = "^1.2.3"
pymdgen = "^1.0.0"
[tool.ruff]
target-version = "py310"
line-length = 88
extend-exclude = ["migrations"]

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long, handled by ruff format
]

[tool.poetry.plugins."markdown.extensions"]
pymdgen = "pymdgen.md:Extension"
[tool.ruff.lint.isort]
known-first-party = ["django_security_keys"]

[build-system]
requires = [ "poetry>=0.12",]
build-backend = "poetry.masonry.api"
[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.isort]
profile = "black"
multi_line_output = 3
[tool.mypy]
python_version = "3.10"
ignore_missing_imports = true # Third-party packages without stubs
disable_error_code = [
"union-attr", # Django user types (User | SimpleLazyObject | AnonymousUser)
"arg-type", # Django view arguments (AbstractBaseUser vs User, SessionBase vs SessionStore)
"attr-defined", # Django models have dynamic attributes like .id that mypy doesn't see
"override", # Django backend signature requirements vs mypy expectations
"has-type", # Django internal type resolution (device caching, etc.)
]

6 changes: 3 additions & 3 deletions src/django_security_keys/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def authenticate(
# request can be None, for example in test environments

if not request:
return
return None

credential = kwargs.get("u2f_credential")

Expand All @@ -41,14 +41,14 @@ def authenticate(
# on username

if not username or not credential:
return
return None

has_credentials = SecurityKey.credentials(username, for_login=True)

# no credential supplied

if not has_credentials:
return
return None

# verify passkey login
try:
Expand Down
4 changes: 2 additions & 2 deletions src/django_security_keys/ext/two_factor/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def clean(self):
self.device.user.username, self.request.session, credential
)
self.device.authenticated = True
except Exception:
raise ValidationError(_("Security key authentication failed"))
except Exception as exc:
raise ValidationError(_("Security key authentication failed")) from exc

return self.cleaned_data
12 changes: 8 additions & 4 deletions src/django_security_keys/ext/two_factor/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.contrib.auth import authenticate
from django.contrib.auth.forms import AuthenticationForm
from django.core.handlers.wsgi import WSGIRequest
from django.http.response import HttpResponse, HttpResponseRedirect
from django.http.response import HttpResponseBase, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.views.generic import FormView
from webauthn.helpers import base64url_to_bytes
Expand All @@ -19,7 +19,7 @@


class DisableView(two_factor.views.DisableView):
def dispatch(self, *args: Any, **kwargs: Any) -> HttpResponse:
def dispatch(self, *args: Any, **kwargs: Any) -> HttpResponseBase:
self.success_url = "/"
return FormView.dispatch(self, *args, **kwargs)

Expand Down Expand Up @@ -72,13 +72,15 @@ def attempt_passkey_auth(
if self.steps.current == "auth":
try:
credential = request.POST.get("credential")
if not credential:
raise Exception("No credential provided")
try:
user_handle = base64url_to_bytes(
json.loads(credential)["response"]["userHandle"]
).decode("utf-8")
username = UserHandle.objects.get(handle=user_handle).user.username
except Exception as exc:
raise Exception(f"Failed login using passkey: {exc}")
raise Exception(f"Failed login using passkey: {exc}") from exc
# support passkey login using webauthn
if username and credential:
user = authenticate(
Expand All @@ -100,6 +102,8 @@ def attempt_passkey_auth(
self.passkey_error = f"{exc}"
return self.render_goto_step("auth")

return None

def get_context_data(
self, form: AuthenticationForm | SecurityKeyDeviceValidation, **kwargs: Any
) -> dict[str, Any]:
Expand All @@ -121,7 +125,7 @@ def get_context_data(

return context

def get_security_key_device(self) -> SecurityKeyDevice:
def get_security_key_device(self) -> SecurityKeyDevice | None:
"""
Will return a device object representing a webauthn
choice if the user has any webauthn devices set up
Expand Down
5 changes: 3 additions & 2 deletions src/django_security_keys/migrations/0003_date_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import datetime

from django.db import migrations, models
from django.utils.timezone import utc


class Migration(migrations.Migration):
Expand All @@ -17,7 +16,9 @@ class Migration(migrations.Migration):
name="created",
field=models.DateTimeField(
auto_now_add=True,
default=datetime.datetime(2021, 12, 3, 9, 3, 14, 893775, tzinfo=utc),
default=datetime.datetime(
2021, 12, 3, 9, 3, 14, 893775, tzinfo=datetime.timezone.utc
),
),
preserve_default=False,
),
Expand Down
Loading
Loading