mirror of
https://github.com/flarum/framework.git
synced 2025-02-22 09:55:27 +08:00
Overhaul sessions, tokens, and authentication
- Use cookies + CSRF token for API authentication in the default client. This mitigates potential XSS attacks by making the token unavailable to JavaScript. The Authorization header is still supported, but not used by default. - Make sensitive/destructive actions (editing a user, permanently deleting anything, visiting the admin CP) require the user to re-enter their password if they haven't entered it in the last 30 minutes. - Refactor and clean up the authentication middleware. - Add an `onhide` hook to the Modal component. (+1 squashed commit)
This commit is contained in:
parent
22331306c6
commit
32e9c0587c
@ -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)
|
||||
|
||||
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 (
|
||||
<article {...attrs}>
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
73
framework/core/js/lib/components/ConfirmPasswordModal.js
Normal file
73
framework/core/js/lib/components/ConfirmPasswordModal.js
Normal file
@ -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 (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<input
|
||||
type="password"
|
||||
className="FormControl"
|
||||
bidi={this.password}
|
||||
placeholder={extractText(app.translator.trans('core.forum.confirm_password.password_placeholder'))}
|
||||
disabled={this.loading}/>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<Button
|
||||
type="submit"
|
||||
className="Button Button--primary Button--block"
|
||||
loading={this.loading}>
|
||||
{app.translator.trans('core.forum.confirm_password.submit_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,6 +77,10 @@ export default class ModalManager extends Component {
|
||||
* @protected
|
||||
*/
|
||||
clear() {
|
||||
if (this.component) {
|
||||
this.component.onhide();
|
||||
}
|
||||
|
||||
this.component = null;
|
||||
|
||||
m.lazyRedraw();
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -167,6 +167,9 @@
|
||||
color: @muted-more-color;
|
||||
}
|
||||
}
|
||||
.Post--loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.PostMeta {
|
||||
display: inline;
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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');
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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');
|
||||
}
|
||||
}
|
@ -42,6 +42,8 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin');
|
||||
|
||||
$this->flushAssetsWhenThemeChanged();
|
||||
|
||||
$this->flushAssetsWhenExtensionsChanged();
|
||||
|
@ -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;
|
||||
|
@ -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()));
|
||||
|
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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');
|
||||
}
|
||||
}
|
@ -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()));
|
||||
|
@ -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);
|
||||
|
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
|
@ -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'))
|
||||
);
|
||||
|
@ -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'))
|
||||
);
|
||||
|
@ -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'))
|
||||
);
|
||||
|
@ -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');
|
||||
|
@ -47,7 +47,7 @@ class SetSettingsController implements ControllerInterface
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request)
|
||||
{
|
||||
$this->assertAdmin($request->getAttribute('actor'));
|
||||
$this->assertAdminAndSudo($request);
|
||||
|
||||
$settings = $request->getParsedBody();
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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)
|
||||
);
|
||||
|
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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
|
||||
{
|
||||
}
|
@ -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]);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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]);
|
||||
}
|
||||
}
|
@ -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]);
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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]);
|
||||
}
|
||||
}
|
@ -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]);
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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]);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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]);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -1,93 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
18
framework/core/src/Http/Exception/TokenMismatchException.php
Normal file
18
framework/core/src/Http/Exception/TokenMismatchException.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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
|
||||
{
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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]);
|
||||
}
|
||||
}
|
||||
}
|
@ -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];
|
||||
|
52
framework/core/src/Http/Middleware/SetLocale.php
Normal file
52
framework/core/src/Http/Middleware/SetLocale.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
}
|
81
framework/core/src/Http/Middleware/StartSession.php
Normal file
81
framework/core/src/Http/Middleware/StartSession.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
}
|
140
framework/core/src/Http/Session.php
Normal file
140
framework/core/src/Http/Session.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
29
framework/core/src/Http/WriteSessionCookieTrait.php
Normal file
29
framework/core/src/Http/WriteSessionCookieTrait.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* 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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
32
framework/core/views/login.blade.php
Normal file
32
framework/core/views/login.blade.php
Normal file
@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Log In</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Log In</h1>
|
||||
|
||||
<form class="form-horizontal" role="form" method="POST" action="{{ app('Flarum\Admin\UrlGenerator')->toRoute('index') }}">
|
||||
<input type="hidden" name="token" value="{{ $token }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Username or Email</label>
|
||||
<input type="text" class="form-control" name="identification">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Password</label>
|
||||
<input type="password" class="form-control" name="password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Log In</button>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user