feat(ics): add HTTP Basic Authentication for subscription feeds#1515
feat(ics): add HTTP Basic Authentication for subscription feeds#1515JohnVillalovos wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional HTTP Basic Authentication gate to calendar subscription feeds (ICS/Atom) so feed consumers can authenticate per-request (without creating a persistent browser session), enabling “fully authenticated” feed rendering when credentials are provided.
Changes:
- Introduces
SubscriptionPage::tryBasicAuth()and a shared subscription-feed request lifecycle (gate → presenter → render). - Adds feed-scoped session support (
GetFeedUserSession()) so presenters can render with authenticated context when Basic Auth succeeds. - Adds configuration and tests for the new Basic Auth feature.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Presenters/CalendarSubscriptionPresenterTest.php | Extends the test page double with SetIsNotFound() and feed-session accessors. |
| tests/Pages/Export/CalendarSubscriptionPageTest.php | New PHPUnit coverage for tryBasicAuth() behavior (gates + basic-auth branches). |
| tests/fakes/FakeWebAuthentication.php | Adds fake support for LoginForFeed() and validation call tracking. |
| tests/fakes/FakeAuth.php | Implements new BuildSession() API on the authentication fake. |
| tests/Application/Authentication/AuthenticationTest.php | Updates PHPDoc type annotation to match the testable auth class. |
| Presenters/CalendarSubscriptionPresenter.php | Uses feed-scoped session when provided (fallback to server session otherwise). |
| Pages/Export/SubscriptionPage.php | Adds notFound flag, feed session storage, Basic Auth gate, and a shared PageLoad() template. |
| Pages/Export/ICalendarSubscriptionPage.php | Extends the page contract with SetIsNotFound() and GetFeedUserSession(). |
| Pages/Export/CalendarSubscriptionPage.php | Converts to renderFeed() implementation (rendering only). |
| Pages/Export/AtomSubscriptionPage.php | Converts to renderFeed() implementation (rendering only). |
| lib/Config/ConfigKeys.php | Adds new config key definition for enabling ICS Basic Auth. |
| lib/Application/Authentication/WebAuthentication.php | Adds/implements LoginForFeed() on IWebAuthentication. |
| lib/Application/Authentication/IAuthentication.php | Adds BuildSession() to build a session without recording a login event. |
| lib/Application/Authentication/Authentication.php | Implements BuildSession() and makes GetUserSession() extensible (protected). |
| config/config.dist.php | Adds ics[basic.auth] default config entry. |
| .env.example | Adds LB_ICS_BASIC_AUTH env var example. |
| /** | ||
| * Template method enforcing the shared request lifecycle: | ||
| * 1. tryBasicAuth() — fail-closed gates + optional Basic Auth (sets 404/401). | ||
| * 2. presenter->PageLoad() — validator + reservation fetch. | ||
| * 3. If still valid, renderFeed() emits the format-specific output. | ||
| * | ||
| * Subclasses must NOT override PageLoad() — they implement renderFeed() | ||
| * instead so the gate boilerplate cannot be bypassed by accident. | ||
| */ | ||
| public function PageLoad() | ||
| { | ||
| $this->tryBasicAuth(); | ||
| if ($this->notFound) { | ||
| return; | ||
| } | ||
|
|
||
| $this->presenter->PageLoad(); | ||
| if ($this->notFound) { | ||
| http_response_code(404); | ||
| return; | ||
| } | ||
|
|
||
| $this->renderFeed(); | ||
| } |
| if (empty($this->GetSubscriptionKey())) { | ||
| http_response_code(404); | ||
| $this->notFound = true; | ||
| return; | ||
| } | ||
|
|
||
| if (!Configuration::Instance()->GetKey(ConfigKeys::ICS_BASIC_AUTH, new BooleanConverter())) { | ||
| return; | ||
| } | ||
|
|
||
| $username = $_SERVER['PHP_AUTH_USER'] ?? null; | ||
| $password = $_SERVER['PHP_AUTH_PW'] ?? null; | ||
|
|
||
| if ($username === null || $password === null) { | ||
| http_response_code(401); | ||
| header('WWW-Authenticate: Basic realm="LibreBooking"'); | ||
| $this->notFound = true; | ||
| return; | ||
| } | ||
|
|
||
| $authentication = $this->createWebAuthentication(); | ||
|
|
||
| if ($authentication->Validate($username, $password)) { | ||
| $this->feedUserSession = $authentication->LoginForFeed($username); | ||
| } else { | ||
| http_response_code(401); | ||
| header('WWW-Authenticate: Basic realm="LibreBooking"'); | ||
| $this->notFound = true; | ||
| } |
| * Abstract base class for calendar subscription feed pages (ICS, Atom, etc.). | ||
| * | ||
| * Provides: | ||
| * - Shared presenter/service constructor wiring | ||
| * - Common QueryString accessors used by CalendarSubscriptionValidator and the presenter | ||
| * - A deferred $notFound flag with a unified SetIsNotFound() implementation | ||
| * - Optional HTTP Basic Authentication via tryBasicAuth() | ||
| * | ||
| * Subclasses must implement PageLoad() to render the specific feed format. | ||
| */ |
| public const ICS_BASIC_AUTH = [ | ||
| 'key' => 'ics.basic.auth', | ||
| 'type' => 'boolean', | ||
| 'default' => false, | ||
| 'label' => 'ICS Subscription Basic Auth', | ||
| 'description' => 'Allow HTTP Basic Authentication on ICS calendar subscription URLs. When enabled, clients may supply credentials to receive a fully authenticated feed.', | ||
| 'config_file_comment' => 'Enable HTTP Basic Authentication on ICS calendar subscription URLs', | ||
| 'section' => 'ics' | ||
| ]; |
| public function setUp(): void | ||
| { | ||
| parent::setUp(); | ||
| unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']); | ||
| $this->page = new TestableSubscriptionPage(); | ||
| $this->page->SubscriptionKey = 'feed-secret'; | ||
| $this->fakeConfig->SetKey(ConfigKeys::ICS_ENABLED, true); | ||
| } |
Codex GPT-5.5 Medium reviewCode Review: 969783fCommit: RecommendationDo not merge upstream yet. The feature direction is reasonable, but the commit has a regression in the core subscription authorization path and a runtime break for common authentication plugins. FindingsBlocker: Invalid subscription validation now renders a feed instead of returning 404
That means a bad This should be fixed before merge, either by checking the presenter's return value in Blocker: Basic Auth with decorated auth plugins can fatal after successful validation
The commit adds
Those plugins inherit The fix should either make each decorator plugin override/delegate Minor: Commit message names the wrong config keyThe commit message says the feature is controlled by The message should be amended so reviewers and operators are not pointed at a nonexistent setting. |
Claude Sonnet 5 ReviewReview:
|
e0e6d50 to
32279f1
Compare
|
@lucs7 Feel free to fork this branch and then update it and create a new PR if you want. Thanks. |
Introduce an optional Basic Auth gate on the ICS and Atom subscription endpoints, controlled by the new ics.subscription.basic.auth config key When enabled, SubscriptionPage::tryBasicAuth() runs before the presenter: on missing or invalid credentials it sets a 401 with WWW-Authenticate so calendar clients can prompt; on success it establishes a real server-side session via IWebAuthentication::LoginForFeed(). LoginForFeed reuses the normal user-session construction but skips the last-login database write, since feed clients poll on every refresh.
32279f1 to
a7b0d8c
Compare
Introduce an optional Basic Auth gate on the ICS and Atom subscription endpoints, controlled by the new ics.subscription.basic.auth config key
When enabled, SubscriptionPage::tryBasicAuth() runs before the presenter: on missing or invalid credentials it sets a 401 with WWW-Authenticate so calendar clients can prompt; on success it establishes a real server-side session via IWebAuthentication::LoginForFeed(). LoginForFeed reuses the normal user-session construction but skips the last-login database write, since feed clients poll on every refresh.