Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 30 additions & 10 deletions Events.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace humhub\modules\rest;

use humhub\components\api\AuthMethodsEvent;
use humhub\components\Event;
use humhub\modules\activity\models\Activity;
use humhub\modules\comment\models\Comment;
Expand All @@ -17,6 +18,7 @@
use humhub\modules\like\models\Like;
use humhub\modules\notification\models\Notification;
use humhub\modules\post\models\Post;
use humhub\modules\rest\components\auth\AuthMethods;
use humhub\modules\rest\definitions\ActivityDefinitions;
use humhub\modules\rest\definitions\CommentDefinitions;
use humhub\modules\rest\definitions\FileDefinitions;
Expand All @@ -41,6 +43,21 @@ class Events
{
public static function onBeforeRequest($event)
{
// The bare `/rest/...` URL space is reserved for the admin config page and an
// explicit catch-all. These rules must be registered for EVERY request (not only
// `api/` requests): otherwise a bare `/rest/<controller>/<action>` URL falls through
// to Yii's default routing and resolves straight to a REST controller action — an
// unconstrained, CSRF-exempt plain GET (the same reasoning behind core's own
// off-prefix guard, see the defence-in-depth check in BaseController::beforeAction()).
Yii::$app->urlManager->addRules([

// API Config
['pattern' => 'rest/admin/index', 'route' => 'rest/admin', 'verb' => ['POST', 'GET']],

// Catch all to ensure verbs
['pattern' => 'rest/<tmpParam:.*>', 'route' => 'rest/error/notfound'],

], true);

// Only prepare if API request
if (!str_starts_with(Yii::$app->request->pathInfo, 'api/')) {
Expand Down Expand Up @@ -166,19 +183,22 @@ public static function onBeforeRequest($event)

]);

Yii::$app->urlManager->addRules([

// API Config
['pattern' => 'rest/admin/index', 'route' => 'rest/admin', 'verb' => ['POST', 'GET']],

// Catch all to ensure verbs
['pattern' => 'rest/<tmpParam:.*>', 'route' => 'rest/error/notfound'],

], true);

Event::trigger(Module::class, Module::EVENT_REST_API_ADD_RULES);
}

/**
* Contributes this module's authentication methods to every API controller of the
* platform, so the core endpoints (`/api/v2`) can be called with a token too - see
* {@see AuthMethods} for the list and `docs/api-stack.md` for the model. Core appends its own
* session authentication after them, keeping the "token wins" ordering intact.
*
* @since 0.13
*/
public static function onCollectApiAuthMethods(AuthMethodsEvent $event): void
{
$event->authMethods = array_merge($event->authMethods, AuthMethods::collect());
}

