Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8abf940
wip: initial cookie to token exchange endpoint
MrSecure Apr 12, 2026
237219d
chore: version updates
MrSecure Apr 12, 2026
bbd7a56
feat: add cookie exchange endpoint and configuration options
MrSecure Apr 12, 2026
98510c5
wip: update login to support custom header
MrSecure Apr 12, 2026
756e4da
feat: enhance entrypoint for config validation and update Dockerfile …
MrSecure Apr 12, 2026
e8f8333
fix: address CodeQL URL redirect and XSS security alerts
Copilot Apr 12, 2026
3ba09d3
fix: replace is_safe_redirect with get_safe_redirect_url to address C…
Copilot Apr 12, 2026
5224771
test: update redirect URL tests to call get_safe_redirect_url directly
Copilot Apr 12, 2026
e280d44
doc: update README, add ENDPOINTS documentation
MrSecure Apr 12, 2026
4d27280
chore: ruff fixes
MrSecure Apr 12, 2026
38e742a
fix: adjust task clean to conver missed items
MrSecure Apr 12, 2026
7fb6661
fix: upgrade nodejs for CI workflow
MrSecure Apr 12, 2026
b1ef07f
chore: add bandit scan
MrSecure Apr 12, 2026
45b7eee
fix: Potential fix for pull request finding 'CodeQL / URL redirection…
MrSecure Apr 13, 2026
e1c39e5
fix: cookie crumble tests
MrSecure Apr 13, 2026
b4ff644
doc: Update README.md
MrSecure Apr 13, 2026
517e19d
test: replace brittle source-inspection tests with functional endpoin…
Copilot Apr 13, 2026
c177f60
test: address code review feedback on cookie-exchange tests
Copilot Apr 13, 2026
269790a
fix: URL-encode rd parameter in external login redirect
Copilot Apr 13, 2026
d30784d
fix: move urllib.parse imports to top of test_main.py
Copilot Apr 13, 2026
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
10 changes: 10 additions & 0 deletions .github/actions/bootstrap/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ inputs:
token:
description: 'A Github token'
required: true
node-version:
description: 'Node.js version to use'
required: false
default: '24.14.1'
python-version:
description: 'Python version to use'
required: false
Expand All @@ -30,6 +34,12 @@ runs:
cache-dependency-glob: "**/uv.lock"
python-version: ${{ inputs.python-version }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
check-latest: true

- name: Install Task
uses: go-task/setup-task@v1
with:
Expand Down
1 change: 1 addition & 0 deletions .github/etc/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ MrSecure
allstar
auth
dependabot
healthz
home.arpa
homelab
homelab-auth
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
exclude: '^homelab-auth/.*'
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 9b1e9a33f1b33f6ecc2fdbb8d7cc894e951106e4 # frozen: v0.14.13
rev: b831c3dc5d27d9da294ae4e915773b99aa24a7c5 # frozen: v0.15.10
hooks:
- id: ruff-check
types_or: [ python, pyi, jupyter ]
Expand Down Expand Up @@ -32,7 +32,7 @@ repos:
# args: ["--config", ".github/linters/lychee.toml"]
# types: [markdown]
- repo: https://github.com/rhysd/actionlint
rev: 0933c147c9d6587653d45fdcb4c497c57a65f9af # frozen: v1.7.10
rev: 914e7df21a07ef503a81201c76d2b11c789d3fca # frozen: v1.7.12
hooks:
- id: actionlint-docker
- repo: https://github.com/koalaman/shellcheck-precommit
Expand All @@ -47,11 +47,11 @@ repos:
# # Check the past 2 commits; it's useful to make this go further back than main when running this where main and HEAD are equal
# entry: trufflehog git file://. --since-commit main~1 --no-verification --no-update --fail
- repo: https://github.com/python-openapi/openapi-spec-validator
rev: a76da2ffdaf698a7fdbd755f89b051fef4c790fd # frozen: 0.8.0b1
rev: 85b3337638d302ab966d8ec7aa5fc93dbd9f508c # frozen: 0.8.4
hooks:
- id: openapi-spec-validator
- repo: https://github.com/streetsidesoftware/cspell-cli
rev: 1cfa010f078c354f3ffb8413616280cc28f5ba21 # frozen: v9.4.0
rev: 4643f154907327ee0a2c7038f0296e0dd77d9776 # frozen: v10.0.0
hooks:
- id: cspell
types: [markdown]
Expand Down
2 changes: 0 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ task init # Set up development environment
task build # Build the project
task test # Run all tests
task lint # Check code quality
task format # Auto-format code
```

### Before Committing
Expand Down Expand Up @@ -214,7 +213,6 @@ def main():
- `task build`: Build the project
- `task test`: Run tests with coverage
- `task lint`: Run all linters
- `task format`: Auto-format code
- `task security-scan`: Check for vulnerabilities
- `task docker-build`: Build Docker image
- `task docker-run`: Run in Docker container
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ COPY --from=builder --chown=app:app /app /app

# Create app user
RUN chown root:app /app/config.yaml && \
chmod 0660 /app/config.yaml
chmod 0660 /app/config.yaml && \
mkdir /app/.gunicorn && \
chown app:app /app/.gunicorn

# Metadata
ARG NAME="homelab_auth"
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,14 @@ task -v build

## API Endpoints

- **GET/POST `/logout`** - Invalidates the user session cookie and logs out the user
- **GET `/verify`** - Validates session cookie/header for Traefik forward-auth checks and returns status responses (`OK`, `Invalid Session`, or `Unauthorized`) without redirecting
- **GET `/`** - Redirects to the login page with a safe `rd` return destination
- **GET `/login`** - Renders the login form with CSRF protection
- **POST `/login`** - Validates credentials, sets the signed session cookie, and redirects to `rd`
- **GET `/done`** - Optional local success landing page after authentication
- **GET `/cookie-crumbling-protocol-v2`** - Returns authenticated token and identity for cookie/header exchange
- **GET/POST `/logout`** - Clears the session cookie and renders logged-out feedback
- **GET `/healthz`** - Returns service health status for probes and monitoring

## Automated Dependency Management

Expand Down
15 changes: 13 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ tasks:
- '{{.RUN_SCRIPT}} pre-commit validate-manifest'
- '{{.RUN_SCRIPT}} python scripts/validate_service_definition.py'

security-scan:
desc: Perform static security analysis on application code (excluding tests)
cmds:
- |
uvx --from 'bandit[toml]' bandit \
-r src \
-x tests \
-ii \
-lll

build:
desc: Build the project; docker images, compiled binaries, etc.
vars:
Expand Down Expand Up @@ -139,7 +149,7 @@ tasks:
deps: ["coverage-erase"]
cmds:
- task: unit-test
# - task: integration-test
- task: integration-test

coverage-erase:
desc: Erase coverage
Expand Down Expand Up @@ -201,6 +211,7 @@ tasks:
clean:
desc: Clean up build artifacts, cache files/directories, temp files, etc.
cmds:
- task: coverage-erase
- rm -rf .pytest_cache
- rm -rf htmlcov
- rm -rf .coverage
Expand All @@ -214,7 +225,7 @@ tasks:
- rm -f sbom.*.json
- rm -f vulns.*.json
- rm -f license-check.*.json
- rm -f MrSecure_homelab_auth_*_*.tar
- rm -f MrSecure_homelab-auth_*_*.tar
- find . -type d -name __pycache__ -exec rm -rf {} + || true
- find . -type f -name '*.pyc' -delete || true

Expand Down
149 changes: 149 additions & 0 deletions docs/ENDPOINTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# API Endpoints

## GET `/verify`

Validates an existing session for forward-auth and API checks.

- Checks the configured session cookie first.
- If cookie exchange is enabled, checks the configured auth header as a fallback.
- Does not redirect to login; returns status directly so reverse proxies can decide the next step.

**Responses:**

- `200 OK` with body `OK` when the token is valid.
- `401 Unauthorized` with body `Invalid Session` when a token is present but invalid or expired.
- `401 Unauthorized` with body `Unauthorized` when no token is provided.

## GET `/`

Entry route that normalizes the destination and forwards the user to the login page.

- Accepts optional query parameter `rd` (return destination).
- Validates `rd` against allowed redirect domain rules.
- Falls back to configured default destination when `rd` is missing or unsafe.
- Returns an HTTP `307` redirect to `/login?rd=<destination>` on the external auth host.

## GET `/login`

Renders the login form.

- Validates and normalizes `rd` the same way as `/`.
- If a valid session cookie already exists, immediately redirects to `rd`.
- If cookie exchange is enabled and a valid auth header is present, immediately redirects to `rd`.
- Otherwise renders the login page with a CSRF token tied to the client address.

**Responses:**

- `200 OK` with HTML login form.
- `302 Found` redirect to `rd` when user is already authenticated.

## POST `/login`

Processes submitted credentials and creates a session.

- Requires a valid CSRF token.
- Rejects missing or oversized username/password inputs.
- Verifies credentials against the configured htpasswd user store.
- On success, sets the signed session cookie with configured security flags and redirects to `rd`.

**Responses:**

- `302 Found` redirect to `rd` with session cookie on successful authentication.
- `400 Bad Request` when CSRF validation fails.
- `401 Unauthorized` when credentials are invalid or required fields are missing.

## GET `/done`

Simple local success page used by some auth flows after login.

- Returns a minimal HTML message indicating login succeeded.

**Responses:**

- `200 OK` with HTML body.

## GET `/cookie-crumbling-protocol-v2`

Exchanges a valid session cookie for structured identity data.

- Intended for integrations that need token/identity/header metadata from the authenticated session.
- Returns `400` for missing/invalid session cookies to avoid proxy auth loops.
- Can be disabled via configuration.

**Response (success - HTTP 200):**

```json
{
"token": "signed.cookie.value",
"identity": "username",
"cookie": "session_cookie_name",
"header": "X-Configured-Auth-Header"
}
```

**Response (missing cookie - HTTP 400):**

```json
{
"error": "Unauthorized"
}
```

**Response (invalid session - HTTP 400):**

```json
{
"error": "Invalid Session"
}
```

**Response (disabled - HTTP 404):**

```json
{
"error": "Not Found"
}
```

**Configuration:**

By default, this endpoint is enabled. To disable it, set `exchange_enabled` to `false` in the `cookie` section of your configuration file:

```yaml
cookie:
name: session
secure: true
httponly: true
samesite: Lax
domain: .example.com
exchange_enabled: false # Set to true (default) to enable
```

When disabled, requests to the endpoint receive a `404` response.

## GET/POST `/logout`

Logs the user out by invalidating the browser session cookie.

- Sets the session cookie to a placeholder value with `max_age=0` to expire it.
- Preserves configured cookie security flags (`Secure`, `HttpOnly`, `SameSite`, and domain).
- Returns the login page with a "Logged out." feedback message.

**Responses:**

- `200 OK` with HTML login page and expired session cookie.

## GET `/healthz`

Lightweight service health endpoint for container and load balancer probes.

- Does not require authentication.
- Returns static JSON status.

**Response (success - HTTP 200):**

```json
{
"status": "healthy"
}
```
3 changes: 3 additions & 0 deletions examples/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ cookie:
allowed_hosts:
- dev.home.arpa
- labs.home.arpa
exchange_enabled: true
header:
name: X-HomeLab-Auth-Token
server:
port: 55000
host: 0.0.0.0
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ token = { env = "GH_TOKEN" }
ignore_token_for_push = false
insecure = false

[tool.bandit]
# Only report Medium and High severity findings.
severity = ["MEDIUM", "HIGH"]
targets = ["src"]
exclude_dirs = ["tests"]

[tool.ruff]
target-version = "py311"
extend-exclude = ["tests/*"]
Loading
Loading