5 Commits

Author SHA1 Message Date
Dan Milne
9dbde8ea31 Fix README: don't claim OIDC certification, just conformance
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:39:10 +11:00
Dan Milne
191a7b5fb3 Update README: add API keys docs, VoidAuth, highlight conformance
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
- Document API keys / bearer tokens for forward auth
- Add VoidAuth to alternatives list
- Move OIDC conformance certification and test counts to top
- Update Ruby requirement to 4.0+, test count to 450

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:36:12 +11:00
Dan Milne
7a9348c1f1 Add voidauth to the list of alternatives
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-03-05 22:30:08 +11:00
Dan Milne
225d8ae5ca Update the README
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-03-05 22:27:24 +11:00
Dan Milne
65c19fa732 Upgrade to Ruby 4.0.1, bump version to 0.9.0
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
Replace CGI.parse (removed in Ruby 4.0) with Rack::Utils.parse_query
in application controller, sessions controller, and OIDC tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:52:11 +11:00
9 changed files with 72 additions and 23 deletions

View File

@@ -1 +1 @@
3.4.8 4.0.1

View File

@@ -8,7 +8,7 @@
# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version # Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.8 ARG RUBY_VERSION=4.0.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
LABEL org.opencontainers.image.source=https://github.com/dkam/clinch LABEL org.opencontainers.image.source=https://github.com/dkam/clinch

View File

@@ -446,6 +446,7 @@ PLATFORMS
arm-linux-gnu arm-linux-gnu
arm-linux-musl arm-linux-musl
arm64-darwin-24 arm64-darwin-24
arm64-darwin-25
x86_64-linux x86_64-linux
x86_64-linux-gnu x86_64-linux-gnu
x86_64-linux-musl x86_64-linux-musl

View File

