Merge branch 'main' into dev
This commit is contained in:
commit
2f63f4cd17
12 changed files with 449 additions and 69 deletions
9
.github/codeql/codeql-config.yml
vendored
Normal file
9
.github/codeql/codeql-config.yml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
name: "WireGUI CodeQL config"
|
||||
|
||||
query-filters:
|
||||
# API token hashing uses HMAC-SHA256 which is appropriate for high-entropy
|
||||
# tokens (256-bit random). Actual password hashing uses bcrypt.
|
||||
# CodeQL flags any SHA-family hash as "weak for password hashing" but this
|
||||
# rule is not applicable to API token lookups.
|
||||
- exclude:
|
||||
id: py/weak-sensitive-data-hashing
|
||||
32
.github/workflows/codeql.yml
vendored
Normal file
32
.github/workflows/codeql.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: "0 6 * * 1"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (Python)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: python
|
||||
config-file: .github/codeql/codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
91
.github/workflows/dev.yml
vendored
Normal file
91
.github/workflows/dev.yml
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
name: Dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: python:3.13-slim
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y --no-install-recommends git ca-certificates
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "noreply@github.com"
|
||||
|
||||
- name: Install uv and dependencies
|
||||
run: |
|
||||
pip install uv
|
||||
uv sync --group dev
|
||||
|
||||
- name: Semantic release (rc)
|
||||
id: semrel
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
VERSION=$(uv run semantic-release version --print 2>/dev/null || echo "")
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "No release needed"
|
||||
else
|
||||
uv run semantic-release version
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "new_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "Released v${VERSION}"
|
||||
fi
|
||||
|
||||
outputs:
|
||||
new_version: ${{ steps.semrel.outputs.new_version }}
|
||||
skip: ${{ steps.semrel.outputs.skip }}
|
||||
|
||||
docker:
|
||||
needs: release
|
||||
if: needs.release.outputs.skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push pre-release image
|
||||
run: |
|
||||
VERSION="${{ needs.release.outputs.new_version }}"
|
||||
IMAGE="ghcr.io/${{ github.repository_owner }}/wiregui"
|
||||
|
||||
echo "Building ${IMAGE}:v${VERSION}"
|
||||
|
||||
docker build --no-cache \
|
||||
--build-arg "VERSION=${VERSION}" \
|
||||
-t "${IMAGE}:v${VERSION}" \
|
||||
-t "${IMAGE}:dev" \
|
||||
.
|
||||
|
||||
docker push "${IMAGE}:v${VERSION}"
|
||||
docker push "${IMAGE}:dev"
|
||||
|
||||
echo "Pushed ${IMAGE}:v${VERSION}, ${IMAGE}:dev"
|
||||
241
.github/workflows/release.yml
vendored
Normal file
241
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: python:3.13-slim
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
env:
|
||||
POSTGRES_USER: wiregui
|
||||
POSTGRES_PASSWORD: wiregui
|
||||
POSTGRES_DB: wiregui
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U wiregui"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
valkey:
|
||||
image: valkey/valkey:8
|
||||
options: >-
|
||||
--health-cmd "valkey-cli ping"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
mock-oidc:
|
||||
image: ghcr.io/navikt/mock-oauth2-server:2.1.10
|
||||
ports:
|
||||
- 9000:9000
|
||||
env:
|
||||
SERVER_PORT: "9000"
|
||||
JSON_CONFIG: >
|
||||
{
|
||||
"interactiveLogin": true,
|
||||
"httpServer": "NettyWrapper",
|
||||
"tokenCallbacks": [
|
||||
{
|
||||
"issuerId": "test-idp",
|
||||
"tokenExpiry": 3600,
|
||||
"requestMappings": [
|
||||
{
|
||||
"requestParam": "scope",
|
||||
"match": "*",
|
||||
"claims": {
|
||||
"sub": "$${claim:sub}",
|
||||
"email": "$${claim:sub}@test.local",
|
||||
"name": "Test User"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
CI: "true"
|
||||
WG_DATABASE_URL: postgresql+asyncpg://wiregui:wiregui@postgres/wiregui
|
||||
WG_REDIS_URL: redis://valkey:6379/0
|
||||
MOCK_OIDC_HOST: mock-oidc
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
git wireguard-tools pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
run: pip install uv
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: uv run playwright install --with-deps chromium
|
||||
|
||||
- name: Run unit tests
|
||||
run: uv run pytest tests/ --ignore=tests/e2e -v --tb=short
|
||||
|
||||
- name: Run E2E tests
|
||||
run: |
|
||||
uv run alembic upgrade head
|
||||
uv run pytest tests/e2e/ -v --tb=short
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
new_tag: ${{ steps.version.outputs.new_tag }}
|
||||
new_version: ${{ steps.version.outputs.new_version }}
|
||||
skip: ${{ steps.version.outputs.skip }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "noreply@github.com"
|
||||
|
||||
- name: Determine version bump
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||
echo "latest_tag=${LATEST_TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
CURRENT="${LATEST_TAG#v}"
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
||||
|
||||
COMMITS=$(git log "${LATEST_TAG}..HEAD" --pretty=format:"%s" 2>/dev/null || git log --pretty=format:"%s")
|
||||
|
||||
BUMP="none"
|
||||
while IFS= read -r msg; do
|
||||
case "$msg" in
|
||||
*"BREAKING CHANGE"*|*"!:"*)
|
||||
BUMP="major"
|
||||
break
|
||||
;;
|
||||
feat:*|feat\(*)
|
||||
[ "$BUMP" != "major" ] && BUMP="minor"
|
||||
;;
|
||||
fix:*|fix\(*|perf:*|perf\(*|refactor:*|refactor\(*)
|
||||
[ "$BUMP" = "none" ] && BUMP="patch"
|
||||
;;
|
||||
esac
|
||||
done <<< "$COMMITS"
|
||||
|
||||
if [ "$BUMP" = "none" ]; then
|
||||
echo "No version-relevant commits since ${LATEST_TAG}, skipping release"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$BUMP" in
|
||||
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
|
||||
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
|
||||
patch) PATCH=$((PATCH + 1)) ;;
|
||||
esac
|
||||
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||
echo "new_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "new_tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "bump=${BUMP}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Version bump: ${BUMP} -> v${NEW_VERSION}"
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
LATEST_TAG="${{ steps.version.outputs.latest_tag }}"
|
||||
NEW_TAG="${{ steps.version.outputs.new_tag }}"
|
||||
|
||||
BODY="## ${NEW_TAG}"$'\n\n'
|
||||
|
||||
for type_label in "feat:Features" "fix:Bug Fixes" "refactor:Refactoring" "perf:Performance" "docs:Documentation" "chore:Maintenance"; do
|
||||
prefix="${type_label%%:*}"
|
||||
label="${type_label#*:}"
|
||||
MATCHES=$(git log "${LATEST_TAG}..HEAD" --pretty=format:"%s" 2>/dev/null | grep -E "^${prefix}[:(]" || true)
|
||||
if [ -n "$MATCHES" ]; then
|
||||
BODY="${BODY}### ${label}"$'\n\n'
|
||||
while IFS= read -r line; do
|
||||
CLEAN=$(echo "$line" | sed -E "s/^${prefix}(\([^)]*\))?:\s*//")
|
||||
BODY="${BODY}- ${CLEAN}"$'\n'
|
||||
done <<< "$MATCHES"
|
||||
BODY="${BODY}"$'\n'
|
||||
fi
|
||||
done
|
||||
|
||||
echo "${BODY}" > /tmp/changelog.md
|
||||
echo "Generated changelog for ${NEW_TAG}"
|
||||
|
||||
- name: Create tag and release
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
NEW_TAG="${{ steps.version.outputs.new_tag }}"
|
||||
|
||||
git tag -a "${NEW_TAG}" -m "Release ${NEW_TAG}"
|
||||
git push origin "${NEW_TAG}"
|
||||
|
||||
gh release create "${NEW_TAG}" \
|
||||
--title "${NEW_TAG}" \
|
||||
--notes-file /tmp/changelog.md
|
||||
|
||||
docker:
|
||||
needs: release
|
||||
if: needs.release.outputs.skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push image
|
||||
run: |
|
||||
VERSION="${{ needs.release.outputs.new_version }}"
|
||||
TAG="${{ needs.release.outputs.new_tag }}"
|
||||
IMAGE="ghcr.io/${{ github.repository_owner }}/wiregui"
|
||||
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
MINOR=$(echo "$VERSION" | cut -d. -f2)
|
||||
|
||||
echo "Building ${IMAGE}:${TAG}"
|
||||
|
||||
docker build --no-cache \
|
||||
--build-arg "VERSION=${VERSION}" \
|
||||
-t "${IMAGE}:${TAG}" \
|
||||
-t "${IMAGE}:${MAJOR}.${MINOR}" \
|
||||
-t "${IMAGE}:latest" \
|
||||
.
|
||||
|
||||
docker push "${IMAGE}:${TAG}"
|
||||
docker push "${IMAGE}:${MAJOR}.${MINOR}"
|
||||
docker push "${IMAGE}:latest"
|
||||
|
||||
echo "Pushed ${IMAGE}:${TAG}, ${IMAGE}:${MAJOR}.${MINOR}, ${IMAGE}:latest"
|
||||
39
SECURITY.md
Normal file
39
SECURITY.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
|---------|--------------------|
|
||||
| latest | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in WireGUI, please report it responsibly through **GitHub's private vulnerability reporting**:
|
||||
|
||||
1. Go to the [Security Advisories](https://github.com/bartei/wiregui/security/advisories) page
|
||||
2. Click **"Report a vulnerability"**
|
||||
3. Fill in the details of the vulnerability
|
||||
|
||||
Please **do not** open a public issue for security vulnerabilities.
|
||||
|
||||
## What to Expect
|
||||
|
||||
- You will receive an acknowledgment within **48 hours**
|
||||
- We will provide a timeline for a fix within **7 days**
|
||||
- Security patches will be released as soon as possible
|
||||
|
||||
## Scope
|
||||
|
||||
The following are in scope for security reports:
|
||||
|
||||
- Authentication and authorization bypasses
|
||||
- SQL injection, XSS, CSRF, or other injection vulnerabilities
|
||||
- WireGuard configuration issues that could expose private keys
|
||||
- API token or session handling flaws
|
||||
- Privilege escalation between user roles
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Denial of service (DoS) attacks
|
||||
- Issues in third-party dependencies (report these upstream)
|
||||
- Social engineering attacks
|
||||
|
|
@ -20,7 +20,7 @@ dependencies = [
|
|||
"cryptography>=44",
|
||||
# Auth
|
||||
"bcrypt>=4.0",
|
||||
"python-jose[cryptography]>=3.3",
|
||||
"pyjwt[crypto]>=2.9",
|
||||
"authlib>=1.4",
|
||||
"pyotp>=2.9",
|
||||
"webauthn>=2.2",
|
||||
|
|
|
|||
|
|
@ -166,8 +166,10 @@ async def test_seed_preserves_providers_not_in_yaml(clean_config, monkeypatch):
|
|||
|
||||
|
||||
async def test_seed_invalid_yaml(clean_config, monkeypatch):
|
||||
path = Path(tempfile.mktemp(suffix=".yaml"))
|
||||
path.write_text(": : : invalid yaml [[[")
|
||||
f = tempfile.NamedTemporaryFile(suffix=".yaml", delete=False, mode="w")
|
||||
f.write(": : : invalid yaml [[[")
|
||||
f.close()
|
||||
path = Path(f.name)
|
||||
monkeypatch.setattr("wiregui.auth.seed.get_settings", lambda: type("S", (), {"idp_config_file": str(path)})())
|
||||
await seed_idp_providers()
|
||||
async with async_session() as session:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
"""Tests for REST API endpoints and token auth."""
|
||||
|
||||
import hashlib
|
||||
|
||||
from wiregui.auth.api_token import generate_api_token, resolve_bearer_token
|
||||
from wiregui.auth.api_token import _token_hmac, generate_api_token, resolve_bearer_token
|
||||
from wiregui.auth.passwords import hash_password
|
||||
from wiregui.models.api_token import ApiToken
|
||||
from wiregui.models.user import User
|
||||
|
|
@ -15,7 +13,7 @@ from wiregui.utils.time import utcnow
|
|||
def test_generate_api_token():
|
||||
plaintext, token_hash = generate_api_token()
|
||||
assert len(plaintext) > 20
|
||||
assert token_hash == hashlib.sha256(plaintext.encode()).hexdigest()
|
||||
assert token_hash == _token_hmac(plaintext)
|
||||
|
||||
|
||||
def test_generate_api_token_unique():
|
||||
|
|
|
|||
70
uv.lock
generated
70
uv.lock
generated
|
|
@ -600,18 +600,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ca/8de7744cb3bc966c85430ca2d0fcaeea872507c6a4cf6e007f7fe269ed9d/ecdsa-0.19.2.tar.gz", hash = "sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930", size = 202432, upload-time = "2026-03-26T09:58:17.675Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399", size = 150818, upload-time = "2026-03-26T09:58:15.808Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.135.2"
|
||||
|
|
@ -1502,6 +1490,20 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
crypto = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyopenssl"
|
||||
version = "26.0.0"
|
||||
|
|
@ -1599,25 +1601,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/34/bd/b0d440685fbcafee462bed793a74aea88541887c4c30556a55ac64914b8d/python_gitlab-6.5.0-py3-none-any.whl", hash = "sha256:494e1e8e5edd15286eaf7c286f3a06652688f1ee20a49e2a0218ddc5cc475e32", size = 144419, upload-time = "2025-10-17T21:40:01.233Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-jose"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ecdsa" },
|
||||
{ name = "pyasn1" },
|
||||
{ name = "rsa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
cryptography = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.22"
|
||||
|
|
@ -1797,18 +1780,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "4.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyasn1" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
|
|
@ -1830,15 +1801,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smmap"
|
||||
version = "5.0.3"
|
||||
|
|
@ -2133,8 +2095,8 @@ dependencies = [
|
|||
{ name = "loguru" },
|
||||
{ name = "nicegui" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "pyotp" },
|
||||
{ name = "python-jose", extra = ["cryptography"] },
|
||||
{ name = "python3-saml" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "qrcode", extra = ["pil"] },
|
||||
|
|
@ -2165,8 +2127,8 @@ requires-dist = [
|
|||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
{ name = "nicegui", specifier = ">=2.12" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.7" },
|
||||
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.9" },
|
||||
{ name = "pyotp", specifier = ">=2.9" },
|
||||
{ name = "python-jose", extras = ["cryptography"], specifier = ">=3.3" },
|
||||
{ name = "python3-saml", specifier = ">=1.16" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "qrcode", extras = ["pil"], specifier = ">=8.0" },
|
||||
|
|
|
|||
|
|
@ -1,27 +1,33 @@
|
|||
"""API token authentication — Bearer token via Authorization header."""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
|
||||
from wiregui.config import get_settings
|
||||
from wiregui.models.api_token import ApiToken
|
||||
from wiregui.models.user import User
|
||||
from wiregui.utils.time import utcnow
|
||||
|
||||
|
||||
def _token_hmac(token: str) -> str:
|
||||
"""Compute a keyed HMAC-SHA256 digest of an API token."""
|
||||
key = get_settings().secret_key.encode()
|
||||
return hmac.new(key, token.encode(), "sha256").hexdigest()
|
||||
|
||||
|
||||
def generate_api_token() -> tuple[str, str]:
|
||||
"""Generate a new API token. Returns (plaintext_token, token_hash)."""
|
||||
plaintext = secrets.token_urlsafe(32)
|
||||
token_hash = hashlib.sha256(plaintext.encode()).hexdigest()
|
||||
return plaintext, token_hash
|
||||
return plaintext, _token_hmac(plaintext)
|
||||
|
||||
|
||||
async def resolve_bearer_token(session: AsyncSession, token: str) -> User | None:
|
||||
"""Look up a Bearer token and return the associated user, or None."""
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
token_hash = _token_hmac(token)
|
||||
result = await session.execute(
|
||||
select(ApiToken).where(ApiToken.token_hash == token_hash)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from jose import JWTError, jwt
|
||||
import jwt
|
||||
|
||||
from wiregui.config import get_settings
|
||||
|
||||
|
|
@ -22,5 +22,5 @@ def decode_access_token(token: str) -> dict | None:
|
|||
"""Decode and validate a JWT. Returns the payload dict or None if invalid/expired."""
|
||||
try:
|
||||
return jwt.decode(token, get_settings().secret_key, algorithms=[ALGORITHM])
|
||||
except JWTError:
|
||||
except (jwt.InvalidTokenError, jwt.ExpiredSignatureError):
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ async def oidc_callback(provider_id: str, request: Request):
|
|||
id_token = token.get("id_token")
|
||||
if id_token:
|
||||
try:
|
||||
from jose import jwt as jose_jwt
|
||||
import jwt as pyjwt
|
||||
# Decode without verification — we already verified during token exchange
|
||||
claims = jose_jwt.get_unverified_claims(id_token)
|
||||
claims = pyjwt.decode(id_token, options={"verify_signature": False})
|
||||
userinfo = userinfo or {}
|
||||
if not userinfo.get("email"):
|
||||
userinfo["email"] = claims.get("email")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue