diff --git a/framework/core/CHANGELOG.md b/framework/core/CHANGELOG.md index 6d418f15a..e041967c8 100644 --- a/framework/core/CHANGELOG.md +++ b/framework/core/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to Flarum and its bundled extensions will be documented in t This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Added +- Improve security by using HTTP-only cookie + CSRF token for API authentication +- Require user to re-enter password after 30 mins when performing sensitive/destructive actions +- Add `onhide` hook to Modal component + ### Fixed - Fix error when sorting discussions by "oldest" (#627) diff --git a/framework/core/js/forum/src/components/ChangeEmailModal.js b/framework/core/js/forum/src/components/ChangeEmailModal.js index 35b430889..eef95577f 100644 --- a/framework/core/js/forum/src/components/ChangeEmailModal.js +++ b/framework/core/js/forum/src/components/ChangeEmailModal.js @@ -81,10 +81,17 @@ export default class ChangeEmailModal extends Modal { return; } + const oldEmail = app.session.user.email(); + this.loading = true; app.session.user.save({email: this.email()}, {errorHandler: this.onerror.bind(this)}) .then(() => this.success = true) .finally(this.loaded.bind(this)); + + // The save method will update the cached email address on the user model... + // But in the case of a "sudo" password prompt, we'll still want to have + // the old email address on file for the purposes of logging in. + app.session.user.pushAttributes({email: oldEmail}); } } diff --git a/framework/core/js/forum/src/components/LogInModal.js b/framework/core/js/forum/src/components/LogInModal.js index a2870936e..8395061f8 100644 --- a/framework/core/js/forum/src/components/LogInModal.js +++ b/framework/core/js/forum/src/components/LogInModal.js @@ -124,8 +124,10 @@ export default class LogInModal extends Modal { const email = this.email(); const password = this.password(); - app.session.login(email, password, {errorHandler: this.onerror.bind(this)}) - .catch(this.loaded.bind(this)); + app.session.login(email, password, {errorHandler: this.onerror.bind(this)}).then( + () => window.location.reload(), + this.loaded.bind(this) + ); } onerror(error) { diff --git a/framework/core/js/forum/src/components/Post.js b/framework/core/js/forum/src/components/Post.js index ba16bf0bf..e760a5c71 100644 --- a/framework/core/js/forum/src/components/Post.js +++ b/framework/core/js/forum/src/components/Post.js @@ -18,6 +18,8 @@ import ItemList from 'flarum/utils/ItemList'; */ export default class Post extends Component { init() { + this.loading = false; + /** * Set up a subtree retainer so that the post will not be redrawn * unless new data comes in. @@ -37,7 +39,7 @@ export default class Post extends Component { view() { const attrs = this.attrs(); - attrs.className = 'Post ' + (attrs.className || ''); + attrs.className = 'Post ' + (this.loading ? 'Post--loading ' : '') + (attrs.className || ''); return (
diff --git a/framework/core/js/forum/src/utils/DiscussionControls.js b/framework/core/js/forum/src/utils/DiscussionControls.js index d62c3e4e5..aa61383d2 100644 --- a/framework/core/js/forum/src/utils/DiscussionControls.js +++ b/framework/core/js/forum/src/utils/DiscussionControls.js @@ -217,18 +217,19 @@ export default { */ deleteAction() { if (confirm(extractText(app.translator.trans('core.forum.discussion_controls.delete_confirmation')))) { - // If there is a discussion list in the cache, remove this discussion. - if (app.cache.discussionList) { - app.cache.discussionList.removeDiscussion(this); - } - // If we're currently viewing the discussion that was deleted, go back // to the previous page. if (app.viewingDiscussion(this)) { app.history.back(); } - return this.delete(); + return this.delete().then(() => { + // If there is a discussion list in the cache, remove this discussion. + if (app.cache.discussionList) { + app.cache.discussionList.removeDiscussion(this); + m.redraw(); + } + }); } }, diff --git a/framework/core/js/forum/src/utils/PostControls.js b/framework/core/js/forum/src/utils/PostControls.js index afe6a1ab3..ff6b79d1a 100644 --- a/framework/core/js/forum/src/utils/PostControls.js +++ b/framework/core/js/forum/src/utils/PostControls.js @@ -78,7 +78,7 @@ export default { * @return {ItemList} * @protected */ - destructiveControls(post) { + destructiveControls(post, context) { const items = new ItemList(); if (post.contentType() === 'comment' && !post.isHidden()) { @@ -101,7 +101,7 @@ export default { items.add('delete', Button.component({ icon: 'times', children: app.translator.trans('core.forum.post_controls.delete_forever_button'), - onclick: this.deleteAction.bind(post) + onclick: this.deleteAction.bind(post, context) })); } } @@ -144,9 +144,14 @@ export default { * * @return {Promise} */ - deleteAction() { - this.discussion().removePost(this.id()); + deleteAction(context) { + if (context) context.loading = true; - return this.delete(); + return this.delete().then(() => { + this.discussion().removePost(this.id()); + }).finally(() => { + if (context) context.loading = false; + m.redraw(); + }); } }; diff --git a/framework/core/js/lib/App.js b/framework/core/js/lib/App.js index 03b74f3f0..5fb5fc68c 100644 --- a/framework/core/js/lib/App.js +++ b/framework/core/js/lib/App.js @@ -2,6 +2,7 @@ import ItemList from 'flarum/utils/ItemList'; import Alert from 'flarum/components/Alert'; import Button from 'flarum/components/Button'; import RequestErrorModal from 'flarum/components/RequestErrorModal'; +import ConfirmPasswordModal from 'flarum/components/ConfirmPasswordModal'; import Translator from 'flarum/Translator'; import extract from 'flarum/utils/extract'; import patchMithril from 'flarum/utils/patchMithril'; @@ -182,14 +183,17 @@ export default class App { * @return {Promise} * @public */ - request(options) { + request(originalOptions) { + const options = Object.assign({}, originalOptions); + // Set some default options if they haven't been overridden. We want to // authenticate all requests with the session token. We also want all // requests to run asynchronously in the background, so that they don't // prevent redraws from occurring. - options.config = options.config || this.session.authorize.bind(this.session); options.background = options.background || true; + extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-CSRF-Token', this.session.csrfToken)); + // If the method is something like PATCH or DELETE, which not all servers // support, then we'll send it as a POST request with a the intended method // specified in the X-Fake-Http-Method header. @@ -218,7 +222,7 @@ export default class App { if (original) { responseText = original(xhr.responseText); } else { - responseText = xhr.responseText.length > 0 ? xhr.responseText : null; + responseText = xhr.responseText || null; } const status = xhr.status; @@ -227,6 +231,11 @@ export default class App { throw new RequestError(status, responseText, options, xhr); } + if (xhr.getResponseHeader) { + const csrfToken = xhr.getResponseHeader('X-CSRF-Token'); + if (csrfToken) app.session.csrfToken = csrfToken; + } + try { return JSON.parse(responseText); } catch (e) { @@ -238,9 +247,20 @@ export default class App { // Now make the request. If it's a failure, inspect the error that was // returned and show an alert containing its contents. - return m.request(options).then(null, error => { + const deferred = m.deferred(); + + m.request(options).then(response => deferred.resolve(response), error => { this.requestError = error; + if (error.response && error.response.errors && error.response.errors[0] && error.response.errors[0].code === 'invalid_access_token') { + this.modal.show(new ConfirmPasswordModal({ + deferredRequest: originalOptions, + deferred, + error + })); + return; + } + let children; switch (error.status) { @@ -283,8 +303,10 @@ export default class App { this.alerts.show(error.alert); } - throw error; + deferred.reject(error); }); + + return deferred.promise; } /** diff --git a/framework/core/js/lib/Model.js b/framework/core/js/lib/Model.js index ad16ceb9b..3a6ba4053 100644 --- a/framework/core/js/lib/Model.js +++ b/framework/core/js/lib/Model.js @@ -150,7 +150,7 @@ export default class Model { // Before we update the model's data, we should make a copy of the model's // old data so that we can revert back to it if something goes awry during // persistence. - const oldData = JSON.parse(JSON.stringify(this.data)); + const oldData = this.copyData(); this.pushData(data); @@ -209,6 +209,10 @@ export default class Model { return '/' + this.data.type + (this.exists ? '/' + this.data.id : ''); } + copyData() { + return JSON.parse(JSON.stringify(this.data)); + } + /** * Generate a function which returns the value of the given attribute. * diff --git a/framework/core/js/lib/Session.js b/framework/core/js/lib/Session.js index 5053c955d..19f900fda 100644 --- a/framework/core/js/lib/Session.js +++ b/framework/core/js/lib/Session.js @@ -3,7 +3,7 @@ * to the current authenticated user, and provides methods to log in/out. */ export default class Session { - constructor(token, user) { + constructor(user, csrfToken) { /** * The current authenticated user. * @@ -13,12 +13,12 @@ export default class Session { this.user = user; /** - * The token that was used for authentication. + * The CSRF token. * * @type {String|null} * @public */ - this.token = token; + this.csrfToken = csrfToken; } /** @@ -35,8 +35,7 @@ export default class Session { method: 'POST', url: app.forum.attribute('baseUrl') + '/login', data: {identification, password} - }, options)) - .then(() => window.location.reload()); + }, options)); } /** @@ -45,19 +44,6 @@ export default class Session { * @public */ logout() { - window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token; - } - - /** - * Apply an authorization header with the current token to the given - * XMLHttpRequest object. - * - * @param {XMLHttpRequest} xhr - * @public - */ - authorize(xhr) { - if (this.token) { - xhr.setRequestHeader('Authorization', 'Token ' + this.token); - } + window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.csrfToken; } } diff --git a/framework/core/js/lib/components/ConfirmPasswordModal.js b/framework/core/js/lib/components/ConfirmPasswordModal.js new file mode 100644 index 000000000..31ac07b91 --- /dev/null +++ b/framework/core/js/lib/components/ConfirmPasswordModal.js @@ -0,0 +1,73 @@ +import Modal from 'flarum/components/Modal'; +import Button from 'flarum/components/Button'; +import extractText from 'flarum/utils/extractText'; + +export default class ConfirmPasswordModal extends Modal { + init() { + super.init(); + + this.password = m.prop(''); + } + + className() { + return 'ConfirmPasswordModal Modal--small'; + } + + title() { + return app.translator.trans('core.forum.confirm_password.title'); + } + + content() { + return ( +
+
+
+ +
+ +
+ +
+
+
+ ); + } + + onsubmit(e) { + e.preventDefault(); + + this.loading = true; + + app.session.login(app.session.user.email(), this.password(), {errorHandler: this.onerror.bind(this)}) + .then(() => { + this.success = true; + this.hide(); + app.request(this.props.deferredRequest).then(response => this.props.deferred.resolve(response), response => this.props.deferred.reject(response)); + }) + .catch(this.loaded.bind(this)); + } + + onerror(error) { + if (error.status === 401) { + error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message'); + } + + super.onerror(error); + } + + onhide() { + if (this.success) return; + + this.props.deferred.reject(this.props.error); + } +} diff --git a/framework/core/js/lib/components/Modal.js b/framework/core/js/lib/components/Modal.js index 6f76bdf40..593085ad3 100644 --- a/framework/core/js/lib/components/Modal.js +++ b/framework/core/js/lib/components/Modal.js @@ -98,7 +98,10 @@ export default class Modal extends Component { * Focus on the first input when the modal is ready to be used. */ onready() { - this.$('form :input:first').focus().select(); + this.$('form').find('input, select, textarea').first().focus().select(); + } + + onhide() { } /** diff --git a/framework/core/js/lib/components/ModalManager.js b/framework/core/js/lib/components/ModalManager.js index 35eb5a445..6277b23ba 100644 --- a/framework/core/js/lib/components/ModalManager.js +++ b/framework/core/js/lib/components/ModalManager.js @@ -77,6 +77,10 @@ export default class ModalManager extends Component { * @protected */ clear() { + if (this.component) { + this.component.onhide(); + } + this.component = null; m.lazyRedraw(); diff --git a/framework/core/js/lib/initializers/preload.js b/framework/core/js/lib/initializers/preload.js index b170ac985..31d2d814f 100644 --- a/framework/core/js/lib/initializers/preload.js +++ b/framework/core/js/lib/initializers/preload.js @@ -18,7 +18,7 @@ export default function preload(app) { app.forum = app.store.getById('forums', 1); app.session = new Session( - app.preload.session.token, - app.store.getById('users', app.preload.session.userId) + app.store.getById('users', app.preload.session.userId), + app.preload.session.csrfToken ); } diff --git a/framework/core/less/forum/Post.less b/framework/core/less/forum/Post.less index da5fcb6f8..85f354a61 100644 --- a/framework/core/less/forum/Post.less +++ b/framework/core/less/forum/Post.less @@ -167,6 +167,9 @@ color: @muted-more-color; } } +.Post--loading { + opacity: 0.5; +} .PostMeta { display: inline; } diff --git a/framework/core/migrations/2015_12_03_010529_drop_access_tokens_table.php b/framework/core/migrations/2015_12_03_010529_drop_access_tokens_table.php new file mode 100644 index 000000000..d2739dda4 --- /dev/null +++ b/framework/core/migrations/2015_12_03_010529_drop_access_tokens_table.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Core\Migration; + +use Flarum\Database\AbstractMigration; +use Illuminate\Database\Schema\Blueprint; + +class DropAccessTokensTable extends AbstractMigration +{ + public function up() + { + $this->schema->drop('access_tokens'); + } + + public function down() + { + $this->schema->create('access_tokens', function (Blueprint $table) { + $table->string('id', 100)->primary(); + $table->integer('user_id')->unsigned(); + $table->timestamp('created_at'); + $table->timestamp('expires_at'); + }); + } +} diff --git a/framework/core/migrations/2015_12_03_010610_create_sessions_table.php b/framework/core/migrations/2015_12_03_010610_create_sessions_table.php new file mode 100644 index 000000000..9c37e07ba --- /dev/null +++ b/framework/core/migrations/2015_12_03_010610_create_sessions_table.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Core\Migration; + +use Flarum\Database\AbstractMigration; +use Illuminate\Database\Schema\Blueprint; + +class CreateSessionsTable extends AbstractMigration +{ + public function up() + { + $this->schema->create('sessions', function (Blueprint $table) { + $table->string('id', 40)->primary(); + $table->integer('user_id')->unsigned()->nullable(); + $table->string('csrf_token', 40); + $table->integer('last_activity'); + $table->integer('duration'); + $table->dateTime('sudo_expiry_time'); + }); + } + + public function down() + { + $this->schema->drop('sessions'); + } +} diff --git a/framework/core/src/Admin/AdminServiceProvider.php b/framework/core/src/Admin/AdminServiceProvider.php index 114c0d7cc..ea41ef82b 100644 --- a/framework/core/src/Admin/AdminServiceProvider.php +++ b/framework/core/src/Admin/AdminServiceProvider.php @@ -42,6 +42,8 @@ class AdminServiceProvider extends AbstractServiceProvider */ public function boot() { + $this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin'); + $this->flushAssetsWhenThemeChanged(); $this->flushAssetsWhenExtensionsChanged(); diff --git a/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php b/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php index db91ec3c4..ac0268f87 100644 --- a/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php +++ b/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php @@ -10,26 +10,39 @@ namespace Flarum\Admin\Middleware; -use Flarum\Core\Access\Gate; +use Exception; +use Flarum\Core\Access\AssertPermissionTrait; +use Flarum\Forum\Controller\LogInController; use Illuminate\Contracts\Container\Container; +use Illuminate\Contracts\View\Factory; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -use Flarum\Core\Exception\PermissionDeniedException; +use Zend\Diactoros\Response\HtmlResponse; +use Zend\Diactoros\Response\RedirectResponse; use Zend\Stratigility\MiddlewareInterface; class RequireAdministrateAbility implements MiddlewareInterface { - /** - * @var Gate - */ - protected $gate; + use AssertPermissionTrait; /** - * @param Gate $gate + * @var LogInController */ - public function __construct(Gate $gate) + private $logInController; + + /** + * @var Factory + */ + private $view; + + /** + * @param LogInController $logInController + * @param Factory $view + */ + public function __construct(LogInController $logInController, Factory $view) { - $this->gate = $gate; + $this->logInController = $logInController; + $this->view = $view; } /** @@ -37,10 +50,24 @@ class RequireAdministrateAbility implements MiddlewareInterface */ public function __invoke(Request $request, Response $response, callable $out = null) { - $actor = $request->getAttribute('actor'); + try { + $this->assertAdminAndSudo($request); + } catch (Exception $e) { + if ($request->getMethod() === 'POST') { + $response = $this->logInController->handle($request); - if (! $this->gate->forUser($actor)->allows('administrate')) { - throw new PermissionDeniedException; + if ($response->getStatusCode() === 200) { + return $response + ->withStatus(302) + ->withHeader('location', app('Flarum\Admin\UrlGenerator')->toRoute('index')); + } + } + + return new HtmlResponse( + $this->view->make('flarum.admin::login') + ->with('token', $request->getAttribute('session')->csrf_token) + ->render() + ); } return $out ? $out($request, $response) : $response; diff --git a/framework/core/src/Admin/Server.php b/framework/core/src/Admin/Server.php index 13469292c..022924060 100644 --- a/framework/core/src/Admin/Server.php +++ b/framework/core/src/Admin/Server.php @@ -13,6 +13,7 @@ namespace Flarum\Admin; use Flarum\Foundation\Application; use Flarum\Http\AbstractServer; +use Zend\Diactoros\Response\HtmlResponse; use Zend\Stratigility\MiddlewarePipe; use Flarum\Http\Middleware\HandleErrors; @@ -30,8 +31,10 @@ class Server extends AbstractServer $errorDir = __DIR__ . '/../../error'; if ($app->isUpToDate()) { - $pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie')); $pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\ParseJsonBody')); + $pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\AuthorizeWithCookie')); + $pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\StartSession')); + $pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\SetLocale')); $pipe->pipe($adminPath, $app->make('Flarum\Admin\Middleware\RequireAdministrateAbility')); $pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.admin.routes')])); $pipe->pipe($adminPath, new HandleErrors($errorDir, $app->inDebugMode())); diff --git a/framework/core/src/Api/AccessToken.php b/framework/core/src/Api/AccessToken.php deleted file mode 100644 index eac23459a..000000000 --- a/framework/core/src/Api/AccessToken.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Api; - -use Flarum\Database\AbstractModel; -use DateTime; - -/** - * @property string $id - * @property int $user_id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $expires_at - * @property \Flarum\Core\User|null $user - */ -class AccessToken extends AbstractModel -{ - /** - * {@inheritdoc} - */ - protected $table = 'access_tokens'; - - /** - * Use a custom primary key for this model. - * - * @var bool - */ - public $incrementing = false; - - /** - * {@inheritdoc} - */ - protected $dates = ['created_at', 'expires_at']; - - /** - * Generate an access token for the specified user. - * - * @param int $userId - * @param int $minutes - * @return static - */ - public static function generate($userId, $minutes = 60) - { - $token = new static; - - $token->id = str_random(40); - $token->user_id = $userId; - $token->created_at = time(); - $token->expires_at = time() + $minutes * 60; - - return $token; - } - - /** - * Check that the token has not expired. - * - * @return bool - */ - public function isValid() - { - return $this->expires_at > new DateTime; - } - - /** - * Define the relationship with the owner of this access token. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function user() - { - return $this->belongsTo('Flarum\Core\User'); - } -} diff --git a/framework/core/src/Api/ApiServiceProvider.php b/framework/core/src/Api/ApiServiceProvider.php index f8c4ebcd2..0bc8d6827 100644 --- a/framework/core/src/Api/ApiServiceProvider.php +++ b/framework/core/src/Api/ApiServiceProvider.php @@ -44,9 +44,13 @@ class ApiServiceProvider extends AbstractServiceProvider $handler->registerHandler(new Handler\FloodingExceptionHandler); $handler->registerHandler(new Handler\IlluminateValidationExceptionHandler); + $handler->registerHandler(new Handler\InvalidAccessTokenExceptionHandler); $handler->registerHandler(new Handler\InvalidConfirmationTokenExceptionHandler); + $handler->registerHandler(new Handler\MethodNotAllowedExceptionHandler); $handler->registerHandler(new Handler\ModelNotFoundExceptionHandler); $handler->registerHandler(new Handler\PermissionDeniedExceptionHandler); + $handler->registerHandler(new Handler\RouteNotFoundExceptionHandler); + $handler->registerHandler(new Handler\TokenMismatchExceptionHandler); $handler->registerHandler(new Handler\ValidationExceptionHandler); $handler->registerHandler(new InvalidParameterExceptionHandler); $handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode())); diff --git a/framework/core/src/Api/Client.php b/framework/core/src/Api/Client.php index 0a2920172..ea6666f74 100644 --- a/framework/core/src/Api/Client.php +++ b/framework/core/src/Api/Client.php @@ -12,6 +12,7 @@ namespace Flarum\Api; use Flarum\Http\Controller\ControllerInterface; use Flarum\Core\User; +use Flarum\Http\Session; use Illuminate\Contracts\Container\Container; use Exception; use InvalidArgumentException; @@ -43,14 +44,23 @@ class Client * Execute the given API action class, pass the input and return its response. * * @param string|ControllerInterface $controller - * @param User $actor + * @param Session|User|null $session * @param array $queryParams * @param array $body * @return \Psr\Http\Message\ResponseInterface */ - public function send($controller, User $actor, array $queryParams = [], array $body = []) + public function send($controller, $session, array $queryParams = [], array $body = []) { - $request = ServerRequestFactory::fromGlobals(null, $queryParams, $body)->withAttribute('actor', $actor); + $request = ServerRequestFactory::fromGlobals(null, $queryParams, $body); + + if ($session instanceof Session) { + $request = $request->withAttribute('session', $session); + $actor = $session->user; + } else { + $actor = $session; + } + + $request = $request->withAttribute('actor', $actor); if (is_string($controller)) { $controller = $this->container->make($controller); diff --git a/framework/core/src/Api/Command/GenerateAccessToken.php b/framework/core/src/Api/Command/GenerateAccessToken.php deleted file mode 100644 index 10a13ab5b..000000000 --- a/framework/core/src/Api/Command/GenerateAccessToken.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Api\Command; - -class GenerateAccessToken -{ - /** - * The ID of the user to generate an access token for. - * - * @var int - */ - public $userId; - - /** - * @param int $userId The ID of the user to generate an access token for. - */ - public function __construct($userId) - { - $this->userId = $userId; - } -} diff --git a/framework/core/src/Api/Command/GenerateAccessTokenHandler.php b/framework/core/src/Api/Command/GenerateAccessTokenHandler.php deleted file mode 100644 index 1e5668526..000000000 --- a/framework/core/src/Api/Command/GenerateAccessTokenHandler.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Api\Command; - -use Flarum\Api\AccessToken; -use Flarum\Api\Command\GenerateAccessToken; - -class GenerateAccessTokenHandler -{ - /** - * @param GenerateAccessToken $command - * @return AccessToken - */ - public function handle(GenerateAccessToken $command) - { - $token = AccessToken::generate($command->userId); - - $token->save(); - - return $token; - } -} diff --git a/framework/core/src/Api/Controller/DeleteDiscussionController.php b/framework/core/src/Api/Controller/DeleteDiscussionController.php index 06a20c04b..043261f1d 100644 --- a/framework/core/src/Api/Controller/DeleteDiscussionController.php +++ b/framework/core/src/Api/Controller/DeleteDiscussionController.php @@ -10,12 +10,15 @@ namespace Flarum\Api\Controller; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Core\Command\DeleteDiscussion; use Illuminate\Contracts\Bus\Dispatcher; use Psr\Http\Message\ServerRequestInterface; class DeleteDiscussionController extends AbstractDeleteController { + use AssertPermissionTrait; + /** * @var Dispatcher */ @@ -38,6 +41,8 @@ class DeleteDiscussionController extends AbstractDeleteController $actor = $request->getAttribute('actor'); $input = $request->getParsedBody(); + $this->assertSudo($request); + $this->bus->dispatch( new DeleteDiscussion($id, $actor, $input) ); diff --git a/framework/core/src/Api/Controller/DeleteGroupController.php b/framework/core/src/Api/Controller/DeleteGroupController.php index 6f9ab7435..593300fa7 100644 --- a/framework/core/src/Api/Controller/DeleteGroupController.php +++ b/framework/core/src/Api/Controller/DeleteGroupController.php @@ -10,12 +10,15 @@ namespace Flarum\Api\Controller; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Core\Command\DeleteGroup; use Illuminate\Contracts\Bus\Dispatcher; use Psr\Http\Message\ServerRequestInterface; class DeleteGroupController extends AbstractDeleteController { + use AssertPermissionTrait; + /** * @var Dispatcher */ @@ -34,6 +37,8 @@ class DeleteGroupController extends AbstractDeleteController */ protected function delete(ServerRequestInterface $request) { + $this->assertSudo($request); + $this->bus->dispatch( new DeleteGroup(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) ); diff --git a/framework/core/src/Api/Controller/DeletePostController.php b/framework/core/src/Api/Controller/DeletePostController.php index 7e31838f9..b32751a28 100644 --- a/framework/core/src/Api/Controller/DeletePostController.php +++ b/framework/core/src/Api/Controller/DeletePostController.php @@ -10,12 +10,15 @@ namespace Flarum\Api\Controller; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Core\Command\DeletePost; use Illuminate\Contracts\Bus\Dispatcher; use Psr\Http\Message\ServerRequestInterface; class DeletePostController extends AbstractDeleteController { + use AssertPermissionTrait; + /** * @var Dispatcher */ @@ -34,6 +37,8 @@ class DeletePostController extends AbstractDeleteController */ protected function delete(ServerRequestInterface $request) { + $this->assertSudo($request); + $this->bus->dispatch( new DeletePost(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) ); diff --git a/framework/core/src/Api/Controller/DeleteUserController.php b/framework/core/src/Api/Controller/DeleteUserController.php index cb214e531..306e5567a 100644 --- a/framework/core/src/Api/Controller/DeleteUserController.php +++ b/framework/core/src/Api/Controller/DeleteUserController.php @@ -10,12 +10,15 @@ namespace Flarum\Api\Controller; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Core\Command\DeleteUser; use Illuminate\Contracts\Bus\Dispatcher; use Psr\Http\Message\ServerRequestInterface; class DeleteUserController extends AbstractDeleteController { + use AssertPermissionTrait; + /** * @var Dispatcher */ @@ -34,6 +37,8 @@ class DeleteUserController extends AbstractDeleteController */ protected function delete(ServerRequestInterface $request) { + $this->assertSudo($request); + $this->bus->dispatch( new DeleteUser(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) ); diff --git a/framework/core/src/Api/Controller/SetPermissionController.php b/framework/core/src/Api/Controller/SetPermissionController.php index 783157b94..b709f08bb 100644 --- a/framework/core/src/Api/Controller/SetPermissionController.php +++ b/framework/core/src/Api/Controller/SetPermissionController.php @@ -25,7 +25,7 @@ class SetPermissionController implements ControllerInterface */ public function handle(ServerRequestInterface $request) { - $this->assertAdmin($request->getAttribute('actor')); + $this->assertAdminAndSudo($request); $body = $request->getParsedBody(); $permission = array_get($body, 'permission'); diff --git a/framework/core/src/Api/Controller/SetSettingsController.php b/framework/core/src/Api/Controller/SetSettingsController.php index 3d5289439..418184b4e 100644 --- a/framework/core/src/Api/Controller/SetSettingsController.php +++ b/framework/core/src/Api/Controller/SetSettingsController.php @@ -47,7 +47,7 @@ class SetSettingsController implements ControllerInterface */ public function handle(ServerRequestInterface $request) { - $this->assertAdmin($request->getAttribute('actor')); + $this->assertAdminAndSudo($request); $settings = $request->getParsedBody(); diff --git a/framework/core/src/Api/Controller/TokenController.php b/framework/core/src/Api/Controller/TokenController.php index 074709f90..38279dfcc 100644 --- a/framework/core/src/Api/Controller/TokenController.php +++ b/framework/core/src/Api/Controller/TokenController.php @@ -10,11 +10,10 @@ namespace Flarum\Api\Controller; -use Flarum\Api\Command\GenerateAccessToken; use Flarum\Core\Exception\PermissionDeniedException; use Flarum\Core\Repository\UserRepository; -use Flarum\Event\UserEmailChangeWasRequested; use Flarum\Http\Controller\ControllerInterface; +use Flarum\Http\Session; use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher; use Illuminate\Contracts\Events\Dispatcher as EventDispatcher; use Psr\Http\Message\ServerRequestInterface; @@ -65,19 +64,13 @@ class TokenController implements ControllerInterface throw new PermissionDeniedException; } - if (! $user->is_activated) { - $this->events->fire(new UserEmailChangeWasRequested($user, $user->email)); + $session = $request->getAttribute('session') ?: Session::generate($user); + $session->assign($user)->regenerateId()->renew()->save(); - return new JsonResponse(['emailConfirmationRequired' => $user->email], 401); - } - - $token = $this->bus->dispatch( - new GenerateAccessToken($user->id) - ); - - return new JsonResponse([ - 'token' => $token->id, + return (new JsonResponse([ + 'token' => $session->id, 'userId' => $user->id - ]); + ])) + ->withHeader('X-CSRF-Token', $session->csrf_token); } } diff --git a/framework/core/src/Api/Controller/UninstallExtensionController.php b/framework/core/src/Api/Controller/UninstallExtensionController.php index 3b7082817..f1cf8113a 100644 --- a/framework/core/src/Api/Controller/UninstallExtensionController.php +++ b/framework/core/src/Api/Controller/UninstallExtensionController.php @@ -33,7 +33,7 @@ class UninstallExtensionController extends AbstractDeleteController protected function delete(ServerRequestInterface $request) { - $this->assertAdmin($request->getAttribute('actor')); + $this->assertAdminAndSudo($request); $name = array_get($request->getQueryParams(), 'name'); diff --git a/framework/core/src/Api/Controller/UpdateExtensionController.php b/framework/core/src/Api/Controller/UpdateExtensionController.php index 3d221f88c..e5227a6f5 100644 --- a/framework/core/src/Api/Controller/UpdateExtensionController.php +++ b/framework/core/src/Api/Controller/UpdateExtensionController.php @@ -38,7 +38,7 @@ class UpdateExtensionController implements ControllerInterface */ public function handle(ServerRequestInterface $request) { - $this->assertAdmin($request->getAttribute('actor')); + $this->assertAdminAndSudo($request); $enabled = array_get($request->getParsedBody(), 'enabled'); $name = array_get($request->getQueryParams(), 'name'); diff --git a/framework/core/src/Api/Controller/UpdateUserController.php b/framework/core/src/Api/Controller/UpdateUserController.php index 601669448..ca675951c 100644 --- a/framework/core/src/Api/Controller/UpdateUserController.php +++ b/framework/core/src/Api/Controller/UpdateUserController.php @@ -10,6 +10,7 @@ namespace Flarum\Api\Controller; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Core\Command\EditUser; use Illuminate\Contracts\Bus\Dispatcher; use Psr\Http\Message\ServerRequestInterface; @@ -17,6 +18,8 @@ use Tobscure\JsonApi\Document; class UpdateUserController extends AbstractResourceController { + use AssertPermissionTrait; + /** * {@inheritdoc} */ @@ -49,6 +52,8 @@ class UpdateUserController extends AbstractResourceController $actor = $request->getAttribute('actor'); $data = array_get($request->getParsedBody(), 'data', []); + $this->assertSudo($request); + return $this->bus->dispatch( new EditUser($id, $actor, $data) ); diff --git a/framework/core/src/Api/Exception/InvalidAccessTokenException.php b/framework/core/src/Api/Exception/InvalidAccessTokenException.php new file mode 100644 index 000000000..340e47b76 --- /dev/null +++ b/framework/core/src/Api/Exception/InvalidAccessTokenException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Api\Exception; + +use Exception; + +class InvalidAccessTokenException extends Exception +{ +} diff --git a/framework/core/src/Api/Handler/FloodingExceptionHandler.php b/framework/core/src/Api/Handler/FloodingExceptionHandler.php index 9f5e8440c..86a419669 100644 --- a/framework/core/src/Api/Handler/FloodingExceptionHandler.php +++ b/framework/core/src/Api/Handler/FloodingExceptionHandler.php @@ -31,7 +31,10 @@ class FloodingExceptionHandler implements ExceptionHandlerInterface public function handle(Exception $e) { $status = 429; - $error = []; + $error = [ + 'status' => (string) $status, + 'code' => 'too_many_requests' + ]; return new ResponseBag($status, [$error]); } diff --git a/framework/core/src/Api/Handler/IlluminateValidationExceptionHandler.php b/framework/core/src/Api/Handler/IlluminateValidationExceptionHandler.php index 743b4ca78..c3b3fe84f 100644 --- a/framework/core/src/Api/Handler/IlluminateValidationExceptionHandler.php +++ b/framework/core/src/Api/Handler/IlluminateValidationExceptionHandler.php @@ -44,8 +44,10 @@ class IlluminateValidationExceptionHandler implements ExceptionHandlerInterface { $errors = array_map(function ($field, $messages) { return [ + 'status' => '422', + 'code' => 'validation_error', 'detail' => implode("\n", $messages), - 'source' => ['pointer' => '/data/attributes/' . $field], + 'source' => ['pointer' => "/data/attributes/$field"] ]; }, array_keys($errors), $errors); diff --git a/framework/core/src/Api/Handler/InvalidAccessTokenExceptionHandler.php b/framework/core/src/Api/Handler/InvalidAccessTokenExceptionHandler.php new file mode 100644 index 000000000..366e57c7c --- /dev/null +++ b/framework/core/src/Api/Handler/InvalidAccessTokenExceptionHandler.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Api\Handler; + +use Exception; +use Flarum\Api\Exception\InvalidAccessTokenException; +use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; +use Tobscure\JsonApi\Exception\Handler\ResponseBag; + +class InvalidAccessTokenExceptionHandler implements ExceptionHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function manages(Exception $e) + { + return $e instanceof InvalidAccessTokenException; + } + + /** + * {@inheritdoc} + */ + public function handle(Exception $e) + { + $status = 401; + $error = [ + 'status' => (string) $status, + 'code' => 'invalid_access_token' + ]; + + return new ResponseBag($status, [$error]); + } +} diff --git a/framework/core/src/Api/Handler/InvalidConfirmationTokenExceptionHandler.php b/framework/core/src/Api/Handler/InvalidConfirmationTokenExceptionHandler.php index 9202e487f..52ff1fab9 100644 --- a/framework/core/src/Api/Handler/InvalidConfirmationTokenExceptionHandler.php +++ b/framework/core/src/Api/Handler/InvalidConfirmationTokenExceptionHandler.php @@ -31,7 +31,10 @@ class InvalidConfirmationTokenExceptionHandler implements ExceptionHandlerInterf public function handle(Exception $e) { $status = 403; - $error = ['code' => 'invalid_confirmation_token']; + $error = [ + 'status' => (string) $status, + 'code' => 'invalid_confirmation_token' + ]; return new ResponseBag($status, [$error]); } diff --git a/framework/core/src/Api/Handler/MethodNotAllowedExceptionHandler.php b/framework/core/src/Api/Handler/MethodNotAllowedExceptionHandler.php new file mode 100644 index 000000000..d4e1b1eff --- /dev/null +++ b/framework/core/src/Api/Handler/MethodNotAllowedExceptionHandler.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Api\Handler; + +use Exception; +use Flarum\Http\Exception\MethodNotAllowedException; +use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; +use Tobscure\JsonApi\Exception\Handler\ResponseBag; + +class MethodNotAllowedExceptionHandler implements ExceptionHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function manages(Exception $e) + { + return $e instanceof MethodNotAllowedException; + } + + /** + * {@inheritdoc} + */ + public function handle(Exception $e) + { + $status = 405; + $error = [ + 'status' => (string) $status, + 'code' => 'method_not_allowed' + ]; + + return new ResponseBag($status, [$error]); + } +} diff --git a/framework/core/src/Api/Handler/ModelNotFoundExceptionHandler.php b/framework/core/src/Api/Handler/ModelNotFoundExceptionHandler.php index 315f71111..ea80d07a7 100644 --- a/framework/core/src/Api/Handler/ModelNotFoundExceptionHandler.php +++ b/framework/core/src/Api/Handler/ModelNotFoundExceptionHandler.php @@ -11,6 +11,7 @@ namespace Flarum\Api\Handler; use Exception; +use Flarum\Http\Exception\RouteNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; use Tobscure\JsonApi\Exception\Handler\ResponseBag; @@ -31,7 +32,10 @@ class ModelNotFoundExceptionHandler implements ExceptionHandlerInterface public function handle(Exception $e) { $status = 404; - $error = []; + $error = [ + 'status' => '404', + 'code' => 'resource_not_found' + ]; return new ResponseBag($status, [$error]); } diff --git a/framework/core/src/Api/Handler/PermissionDeniedExceptionHandler.php b/framework/core/src/Api/Handler/PermissionDeniedExceptionHandler.php index ac689d837..238dc45fd 100644 --- a/framework/core/src/Api/Handler/PermissionDeniedExceptionHandler.php +++ b/framework/core/src/Api/Handler/PermissionDeniedExceptionHandler.php @@ -31,7 +31,10 @@ class PermissionDeniedExceptionHandler implements ExceptionHandlerInterface public function handle(Exception $e) { $status = 401; - $error = []; + $error = [ + 'status' => (string) $status, + 'code' => 'permission_denied' + ]; return new ResponseBag($status, [$error]); } diff --git a/framework/core/src/Api/Handler/RouteNotFoundExceptionHandler.php b/framework/core/src/Api/Handler/RouteNotFoundExceptionHandler.php new file mode 100644 index 000000000..5d27e0b0e --- /dev/null +++ b/framework/core/src/Api/Handler/RouteNotFoundExceptionHandler.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Api\Handler; + +use Exception; +use Flarum\Http\Exception\RouteNotFoundException; +use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; +use Tobscure\JsonApi\Exception\Handler\ResponseBag; + +class RouteNotFoundExceptionHandler implements ExceptionHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function manages(Exception $e) + { + return $e instanceof RouteNotFoundException; + } + + /** + * {@inheritdoc} + */ + public function handle(Exception $e) + { + $status = 404; + $error = [ + 'status' => (string) $status, + 'code' => 'route_not_found' + ]; + + return new ResponseBag($status, [$error]); + } +} diff --git a/framework/core/src/Api/Handler/TokenMismatchExceptionHandler.php b/framework/core/src/Api/Handler/TokenMismatchExceptionHandler.php new file mode 100644 index 000000000..ad82b6aa9 --- /dev/null +++ b/framework/core/src/Api/Handler/TokenMismatchExceptionHandler.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Api\Handler; + +use Exception; +use Flarum\Http\Exception\TokenMismatchException; +use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; +use Tobscure\JsonApi\Exception\Handler\ResponseBag; + +class TokenMismatchExceptionHandler implements ExceptionHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function manages(Exception $e) + { + return $e instanceof TokenMismatchException; + } + + /** + * {@inheritdoc} + */ + public function handle(Exception $e) + { + $status = 400; + $error = [ + 'status' => (string) $status, + 'code' => 'csrf_token_mismatch' + ]; + + return new ResponseBag($status, [$error]); + } +} diff --git a/framework/core/src/Api/Handler/ValidationExceptionHandler.php b/framework/core/src/Api/Handler/ValidationExceptionHandler.php index 4e0ad26e8..7b2ce70d1 100644 --- a/framework/core/src/Api/Handler/ValidationExceptionHandler.php +++ b/framework/core/src/Api/Handler/ValidationExceptionHandler.php @@ -33,10 +33,13 @@ class ValidationExceptionHandler implements ExceptionHandlerInterface $status = 422; $messages = $e->getMessages(); - $errors = array_map(function ($path, $detail) { - $source = ['pointer' => '/data/attributes/' . $path]; - - return compact('source', 'detail'); + $errors = array_map(function ($path, $detail) use ($status) { + return [ + 'status' => (string) $status, + 'code' => 'validation_error', + 'detail' => $detail, + 'source' => ['pointer' => "/data/attributes/$path"] + ]; }, array_keys($messages), $messages); return new ResponseBag($status, $errors); diff --git a/framework/core/src/Api/Middleware/AuthenticateWithHeader.php b/framework/core/src/Api/Middleware/AuthenticateWithHeader.php deleted file mode 100644 index c76d42f4f..000000000 --- a/framework/core/src/Api/Middleware/AuthenticateWithHeader.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Api\Middleware; - -use Flarum\Api\AccessToken; -use Flarum\Api\ApiKey; -use Flarum\Core\Guest; -use Flarum\Core\User; -use Flarum\Locale\LocaleManager; -use Illuminate\Contracts\Container\Container; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; -use Zend\Stratigility\MiddlewareInterface; - -class AuthenticateWithHeader implements MiddlewareInterface -{ - /** - * @var string - */ - protected $prefix = 'Token '; - - /** - * @var LocaleManager - */ - protected $locales; - - /** - * @param LocaleManager $locales - */ - public function __construct(LocaleManager $locales) - { - $this->locales = $locales; - } - - /** - * {@inheritdoc} - */ - public function __invoke(Request $request, Response $response, callable $out = null) - { - $request = $this->logIn($request); - - return $out ? $out($request, $response) : $response; - } - - /** - * @param Request $request - * @return Request - */ - protected function logIn(Request $request) - { - $header = $request->getHeaderLine('authorization'); - - $parts = explode(';', $header); - - $actor = new Guest; - - if (isset($parts[0]) && starts_with($parts[0], $this->prefix)) { - $token = substr($parts[0], strlen($this->prefix)); - - if (($accessToken = AccessToken::find($token)) && $accessToken->isValid()) { - $actor = $accessToken->user; - - $actor->updateLastSeen()->save(); - } elseif (isset($parts[1]) && ($apiKey = ApiKey::valid($token))) { - $userParts = explode('=', trim($parts[1])); - - if (isset($userParts[0]) && $userParts[0] === 'userId') { - $actor = User::find($userParts[1]); - } - } - } - - if ($actor->exists) { - $locale = $actor->getPreference('locale'); - } else { - $locale = array_get($request->getCookieParams(), 'locale'); - } - - if ($locale && $this->locales->hasLocale($locale)) { - $this->locales->setLocale($locale); - } - - return $request->withAttribute('actor', $actor ?: new Guest); - } -} diff --git a/framework/core/src/Api/Server.php b/framework/core/src/Api/Server.php index 48f57dc0f..56371d50f 100644 --- a/framework/core/src/Api/Server.php +++ b/framework/core/src/Api/Server.php @@ -28,10 +28,12 @@ class Server extends AbstractServer $apiPath = parse_url($app->url('api'), PHP_URL_PATH); if ($app->isInstalled() && $app->isUpToDate()) { - $pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie')); - $pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\AuthenticateWithHeader')); $pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\ParseJsonBody')); $pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\FakeHttpMethods')); + $pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\AuthorizeWithCookie')); + $pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\AuthorizeWithHeader')); + $pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\StartSession')); + $pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\SetLocale')); $pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.api.routes')])); $pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\HandleErrors')); } else { diff --git a/framework/core/src/Core/Access/AssertPermissionTrait.php b/framework/core/src/Core/Access/AssertPermissionTrait.php index a88506993..0bc5eee7f 100644 --- a/framework/core/src/Core/Access/AssertPermissionTrait.php +++ b/framework/core/src/Core/Access/AssertPermissionTrait.php @@ -10,8 +10,10 @@ namespace Flarum\Core\Access; +use Flarum\Api\Exception\InvalidAccessTokenException; use Flarum\Core\Exception\PermissionDeniedException; use Flarum\Core\User; +use Psr\Http\Message\ServerRequestInterface; trait AssertPermissionTrait { @@ -61,6 +63,30 @@ trait AssertPermissionTrait */ protected function assertAdmin(User $actor) { - $this->assertPermission($actor->isAdmin()); + $this->assertCan($actor, 'administrate'); + } + + /** + * @param ServerRequestInterface $request + * @throws InvalidAccessTokenException + */ + protected function assertSudo(ServerRequestInterface $request) + { + $session = $request->getAttribute('session'); + + if (! $session || ! $session->isSudo()) { + throw new InvalidAccessTokenException; + } + } + + /** + * @param ServerRequestInterface $request + * @throws PermissionDeniedException + */ + protected function assertAdminAndSudo(ServerRequestInterface $request) + { + $this->assertAdmin($request->getAttribute('actor')); + + $this->assertSudo($request); } } diff --git a/framework/core/src/Core/User.php b/framework/core/src/Core/User.php index 0a2c1a194..195844898 100755 --- a/framework/core/src/Core/User.php +++ b/framework/core/src/Core/User.php @@ -135,7 +135,7 @@ class User extends AbstractModel $user->read()->detach(); $user->groups()->detach(); - $user->accessTokens()->delete(); + $user->sessions()->delete(); $user->notifications()->delete(); }); @@ -654,13 +654,13 @@ class User extends AbstractModel } /** - * Define the relationship with the user's access tokens. + * Define the relationship with the user's sessions. * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ - public function accessTokens() + public function sessions() { - return $this->hasMany('Flarum\Api\AccessToken'); + return $this->hasMany('Flarum\Http\Session'); } /** diff --git a/framework/core/src/Event/UserLoggedIn.php b/framework/core/src/Event/UserLoggedIn.php index a1d86a8cb..32c0ceaf4 100644 --- a/framework/core/src/Event/UserLoggedIn.php +++ b/framework/core/src/Event/UserLoggedIn.php @@ -11,16 +11,17 @@ namespace Flarum\Event; use Flarum\Core\User; +use Flarum\Http\Session; class UserLoggedIn { public $user; - public $token; + public $session; - public function __construct(User $user, $token) + public function __construct(User $user, Session $session) { $this->user = $user; - $this->token = $token; + $this->session = $session; } } diff --git a/framework/core/src/Forum/Controller/AuthenticateUserTrait.php b/framework/core/src/Forum/Controller/AuthenticateUserTrait.php index c0d8eedac..f53250da2 100644 --- a/framework/core/src/Forum/Controller/AuthenticateUserTrait.php +++ b/framework/core/src/Forum/Controller/AuthenticateUserTrait.php @@ -12,14 +12,11 @@ namespace Flarum\Forum\Controller; use Flarum\Core\User; use Zend\Diactoros\Response\HtmlResponse; -use Flarum\Api\Command\GenerateAccessToken; use Flarum\Core\AuthToken; -use DateTime; +use Psr\Http\Message\ServerRequestInterface as Request; trait AuthenticateUserTrait { - use WriteRememberCookieTrait; - /** * @var \Illuminate\Contracts\Bus\Dispatcher */ @@ -45,7 +42,7 @@ trait AuthenticateUserTrait * @param array $suggestions * @return HtmlResponse */ - protected function authenticate(array $identification, array $suggestions = []) + protected function authenticate(Request $request, array $identification, array $suggestions = []) { $user = User::where($identification)->first(); @@ -70,13 +67,8 @@ trait AuthenticateUserTrait $response = new HtmlResponse($content); if ($user) { - // Extend the token's expiry to 2 weeks so that we can set a - // remember cookie - $accessToken = $this->bus->dispatch(new GenerateAccessToken($user->id)); - $accessToken::unguard(); - $accessToken->update(['expires_at' => new DateTime('+2 weeks')]); - - $response = $this->withRememberCookie($response, $accessToken->id); + $session = $request->getAttribute('session'); + $session->assign($user)->regenerateId()->renew()->setDuration(60 * 24 * 14)->save(); } return $response; diff --git a/framework/core/src/Forum/Controller/ConfirmEmailController.php b/framework/core/src/Forum/Controller/ConfirmEmailController.php index df1fe0254..d763ac94d 100644 --- a/framework/core/src/Forum/Controller/ConfirmEmailController.php +++ b/framework/core/src/Forum/Controller/ConfirmEmailController.php @@ -11,7 +11,6 @@ namespace Flarum\Forum\Controller; use Flarum\Core\Command\ConfirmEmail; -use Flarum\Api\Command\GenerateAccessToken; use Flarum\Core\Exception\InvalidConfirmationTokenException; use Flarum\Foundation\Application; use Flarum\Http\Controller\ControllerInterface; @@ -22,8 +21,6 @@ use Zend\Diactoros\Response\RedirectResponse; class ConfirmEmailController implements ControllerInterface { - use WriteRememberCookieTrait; - /** * @var Dispatcher */ @@ -60,13 +57,9 @@ class ConfirmEmailController implements ControllerInterface return new HtmlResponse('Invalid confirmation token'); } - $token = $this->bus->dispatch( - new GenerateAccessToken($user->id) - ); + $session = $request->getAttribute('session'); + $session->assign($user)->regenerateId()->renew()->setDuration(60 * 24 * 14)->save(); - return $this->withRememberCookie( - new RedirectResponse($this->app->url()), - $token->id - ); + return new RedirectResponse($this->app->url()); } } diff --git a/framework/core/src/Forum/Controller/LoginController.php b/framework/core/src/Forum/Controller/LogInController.php similarity index 69% rename from framework/core/src/Forum/Controller/LoginController.php rename to framework/core/src/Forum/Controller/LogInController.php index 3ca89af69..974668106 100644 --- a/framework/core/src/Forum/Controller/LoginController.php +++ b/framework/core/src/Forum/Controller/LogInController.php @@ -11,19 +11,16 @@ namespace Flarum\Forum\Controller; use Flarum\Api\Client; -use Flarum\Api\AccessToken; +use Flarum\Http\Session; use Flarum\Event\UserLoggedIn; use Flarum\Core\Repository\UserRepository; use Flarum\Http\Controller\ControllerInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\JsonResponse; -use DateTime; -class LoginController implements ControllerInterface +class LogInController implements ControllerInterface { - use WriteRememberCookieTrait; - /** * @var \Flarum\Core\Repository\UserRepository */ @@ -52,26 +49,20 @@ class LoginController implements ControllerInterface public function handle(Request $request, array $routeParams = []) { $controller = 'Flarum\Api\Controller\TokenController'; - $actor = $request->getAttribute('actor'); + $session = $request->getAttribute('session'); $params = array_only($request->getParsedBody(), ['identification', 'password']); - $response = $this->apiClient->send($controller, $actor, [], $params); + $response = $this->apiClient->send($controller, $session, [], $params); if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()); - // Extend the token's expiry to 2 weeks so that we can set a - // remember cookie - AccessToken::where('id', $data->token)->update(['expires_at' => new DateTime('+2 weeks')]); + $session = Session::find($data->token); + $session->setDuration(60 * 24 * 14)->save(); - event(new UserLoggedIn($this->users->findOrFail($data->userId), $data->token)); - - return $this->withRememberCookie( - $response, - $data->token - ); - } else { - return $response; + event(new UserLoggedIn($this->users->findOrFail($data->userId), $session)); } + + return $response; } } diff --git a/framework/core/src/Forum/Controller/LogoutController.php b/framework/core/src/Forum/Controller/LogOutController.php similarity index 68% rename from framework/core/src/Forum/Controller/LogoutController.php rename to framework/core/src/Forum/Controller/LogOutController.php index e8975b168..c00dc0f43 100644 --- a/framework/core/src/Forum/Controller/LogoutController.php +++ b/framework/core/src/Forum/Controller/LogOutController.php @@ -10,18 +10,16 @@ namespace Flarum\Forum\Controller; -use Flarum\Api\AccessToken; use Flarum\Event\UserLoggedOut; use Flarum\Foundation\Application; use Flarum\Http\Controller\ControllerInterface; +use Flarum\Http\Exception\TokenMismatchException; use Illuminate\Contracts\Events\Dispatcher; use Psr\Http\Message\ServerRequestInterface as Request; use Zend\Diactoros\Response\RedirectResponse; -class LogoutController implements ControllerInterface +class LogOutController implements ControllerInterface { - use WriteRememberCookieTrait; - /** * @var Application */ @@ -46,21 +44,24 @@ class LogoutController implements ControllerInterface * @param Request $request * @param array $routeParams * @return \Psr\Http\Message\ResponseInterface + * @throws TokenMismatchException */ public function handle(Request $request, array $routeParams = []) { - $user = $request->getAttribute('actor'); + $session = $request->getAttribute('session'); - if ($user->exists) { - $token = array_get($request->getQueryParams(), 'token'); + if ($user = $session->user) { + if (array_get($request->getQueryParams(), 'token') !== $session->csrf_token) { + throw new TokenMismatchException; + } - AccessToken::where('user_id', $user->id)->findOrFail($token); + $session->exists = false; - $user->accessTokens()->delete(); + $user->sessions()->delete(); $this->events->fire(new UserLoggedOut($user)); } - return $this->withForgetCookie(new RedirectResponse($this->app->url())); + return new RedirectResponse($this->app->url()); } } diff --git a/framework/core/src/Forum/Controller/RegisterController.php b/framework/core/src/Forum/Controller/RegisterController.php index 1804772af..6c0f7c954 100644 --- a/framework/core/src/Forum/Controller/RegisterController.php +++ b/framework/core/src/Forum/Controller/RegisterController.php @@ -11,19 +11,15 @@ namespace Flarum\Forum\Controller; use Flarum\Api\Client; -use Flarum\Api\AccessToken; +use Flarum\Core\User; use Flarum\Http\Controller\ControllerInterface; -use Flarum\Api\Command\GenerateAccessToken; use Psr\Http\Message\ServerRequestInterface as Request; use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\JsonResponse; use Illuminate\Contracts\Bus\Dispatcher; -use DateTime; class RegisterController implements ControllerInterface { - use WriteRememberCookieTrait; - /** * @var Dispatcher */ @@ -61,21 +57,13 @@ class RegisterController implements ControllerInterface $body = json_decode($response->getBody()); $statusCode = $response->getStatusCode(); - $response = new JsonResponse($body, $statusCode); + if (isset($body->data)) { + $user = User::find($body->data->id); - if (! empty($body->data->attributes->isActivated)) { - $token = $this->bus->dispatch(new GenerateAccessToken($body->data->id)); - - // Extend the token's expiry to 2 weeks so that we can set a - // remember cookie - AccessToken::where('id', $token->id)->update(['expires_at' => new DateTime('+2 weeks')]); - - return $this->withRememberCookie( - $response, - $token->id - ); + $session = $request->getAttribute('session'); + $session->assign($user)->regenerateId()->renew()->setDuration(60 * 24 * 14)->save(); } - return $response; + return new JsonResponse($body, $statusCode); } } diff --git a/framework/core/src/Forum/Controller/WriteRememberCookieTrait.php b/framework/core/src/Forum/Controller/WriteRememberCookieTrait.php deleted file mode 100644 index 86fd77ce0..000000000 --- a/framework/core/src/Forum/Controller/WriteRememberCookieTrait.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Forum\Controller; - -use Dflydev\FigCookies\FigResponseCookies; -use Dflydev\FigCookies\SetCookie; -use Psr\Http\Message\ResponseInterface; - -trait WriteRememberCookieTrait -{ - protected function withRememberCookie(ResponseInterface $response, $token) - { - // Set a long-living cookie (two weeks) with the remember token - return FigResponseCookies::set( - $response, - SetCookie::create('flarum_remember', $token) - ->withMaxAge(14 * 24 * 60 * 60) - ->withPath('/') - ->withHttpOnly(true) - ); - } - - protected function withForgetCookie(ResponseInterface $response) - { - // Delete the cookie by setting it to an expiration date in the past - return FigResponseCookies::set( - $response, - SetCookie::create('flarum_remember') - ->withMaxAge(-2628000) - ->withPath('/') - ->withHttpOnly(true) - ); - } -} diff --git a/framework/core/src/Forum/Server.php b/framework/core/src/Forum/Server.php index 03d0b3029..a6dc97069 100644 --- a/framework/core/src/Forum/Server.php +++ b/framework/core/src/Forum/Server.php @@ -35,8 +35,10 @@ class Server extends AbstractServer $pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.install.routes')])); $pipe->pipe($basePath, new HandleErrors($errorDir, true)); } elseif ($app->isUpToDate()) { - $pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie')); $pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\ParseJsonBody')); + $pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\AuthorizeWithCookie')); + $pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\StartSession')); + $pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\SetLocale')); $pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.forum.routes')])); $pipe->pipe($basePath, new HandleErrors($errorDir, $app->inDebugMode())); } else { diff --git a/framework/core/src/Http/Controller/ClientView.php b/framework/core/src/Http/Controller/ClientView.php index e7b89e973..4f5e919db 100644 --- a/framework/core/src/Http/Controller/ClientView.php +++ b/framework/core/src/Http/Controller/ClientView.php @@ -339,9 +339,11 @@ class ClientView implements Renderable */ protected function getSession() { + $session = $this->request->getAttribute('session'); + return [ 'userId' => $this->actor->id, - 'token' => array_get($this->request->getCookieParams(), 'flarum_remember'), + 'csrfToken' => $session->csrf_token ]; } } diff --git a/framework/core/src/Http/Exception/MethodNotAllowedException.php b/framework/core/src/Http/Exception/MethodNotAllowedException.php new file mode 100644 index 000000000..0b7cd3f8e --- /dev/null +++ b/framework/core/src/Http/Exception/MethodNotAllowedException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Http\Exception; + +use Exception; + +class MethodNotAllowedException extends Exception +{ + public function __construct($message = null, $code = 405, Exception $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/framework/core/src/Http/Exception/TokenMismatchException.php b/framework/core/src/Http/Exception/TokenMismatchException.php new file mode 100644 index 000000000..49da0abaa --- /dev/null +++ b/framework/core/src/Http/Exception/TokenMismatchException.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Http\Exception; + +use Exception; + +class TokenMismatchException extends Exception +{ +} diff --git a/framework/core/src/Http/Middleware/AuthenticateWithCookie.php b/framework/core/src/Http/Middleware/AuthenticateWithCookie.php index f39f7a2aa..aca046548 100644 --- a/framework/core/src/Http/Middleware/AuthenticateWithCookie.php +++ b/framework/core/src/Http/Middleware/AuthenticateWithCookie.php @@ -10,82 +10,43 @@ namespace Flarum\Http\Middleware; -use Flarum\Api\AccessToken; -use Flarum\Core\Guest; -use Flarum\Locale\LocaleManager; +use Flarum\Http\Exception\TokenMismatchException; +use Flarum\Http\Session; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Zend\Stratigility\MiddlewareInterface; class AuthenticateWithCookie implements MiddlewareInterface { - /** - * @var LocaleManager - */ - protected $locales; - - /** - * @param LocaleManager $locales - */ - public function __construct(LocaleManager $locales) - { - $this->locales = $locales; - } - /** * {@inheritdoc} */ public function __invoke(Request $request, Response $response, callable $out = null) { - $request = $this->logIn($request); + $id = array_get($request->getCookieParams(), 'flarum_session'); + + if ($id) { + $session = Session::find($id); + + $request = $request->withAttribute('session', $session); + + if (! $this->isReading($request) && ! $this->tokensMatch($request)) { + throw new TokenMismatchException; + } + } return $out ? $out($request, $response) : $response; } - /** - * Set the application's actor instance according to the request token. - * - * @param Request $request - * @return Request - */ - protected function logIn(Request $request) + private function isReading(Request $request) { - $actor = new Guest; - - if ($token = $this->getToken($request)) { - if (! $token->isValid()) { - // TODO: https://github.com/flarum/core/issues/253 - } elseif ($token->user) { - $actor = $token->user; - $actor->updateLastSeen()->save(); - } - } - - if ($actor->exists) { - $locale = $actor->getPreference('locale'); - } else { - $locale = array_get($request->getCookieParams(), 'locale'); - } - - if ($locale && $this->locales->hasLocale($locale)) { - $this->locales->setLocale($locale); - } - - return $request->withAttribute('actor', $actor); + return in_array($request->getMethod(), ['HEAD', 'GET', 'OPTIONS']); } - /** - * Get the access token referred to by the request cookie. - * - * @param Request $request - * @return AccessToken|null - */ - protected function getToken(Request $request) + private function tokensMatch(Request $request) { - $token = array_get($request->getCookieParams(), 'flarum_remember'); + $input = $request->getHeaderLine('X-CSRF-Token') ?: array_get($request->getParsedBody(), 'token'); - if ($token) { - return AccessToken::find($token); - } + return $request->getAttribute('session')->csrf_token === $input; } } diff --git a/framework/core/src/Http/Middleware/AuthenticateWithHeader.php b/framework/core/src/Http/Middleware/AuthenticateWithHeader.php new file mode 100644 index 000000000..638ea5cc5 --- /dev/null +++ b/framework/core/src/Http/Middleware/AuthenticateWithHeader.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Http\Middleware; + +use Flarum\Api\ApiKey; +use Flarum\Core\User; +use Flarum\Http\Session; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Zend\Stratigility\MiddlewareInterface; + +class AuthenticateWithHeader implements MiddlewareInterface +{ + /** + * @var string + */ + protected $prefix = 'Token '; + + /** + * {@inheritdoc} + */ + public function __invoke(Request $request, Response $response, callable $out = null) + { + $headerLine = $request->getHeaderLine('authorization'); + + $parts = explode(';', $headerLine); + + if (isset($parts[0]) && starts_with($parts[0], $this->prefix)) { + $id = substr($parts[0], strlen($this->prefix)); + + if (isset($parts[1]) && ApiKey::valid($id)) { + if ($actor = $this->getUser($parts[1])) { + $request = $request->withAttribute('actor', $actor); + } + } else { + $session = Session::find($id); + + $request = $request->withAttribute('session', $session); + } + } + + return $out ? $out($request, $response) : $response; + } + + private function getUser($string) + { + $parts = explode('=', trim($string)); + + if (isset($parts[0]) && $parts[0] === 'userId') { + return User::find($parts[1]); + } + } +} diff --git a/framework/core/src/Http/Middleware/DispatchRoute.php b/framework/core/src/Http/Middleware/DispatchRoute.php index b63208fc2..f8f8d55e4 100644 --- a/framework/core/src/Http/Middleware/DispatchRoute.php +++ b/framework/core/src/Http/Middleware/DispatchRoute.php @@ -13,8 +13,9 @@ namespace Flarum\Http\Middleware; use FastRoute\Dispatcher; use FastRoute\RouteParser; -use Flarum\Http\RouteCollection; +use Flarum\Http\Exception\MethodNotAllowedException; use Flarum\Http\Exception\RouteNotFoundException; +use Flarum\Http\RouteCollection; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -47,6 +48,7 @@ class DispatchRoute * @param Response $response * @param callable $out * @return Response + * @throws MethodNotAllowedException * @throws RouteNotFoundException */ public function __invoke(Request $request, Response $response, callable $out = null) @@ -58,8 +60,11 @@ class DispatchRoute switch ($routeInfo[0]) { case Dispatcher::NOT_FOUND: - case Dispatcher::METHOD_NOT_ALLOWED: throw new RouteNotFoundException; + + case Dispatcher::METHOD_NOT_ALLOWED: + throw new MethodNotAllowedException; + case Dispatcher::FOUND: $handler = $routeInfo[1]; $parameters = $routeInfo[2]; diff --git a/framework/core/src/Http/Middleware/SetLocale.php b/framework/core/src/Http/Middleware/SetLocale.php new file mode 100644 index 000000000..6272a02aa --- /dev/null +++ b/framework/core/src/Http/Middleware/SetLocale.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Http\Middleware; + +use Flarum\Locale\LocaleManager; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Zend\Stratigility\MiddlewareInterface; + +class SetLocale implements MiddlewareInterface +{ + /** + * @var LocaleManager + */ + protected $locales; + + /** + * @param LocaleManager $locales + */ + public function __construct(LocaleManager $locales) + { + $this->locales = $locales; + } + + /** + * {@inheritdoc} + */ + public function __invoke(Request $request, Response $response, callable $out = null) + { + $actor = $request->getAttribute('actor'); + + if ($actor->exists) { + $locale = $actor->getPreference('locale'); + } else { + $locale = array_get($request->getCookieParams(), 'locale'); + } + + if ($locale && $this->locales->hasLocale($locale)) { + $this->locales->setLocale($locale); + } + + return $out ? $out($request, $response) : $response; + } +} diff --git a/framework/core/src/Http/Middleware/StartSession.php b/framework/core/src/Http/Middleware/StartSession.php new file mode 100644 index 000000000..9ba822492 --- /dev/null +++ b/framework/core/src/Http/Middleware/StartSession.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Http\Middleware; + +use Dflydev\FigCookies\FigResponseCookies; +use Dflydev\FigCookies\SetCookie; +use Dflydev\FigCookies\SetCookies; +use Flarum\Http\Session; +use Flarum\Core\Guest; +use Flarum\Http\WriteSessionCookieTrait; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Zend\Stratigility\MiddlewareInterface; + +class StartSession implements MiddlewareInterface +{ + use WriteSessionCookieTrait; + + /** + * {@inheritdoc} + */ + public function __invoke(Request $request, Response $response, callable $out = null) + { + $this->collectGarbage(); + + $session = $this->getSession($request); + $actor = $this->getActor($session); + + $request = $request + ->withAttribute('session', $session) + ->withAttribute('actor', $actor); + + $response = $out ? $out($request, $response) : $response; + + return $this->addSessionCookieToResponse($response, $session, 'flarum_session'); + } + + private function getSession(Request $request) + { + $session = $request->getAttribute('session'); + + if (! $session) { + $session = Session::generate(); + } + + $session->extend()->save(); + + return $session; + } + + private function getActor(Session $session) + { + $actor = $session->user ?: new Guest; + + if ($actor->exists) { + $actor->updateLastSeen()->save(); + } + + return $actor; + } + + private function collectGarbage() + { + if ($this->hitsLottery()) { + Session::whereRaw('last_activity <= ? - duration * 60', [time()])->delete(); + } + } + + private function hitsLottery() + { + return mt_rand(1, 100) <= 1; + } +} diff --git a/framework/core/src/Http/Session.php b/framework/core/src/Http/Session.php new file mode 100644 index 000000000..dff826e49 --- /dev/null +++ b/framework/core/src/Http/Session.php @@ -0,0 +1,140 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Http; + +use DateTime; +use Flarum\Core\User; +use Flarum\Database\AbstractModel; +use Illuminate\Support\Str; + +/** + * @property string $id + * @property int $user_id + * @property int $last_activity + * @property int $duration + * @property \Carbon\Carbon $sudo_expiry_time + * @property string $csrf_token + * @property \Flarum\Core\User|null $user + */ +class Session extends AbstractModel +{ + /** + * {@inheritdoc} + */ + protected $table = 'sessions'; + + /** + * Use a custom primary key for this model. + * + * @var bool + */ + public $incrementing = false; + + /** + * {@inheritdoc} + */ + protected $dates = ['sudo_expiry_time']; + + /** + * Generate a session. + * + * @param User|null $user + * @param int $duration How long before the session will expire, in minutes. + * @return static + */ + public static function generate(User $user = null, $duration = 60) + { + $session = new static; + + $session->assign($user) + ->regenerateId() + ->renew() + ->setDuration($duration); + + return $session->extend(); + } + + /** + * Assign the session to a user. + * + * @param User|null $user + * @return $this + */ + public function assign(User $user = null) + { + $this->user_id = $user ? $user->id : null; + + return $this; + } + + /** + * Regenerate the session ID. + * + * @return $this + */ + public function regenerateId() + { + $this->id = sha1(uniqid('', true).Str::random(25).microtime(true)); + $this->csrf_token = Str::random(40); + + return $this; + } + + /** + * @return $this + */ + public function extend() + { + $this->last_activity = time(); + + return $this; + } + + /** + * @return $this + */ + public function renew() + { + $this->extend(); + $this->sudo_expiry_time = time() + 30 * 60; + + return $this; + } + + /** + * @param int $duration How long before the session will expire, in minutes. + * @return $this + */ + public function setDuration($duration) + { + $this->duration = $duration; + + return $this; + } + + /** + * @return bool + */ + public function isSudo() + { + return $this->sudo_expiry_time > new DateTime; + } + + /** + * Define the relationship with the owner of this access token. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/framework/core/src/Http/WriteSessionCookieTrait.php b/framework/core/src/Http/WriteSessionCookieTrait.php new file mode 100644 index 000000000..f74cea56c --- /dev/null +++ b/framework/core/src/Http/WriteSessionCookieTrait.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Http; + +use Dflydev\FigCookies\FigResponseCookies; +use Dflydev\FigCookies\SetCookie; +use Psr\Http\Message\ResponseInterface as Response; + +trait WriteSessionCookieTrait +{ + protected function addSessionCookieToResponse(Response $response, Session $session, $cookieName) + { + return FigResponseCookies::set( + $response, + SetCookie::create($cookieName, $session->exists ? $session->id : null) + ->withMaxAge($session->exists ? $session->duration * 60 : -2628000) + ->withPath('/') + ->withHttpOnly(true) + ); + } +} diff --git a/framework/core/src/Install/Controller/InstallController.php b/framework/core/src/Install/Controller/InstallController.php index c14af5187..d0f52ecbc 100644 --- a/framework/core/src/Install/Controller/InstallController.php +++ b/framework/core/src/Install/Controller/InstallController.php @@ -10,14 +10,15 @@ namespace Flarum\Install\Controller; +use Flarum\Core\User; use Flarum\Http\Controller\ControllerInterface; +use Flarum\Http\Session; +use Flarum\Http\WriteSessionCookieTrait; use Psr\Http\Message\ServerRequestInterface as Request; use Zend\Diactoros\Response\HtmlResponse; use Zend\Diactoros\Response; use Flarum\Install\Console\InstallCommand; use Flarum\Install\Console\DefaultsDataProvider; -use Flarum\Api\Command\GenerateAccessToken; -use Flarum\Forum\Controller\WriteRememberCookieTrait; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Input\StringInput; use Illuminate\Contracts\Bus\Dispatcher; @@ -26,7 +27,7 @@ use DateTime; class InstallController implements ControllerInterface { - use WriteRememberCookieTrait; + use WriteSessionCookieTrait; protected $command; @@ -87,14 +88,9 @@ class InstallController implements ControllerInterface return new HtmlResponse($e->getMessage(), 500); } - $token = $this->bus->dispatch( - new GenerateAccessToken(1) - ); - $token->update(['expires_at' => new DateTime('+2 weeks')]); + $session = Session::generate(User::find(1), 60 * 24 * 14); + $session->save(); - return $this->withRememberCookie( - new Response($body, 200), - $token->id - ); + return $this->addSessionCookieToResponse(new Response($body, 200), $session, 'flarum_session'); } } diff --git a/framework/core/views/login.blade.php b/framework/core/views/login.blade.php new file mode 100644 index 000000000..8f98689ad --- /dev/null +++ b/framework/core/views/login.blade.php @@ -0,0 +1,32 @@ + + + + + + Log In + + + + + +

Log In

+ +
+ + +
+ + +
+ +
+ + +
+ +
+ +
+
+ +