From 8fb5e77db8204e9719bcd5d516299719c962b104 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Tue, 21 Oct 2025 10:37:54 +0200 Subject: [PATCH] fix(cors): Allow Bearer token authentication for CORS requests Bearer token authentication with OAuth 2.0/OIDC currently fails for app-specific APIs (Notes, Calendar, Contacts, etc.) with 401 errors, even though it works correctly for OCS APIs. Root cause: - Bearer token validation successfully authenticates the user - A session is created for the authenticated user - CORSMiddleware detects the logged-in session but no CSRF token - CORSMiddleware calls session->logout() to prevent CSRF attacks - The logout invalidates the session, breaking the API request This fix allows Bearer token authentication to bypass the CSRF check and logout logic in CORSMiddleware, as Bearer tokens are stateless and don't require CSRF protection. This aligns with how SecurityMiddleware already handles Bearer tokens for OCS routes (line 234-237). The fix adds a check for the Authorization: Bearer header before the CSRF and app_api checks, allowing Bearer-authenticated requests to proceed without triggering session logout. This enables proper Bearer token authentication for all Nextcloud APIs including app-specific APIs that use the #[CORS] attribute. Related: https://github.com/nextcloud/server/issues/44365 Related: https://github.com/nextcloud/user_oidc/issues/836 Signed-off-by: Chris Coutinho --- .../Middleware/Security/CORSMiddleware.php | 7 ++++ .../Security/CORSMiddlewareTest.php | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php index 4453f5a7d4b4f..f1ca9b48d2112 100644 --- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php @@ -73,6 +73,13 @@ public function beforeController($controller, $methodName) { $user = array_key_exists('PHP_AUTH_USER', $this->request->server) ? $this->request->server['PHP_AUTH_USER'] : null; $pass = array_key_exists('PHP_AUTH_PW', $this->request->server) ? $this->request->server['PHP_AUTH_PW'] : null; + // Allow Bearer token authentication for CORS requests + // Bearer tokens are stateless and don't require CSRF protection + $authorizationHeader = $this->request->getHeader('Authorization'); + if (!empty($authorizationHeader) && str_starts_with($authorizationHeader, 'Bearer ')) { + return; + } + // Allow to use the current session if a CSRF token is provided if ($this->request->passesCSRFCheck()) { return; diff --git a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php index c325ae638fb96..0dad23e92ec91 100644 --- a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php @@ -341,4 +341,37 @@ public function testAfterExceptionWithRegularException(): void { $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); $middleware->afterException($this->controller, __FUNCTION__, new \Exception('A regular exception')); } + + public static function dataCORSShouldAllowBearerAuth(): array { + return [ + ['testCORSShouldNeverAllowCookieAuth'], + ['testCORSShouldNeverAllowCookieAuthAttribute'], + ['testCORSAttributeShouldNeverAllowCookieAuth'], + ['testCORSAttributeShouldNeverAllowCookieAuthAttribute'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldAllowBearerAuth')] + public function testCORSShouldAllowBearerAuth(string $method): void { + $request = new Request( + [ + 'server' => [ + 'HTTP_AUTHORIZATION' => 'Bearer test-token-123' + ] + ], + $this->createMock(IRequestId::class), + $this->createMock(IConfig::class) + ); + $this->reflector->reflect($this->controller, $method); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); + $this->session->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->session->expects($this->never()) + ->method('logout'); + $this->session->expects($this->never()) + ->method('logClientIn'); + + $middleware->beforeController($this->controller, $method); + } }