@@ -15,14 +15,20 @@ Do you host your own web apps? MeTube, Kavita, Audiobookshelf, Gitea, Grafana, P
Clinch runs as a single Docker container, using SQLite as the database, the job queue (Solid Queue) and the shared cache (Solid Cache). The webserver, Puma, runs the job queue in-process, avoiding the need for another container. Clinch runs as a single Docker container, using SQLite as the database, the job queue (Solid Queue) and the shared cache (Solid Cache). The webserver, Puma, runs the job queue in-process, avoiding the need for another container.
Clinch sits in a sweet spot between two excellent open-source identity solutions: Clinch sits in a sweet spot among several excellent open-source identity solutions:
**[Authelia](https://www.authelia.com)** is a fantastic choice for those who prefer external user management through LDAP and enjoy comprehensive YAML-based configuration. It's lightweight, secure, and works beautifully with reverse proxies. **[Authelia](https://www.authelia.com)** is a fantastic choice for those who prefer external user management through LDAP and enjoy comprehensive YAML-based configuration. It's lightweight, secure, and works beautifully with reverse proxies.
**[VoidAuth](https://voidauth.app/)** is an open-source SSO provider with a similar feature set to Clinch — OIDC, ForwardAuth, passkeys, user management, and easy Docker deployment. If you're evaluating self-hosted auth solutions, it's well worth a look.
**[Authentik](https://goauthentik.io)** is an enterprise-grade powerhouse offering extensive protocol support (OAuth2, SAML, LDAP, RADIUS), advanced policy engines, and distributed "outpost" architecture for complex deployments. **[Authentik](https://goauthentik.io)** is an enterprise-grade powerhouse offering extensive protocol support (OAuth2, SAML, LDAP, RADIUS), advanced policy engines, and distributed "outpost" architecture for complex deployments.
**Clinch** offers a middle ground with built-in user management, a modern web interface, and focused SSO capabilities (OIDC + ForwardAuth). It's perfect for users who want self-hosted simplicity without external dependencies or enterprise complexity. **Clinch** offers a middle ground with built-in user management, a modern web interface, and focused SSO capabilities (OIDC + ForwardAuth). It's perfect for users who want self-hosted simplicity without external dependencies or enterprise complexity.
- **[Passes the OpenID Connect Conformance Tests](https://www.certification.openid.net/plan-detail.html?plan=FbQNTJuYVzrzs&public=true)** — verified against the official OIDC test suite
- **450+ tests, 1800+ assertions** — comprehensive test coverage across integration, model, controller, and security tests
- **Single Docker container** — SQLite, job queue, and cache all in one process
--- ---
## Screenshots ## Screenshots
@@ -76,8 +82,6 @@ Apps that only need "who is it?", or you want available from the internet behind
#### OpenID Connect (OIDC) #### OpenID Connect (OIDC)
**[OpenID Connect Conformance](https://www.certification.openid.net/plan-detail.html?plan=FbQNTJuYVzrzs&public=true)** - Clinch passes the official OpenID Connect conformance tests (valid as of [v0.8.6](https://github.com/dkam/clinch/releases/tag/0.8.6)).
Standard OAuth2/OIDC provider with endpoints: Standard OAuth2/OIDC provider with endpoints:
- `/.well-known/openid-configuration` - Discovery endpoint - `/.well-known/openid-configuration` - Discovery endpoint
- `/authorize` - Authorization endpoint with PKCE support - `/authorize` - Authorization endpoint with PKCE support
@@ -131,6 +135,32 @@ Works with reverse proxies (Caddy, Traefik, Nginx):
**Note:** ForwardAuth requires applications to run on the same domain as Clinch (e.g., `app.yourdomain.com` with Clinch at `auth.yourdomain.com`) for secure session cookie sharing. Take a look at Authentik if you need multi domain support. **Note:** ForwardAuth requires applications to run on the same domain as Clinch (e.g., `app.yourdomain.com` with Clinch at `auth.yourdomain.com`) for secure session cookie sharing. Take a look at Authentik if you need multi domain support.
#### API Keys (Bearer Tokens)
For server-to-server access to ForwardAuth-protected services (e.g., a video player accessing WebDAV, rclone syncing files), Clinch supports API keys that work as bearer tokens — no browser or cookies needed.
- **Token format:** `clk_<base64>` prefix for easy identification
- **Storage:** HMAC-SHA256 hashed (plaintext shown once at creation, never stored)
- **Scope:** Each key is tied to one ForwardAuth application and one user
- **Expiration:** Optional — set a date or leave blank for no expiry
- **Auth flow:** `Authorization: Bearer clk_...` header checked before cookie auth
- **Failure response:** 401 JSON `{"error": "..."}` (no redirect)
**Creating an API key:**
1. Go to **Dashboard → Manage API Keys** (or `/api_keys`)
2. Click **New API Key**, select a ForwardAuth application, and give it a name
3. Copy the `clk_...` token — it's shown only once
**Usage:**
```bash
curl -H "Authorization: Bearer clk_..." \
-H "X-Forwarded-Host: webdav.example.com" \
https://auth.example.com/api/verify
# Returns 200 with X-Remote-User headers on success
```
API keys respect the same access controls as browser sessions — the user must have access to the application, the application must be active, and the user's account must be active.
### SMTP Integration ### SMTP Integration
Send emails for: Send emails for:
- Invitation links (one-time token, 7-day expiry) - Invitation links (one-time token, 7-day expiry)
@@ -287,7 +317,7 @@ This is transparent to end users and requires no configuration.
## Setup & Installation ## Setup & Installation
### Requirements ### Requirements
- Ruby 3.3+ - Ruby 4.0+
- SQLite 3.8+ - SQLite 3.8+
- SMTP server (for sending emails) - SMTP server (for sending emails)
@@ -701,7 +731,7 @@ user.revoke_all_consents!
### Running Tests ### Running Tests
Clinch has comprehensive test coverage with 341 tests covering integration, models, controllers, services, and system tests. Clinch has comprehensive test coverage with 450 tests covering integration, models, controllers, services, and system tests.
```bash ```bash
# Run all tests # Run all tests
@@ -761,7 +791,7 @@ All security scans run automatically on every pull request and push to main via
**Current Status:** **Current Status:**
- ✅ All security scans passing - ✅ All security scans passing
-341 tests, 1349 assertions, 0 failures -450 tests, 1818 assertions, 0 failures
- ✅ No known dependency vulnerabilities - ✅ No known dependency vulnerabilities
- ✅ Phases 1-4 security hardening complete (18+ vulnerabilities fixed) - ✅ Phases 1-4 security hardening complete (18+ vulnerabilities fixed)
- 🟡 3 outstanding security issues (all MEDIUM/LOW priority) - 🟡 3 outstanding security issues (all MEDIUM/LOW priority)

View File

@@ -28,12 +28,10 @@ class ApplicationController < ActionController::Base
uri = URI.parse(url) uri = URI.parse(url)
return url unless uri.query return url unless uri.query
# Parse query string into hash params = Rack::Utils.parse_query(uri.query)
params = CGI.parse(uri.query)
params.delete(param_name) params.delete(param_name)
# Rebuild query string (empty string if no params left) uri.query = params.any? ? Rack::Utils.build_query(params) : nil
uri.query = params.any? ? URI.encode_www_form(params) : nil
uri.to_s uri.to_s
rescue URI::InvalidURIError rescue URI::InvalidURIError
url url

View File

@@ -20,8 +20,8 @@ class SessionsController < ApplicationController
begin begin
uri = URI.parse(session[:return_to_after_authenticating]) uri = URI.parse(session[:return_to_after_authenticating])
if uri.query.present? if uri.query.present?
query_params = CGI.parse(uri.query) query_params = Rack::Utils.parse_query(uri.query)
@login_hint = query_params["login_hint"]&.first @login_hint = query_params["login_hint"]
end end
rescue URI::InvalidURIError rescue URI::InvalidURIError
# Ignore parsing errors # Ignore parsing errors

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Clinch module Clinch
VERSION = "0.8.8" VERSION = "0.9.0"
end end

22
db/schema.rb generated
View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_01_05_000809) do ActiveRecord::Schema[8.1].define(version: 2026_03_05_000001) do
create_table "active_storage_attachments", force: :cascade do |t| create_table "active_storage_attachments", force: :cascade do |t|
t.bigint "blob_id", null: false t.bigint "blob_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
@@ -39,6 +39,24 @@ ActiveRecord::Schema[8.1].define(version: 2026_01_05_000809) do
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end end
create_table "api_keys", force: :cascade do |t|
t.integer "application_id", null: false
t.datetime "created_at", null: false
t.datetime "expires_at"
t.datetime "last_used_at"
t.string "name", null: false
t.datetime "revoked_at"
t.string "token_hmac", null: false
t.datetime "updated_at", null: false
t.integer "user_id", null: false
t.index ["application_id"], name: "index_api_keys_on_application_id"
t.index ["expires_at"], name: "index_api_keys_on_expires_at"
t.index ["revoked_at"], name: "index_api_keys_on_revoked_at"
t.index ["token_hmac"], name: "index_api_keys_on_token_hmac", unique: true
t.index ["user_id", "application_id"], name: "index_api_keys_on_user_id_and_application_id"
t.index ["user_id"], name: "index_api_keys_on_user_id"
end
create_table "application_groups", force: :cascade do |t| create_table "application_groups", force: :cascade do |t|
t.integer "application_id", null: false t.integer "application_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
@@ -249,6 +267,8 @@ ActiveRecord::Schema[8.1].define(version: 2026_01_05_000809) do
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "api_keys", "applications"
add_foreign_key "api_keys", "users"
add_foreign_key "application_groups", "applications" add_foreign_key "application_groups", "applications"
add_foreign_key "application_groups", "groups" add_foreign_key "application_groups", "groups"
add_foreign_key "application_user_claims", "applications", on_delete: :cascade add_foreign_key "application_user_claims", "applications", on_delete: :cascade

View File

@@ -46,7 +46,7 @@ class OidcPromptLoginTest < ActionDispatch::IntegrationTest
assert_response :redirect assert_response :redirect
first_redirect_url = response.location first_redirect_url = response.location
first_code = CGI.parse(URI(first_redirect_url).query)["code"].first first_code = Rack::Utils.parse_query(URI(first_redirect_url).query)["code"]
# Exchange for tokens and extract auth_time # Exchange for tokens and extract auth_time
post "/oauth/token", params: { post "/oauth/token", params: {
@@ -90,7 +90,7 @@ class OidcPromptLoginTest < ActionDispatch::IntegrationTest
# Should receive authorization code # Should receive authorization code
assert_response :redirect assert_response :redirect
second_redirect_url = response.location second_redirect_url = response.location
second_code = CGI.parse(URI(second_redirect_url).query)["code"].first second_code = Rack::Utils.parse_query(URI(second_redirect_url).query)["code"]
assert second_code.present?, "Should receive authorization code after re-authentication" assert second_code.present?, "Should receive authorization code after re-authentication"
@@ -134,11 +134,11 @@ class OidcPromptLoginTest < ActionDispatch::IntegrationTest
# Parse the redirect URL # Parse the redirect URL
uri = URI.parse(redirect_url) uri = URI.parse(redirect_url)
query_params = uri.query ? CGI.parse(uri.query) : {} query_params = uri.query ? Rack::Utils.parse_query(uri.query) : {}
assert_equal "login_required", query_params["error"]&.first, assert_equal "login_required", query_params["error"],
"Should return login_required error for prompt=none when not authenticated" "Should return login_required error for prompt=none when not authenticated"
assert_equal "test-state", query_params["state"]&.first, assert_equal "test-state", query_params["state"],
"Should return state parameter" "Should return state parameter"
end end
@@ -165,7 +165,7 @@ class OidcPromptLoginTest < ActionDispatch::IntegrationTest
assert_response :redirect assert_response :redirect
first_redirect_url = response.location first_redirect_url = response.location
first_code = CGI.parse(URI(first_redirect_url).query)["code"].first first_code = Rack::Utils.parse_query(URI(first_redirect_url).query)["code"]
# Exchange for tokens and extract auth_time from ID token # Exchange for tokens and extract auth_time from ID token
post "/oauth/token", params: { post "/oauth/token", params: {
@@ -209,7 +209,7 @@ class OidcPromptLoginTest < ActionDispatch::IntegrationTest
# Should receive authorization code redirect # Should receive authorization code redirect
assert_response :redirect assert_response :redirect
second_redirect_url = response.location second_redirect_url = response.location
second_code = CGI.parse(URI(second_redirect_url).query)["code"].first second_code = Rack::Utils.parse_query(URI(second_redirect_url).query)["code"]
assert second_code.present?, "Should receive authorization code after re-authentication" assert second_code.present?, "Should receive authorization code after re-authentication"