private static function addModuleNotFoundRoutes($moduleId)
{
/* @var Module $module */
Expand Down
63 changes: 20 additions & 43 deletions components/BaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,18 @@
use humhub\components\access\ControllerAccess;
use humhub\components\Controller;
use humhub\modules\content\models\Content;
use humhub\modules\rest\components\auth\ImpersonateAuth;
use humhub\modules\rest\components\auth\AuthMethods;
use humhub\modules\rest\components\behaviors\LanguagePickerBehavior;
use humhub\modules\rest\components\User as UserComponent;
use humhub\modules\rest\components\auth\JwtAuth;
use humhub\modules\rest\controllers\auth\AuthController;
use humhub\modules\rest\models\ConfigureForm;
use humhub\modules\rest\Module;
use humhub\modules\user\models\User;
use Yii;
use yii\data\Pagination;
use yii\db\ActiveQuery;
use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\auth\QueryParamAuth;
use yii\helpers\ArrayHelper;
use yii\web\JsonParser;
use yii\web\NotFoundHttpException;

/**
* Class BaseController
Expand Down Expand Up @@ -58,30 +54,10 @@ public function behaviors()
return ArrayHelper::merge([
'authenticator' => [
'class' => CompositeAuth::class,
'authMethods' => ArrayHelper::merge(
ConfigureForm::getInstance()->enableJwtAuth ? [[
'class' => JwtAuth::class,
]] : [],
ConfigureForm::getInstance()->enableBearerAuth ? [[
'class' => HttpBearerAuth::class,
]] : [],
ConfigureForm::getInstance()->enableBearerAuth && ConfigureForm::getInstance()->enableQueryParamAuth ? [[
'class' => QueryParamAuth::class,
]] : [],
ConfigureForm::getInstance()->enableBasicAuth ? [[
'class' => HttpBasicAuth::class,
'auth' => function ($username, $password) {
if (($identity = AuthController::authByUserAndPassword($username, $password)) && $this->isUserEnabled($identity)) {
return $identity;
}

return null;
},
]] : [],
[[
'class' => ImpersonateAuth::class,
]],
),
// The same methods this module contributes to core's API controllers, see
// {@see AuthMethods}. `/api/v1` is token-only: browser-session
// authentication is a core opt-in, per controller.
'authMethods' => AuthMethods::collect(),
],
'languagePicker' => [
'class' => LanguagePickerBehavior::class,
Expand All @@ -94,9 +70,21 @@ public function behaviors()
*/
public function beforeAction($action)
{
// Defence in depth: hard-fail any request that reached a REST controller off the API
// URL rules — i.e. whose path is not under the API prefix (a bare `/rest/<controller>/
// <action>` URL). Together with the `rest/<tmpParam>` catch-all in
// Events::onBeforeRequest() this guarantees a mutating action can never be executed
// off-rule as an unconstrained, CSRF-exempt plain request. Must run before auth.
if (!str_starts_with(Yii::$app->request->pathInfo, Module::API_URL_PREFIX)) {
Yii::$app->response->format = 'json';
throw new NotFoundHttpException();
}

Yii::$app->set('user', [
'class' => UserComponent::class,
'identityClass' => User::class,
// Always session-less: token logins (`yii\web\User::login()`) must never write
// into the browser session.
'enableSession' => false,
]);

Expand Down Expand Up @@ -129,18 +117,7 @@ public function actionNotSupported()
*/
public function isUserEnabled(User $user)
{
$config = new ConfigureForm();
$config->loadSettings();

if (!empty($config->enabledForAllUsers)) {
return true;
}

if (in_array($user->guid, (array)$config->enabledUsers)) {
return true;
}

return false;
return AuthMethods::isUserEnabled($user);
}


Expand Down
91 changes: 91 additions & 0 deletions components/auth/AuthMethods.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2026 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/

namespace humhub\modules\rest\components\auth;

use humhub\modules\rest\controllers\auth\AuthController;
use humhub\modules\rest\models\ConfigureForm;
use humhub\modules\user\models\User;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\auth\QueryParamAuth;
use yii\helpers\ArrayHelper;

/**
* The machine authentication methods this module provides, in one place.
*
* Two consumers:
*
* - this module's own controllers ({@see \humhub\modules\rest\components\BaseController}),
* serving `/api/v1`,
* - every API controller of the platform, through the core collect event (see
* {@see \humhub\modules\rest\Events::onCollectApiAuthMethods()}). An installation with
* this module can therefore call the core endpoints with a token too; a core-only
* installation has browser-session authentication and nothing else.
*
* Browser-session authentication is deliberately NOT part of this list: it lives in core
* ({@see \humhub\components\api\SessionAuth}) and each API controller opts in to it
* individually. See `docs/api-stack.md` and core's `docs/develop/concept-api.md`.
*
* @since 0.13
*/
class AuthMethods
{
/**
* Method configurations for {@see \yii\filters\auth\CompositeAuth::$authMethods},
* according to the module's current settings.
*
* Order matters only among themselves (first match wins, `null` falls through to the
* next); core appends session authentication after all of them.
*/
public static function collect(): array
{
$config = ConfigureForm::getInstance();

return ArrayHelper::merge(
$config->enableJwtAuth ? [[
'class' => JwtAuth::class,
]] : [],
$config->enableBearerAuth ? [[
'class' => HttpBearerAuth::class,
]] : [],
$config->enableBearerAuth && $config->enableQueryParamAuth ? [[
'class' => QueryParamAuth::class,
]] : [],
$config->enableBasicAuth ? [[
'class' => HttpBasicAuth::class,
'auth' => function ($username, $password) {
if (($identity = AuthController::authByUserAndPassword($username, $password)) && static::isUserEnabled($identity)) {
return $identity;
}

return null;
},
]] : [],
[[
'class' => ImpersonateAuth::class,
]],
);
}

/**
* Whether the given user may use the API at all - the "Enabled for all registered users"
* setting and the user allowlist below it, which per its own admin hint applies to the
* JWT and HTTP Basic methods only.
*/
public static function isUserEnabled(User $user): bool
{
$config = ConfigureForm::getInstance();

if (!empty($config->enabledForAllUsers)) {
return true;
}

return in_array($user->guid, (array)$config->enabledUsers);
}
}
9 changes: 7 additions & 2 deletions components/auth/ImpersonateAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
namespace humhub\modules\rest\components\auth;

use humhub\modules\rest\models\ImpersonateAuthToken;
use Yii;
use yii\db\Expression;
use yii\filters\auth\HttpBearerAuth;
use yii\helpers\StringHelper;
Expand Down Expand Up @@ -41,8 +40,14 @@ public function authenticate($user, $request, $response)
->one();

if ($accessToken && ($identity = $accessToken->user)) {
// Note: up to HumHub 1.18 this additionally set `Yii::$app->user->isImpersonated`,
// a flag of the `Impersonator` user-component behavior. HumHub 1.19 replaced that
// behavior with the session-bound `Impersonation` component
// (`Yii::$app->user->impersonation`, core #8372), so the flag no longer exists —
// and the API user component is deliberately session-less, so there is no session
// state to mark either. Applying 1.19's impersonation restrictions (hidden private
// content) to impersonate-token requests is a separate follow-up.
$user->login($identity);
Yii::$app->user->isImpersonated = true;
} else {
$identity = null;
}
Expand Down
4 changes: 4 additions & 0 deletions config.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @license https://www.humhub.com/licences
*/

use humhub\components\api\BaseController;
use humhub\components\Application;

return [
Expand All @@ -14,6 +15,9 @@
'namespace' => 'humhub\modules\rest',
'events' => [
[Application::class, Application::EVENT_BEFORE_REQUEST, ['\humhub\modules\rest\Events', 'onBeforeRequest']],
// Token authentication for the platform's own API controllers (`/api/v2`), see
// Events::onCollectApiAuthMethods()
[BaseController::class, BaseController::EVENT_COLLECT_AUTH_METHODS, ['\humhub\modules\rest\Events', 'onCollectApiAuthMethods']],
['humhub\modules\legal\services\ExportService', 'collectUserData', ['humhub\modules\rest\Events', 'onLegalModuleUserDataExport']],
],
];
9 changes: 9 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Changelog
=========

0.13.0 (Unreleased)
-------------------
- Chg: Raised minimum HumHub version to 1.20 — the module builds on the HTTP API framework of core (`humhub\components\api\`)
- Enh: The module's authentication methods (JWT, Bearer, query param, Basic, Impersonate) now apply to the API endpoints of core 1.20 as well (`/api/v2`), contributed through the core API framework; `/api/v1` stays token-only, browser-session authentication is a core opt-in per endpoint (see `docs/api-stack.md`)
- Chg: The `/api/v2` documentation lives in core (`docs/api/`), not in this module — this module's Swagger sources cover its own `/api/v1` surface again; `build-all.sh` no longer builds a page for the shared-components file
- Enh: Hardened the `/rest/...` URL space — the admin-page and catch-all rules are registered for every request, and a REST controller reached off the `api/v1/` prefix (e.g. through Yii's fallback routing) now fails with a 404 before authentication runs
- Fix: Impersonate token authentication crashed on HumHub 1.19 (`isImpersonated` was removed by the core impersonation refactor, core #8372)
- Chg: Replies are validated to nest at most one level (core `Comment` model rule, surfaced through the validation envelope)

0.12.2 (July 16, 2026)
----------------------
- Chg: Removed the obsolete `twofa.beforeCheck` listener (`Module::ignoreTwofaCheck()`) — the event no longer exists since the twofa module moved to the core user gate system; token-authenticated API requests are not intercepted by the gate, so the REST API keeps working for users with 2FA enabled without any opt-out
Expand Down
14 changes: 12 additions & 2 deletions docs/MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ Following RESTful API endpoints are available.

**Base URL:**

The base url for all APIs is: `https://yourhost/api/v1/`
The base url for the endpoints of this module is: `https://yourhost/api/v1/`

Since HumHub 1.20 the platform itself ships API endpoints under
`https://yourhost/api/v2/`, in modernized conventions (ISO-8601 timestamps, camelCase,
plain HTTP status codes). This module's authentication methods apply to them as well — see
the **v2 APIs** below and `docs/api-stack.md`.

**Language**

Logged-in user's language will be used. Can be overwritten by `Accept-Language` header.


**Core APIs:**
**v1 APIs (this module):**

- [User](https://marketplace.humhub.com/module/rest/docs/html/user.html)
- [Content](https://marketplace.humhub.com/module/rest/docs/html/content.html)
Expand All @@ -27,6 +32,11 @@ Logged-in user's language will be used. Can be overwritten by `Accept-Language`
- [Space](https://marketplace.humhub.com/module/rest/docs/html/space.html)
- [Content Topics](https://marketplace.humhub.com/module/rest/docs/html/topic.html)

**v2 APIs (endpoints shipped by HumHub core, 1.20+):**

Documented by core itself and served by every installation at `/docs/api/` — this module only
contributes the token authentication those endpoints accept.

**Module APIs**

- [Calendar](https://marketplace.humhub.com/module/calendar/docs/swagger/calendar.html)
Expand Down
Loading
Loading