Implement token-based auth API

This commit is contained in:
Toby Zerner 2015-01-22 14:44:33 +10:30
parent 28d8718e27
commit 215bdb672a
11 changed files with 194 additions and 44 deletions

View File

@ -0,0 +1,32 @@
<?php namespace Flarum\Api\Actions\Auth;
use Event;
use Response;
use Auth;
use Flarum\Core\Users\User;
use Flarum\Api\Actions\Base;
class Login extends Base
{
/**
* Log in and return a token.
*
* @return Response
*/
protected function run()
{
$identifier = $this->input('identifier');
$password = $this->input('password');
$field = filter_var($identifier, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
$credentials = [$field => $identifier, 'password' => $password];
if (! Auth::attempt($credentials)) {
return $this->respondWithError('invalidLogin', 401);
}
$token = Auth::user()->getRememberToken();
return Response::json(compact('token'));
}
}

View File

@ -24,6 +24,8 @@ class CoreServiceProvider extends ServiceProvider
$this->app->make('validator')->extend('username', 'Flarum\Core\Users\UsernameValidator@validate');
$this->app['config']->set('auth.model', 'Flarum\Core\Users\User');
Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\DiscussionMetadataUpdater');
Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\UserMetadataUpdater');
Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\PostFormatter');

View File

@ -44,7 +44,7 @@ class Discussion extends Entity
// Allow a user to edit their own discussion.
static::grant('edit', function ($grant, $user) {
if (app('flarum.permissions')->granted($user, 'editOwn', 'discussion')) {
$grant->where('user_id', $user->id);
$grant->where('start_user_id', $user->id);
}
});

View File

@ -1,7 +1,9 @@
<?php namespace Flarum\Core\Users;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\UserTrait;
use Illuminate\Auth\Reminders\RemindableInterface;
use Illuminate\Auth\Reminders\RemindableTrait;
use Auth;
use DB;
use Event;
@ -13,10 +15,12 @@ use Flarum\Core\Entity;
use Flarum\Core\Groups\Group;
use Flarum\Core\Support\Exceptions\PermissionDeniedException;
class User extends Entity /*implements UserInterface, RemindableInterface*/
class User extends Entity implements UserInterface, RemindableInterface
{
use EventGenerator;
use Permissible;
use UserTrait, RemindableTrait;
protected static $rules = [
'username' => 'required|username|unique',
@ -206,34 +210,4 @@ class User extends Entity /*implements UserInterface, RemindableInterface*/
{
return $this->hasMany('Flarum\Core\Activity\Activity');
}
/**
* Get the unique identifier for the user.
*
* @return mixed
*/
public function getAuthIdentifier()
{
return $this->getKey();
}
/**
* Get the password for the user.
*
* @return string
*/
public function getAuthPassword()
{
return $this->password;
}
/**
* Get the e-mail address where password reminders are sent.
*
* @return string
*/
public function getReminderEmail()
{
return $this->email;
}
}

View File

@ -18,6 +18,7 @@ class CreateUsersTable extends Migration {
$table->string('username');
$table->string('email');
$table->string('password');
$table->rememberToken();
$table->dateTime('join_time');
$table->string('time_zone');
$table->dateTime('last_seen_time')->nullable();

View File

@ -3,14 +3,35 @@
$action = function($class)
{
return function () use ($class) {
$action = \App::make($class);
$action = App::make($class);
$request = app('request');
$parameters = Route::current()->parameters();
return $action->handle($request, $parameters);
};
};
Route::group(['prefix' => 'api'], function () use ($action) {
// @todo refactor into a unit-testable class
Route::filter('attemptLogin', function($route, $request) {
$prefix = 'Token ';
if (starts_with($request->headers->get('authorization'), $prefix)) {
$token = substr($request->headers->get('authorization'), strlen($prefix));
Auth::once(['remember_token' => $token]);
}
});
Route::group(['prefix' => 'api', 'before' => 'attemptLogin'], function () use ($action) {
/*
|--------------------------------------------------------------------------
| Auth
|--------------------------------------------------------------------------
*/
// Login
Route::post('auth/login', [
'as' => 'flarum.api.auth.login',
'uses' => $action('Flarum\Api\Actions\Auth\Login')
]);
/*
|--------------------------------------------------------------------------

View File

@ -1,10 +1,35 @@
<?php
namespace Codeception\Module;
// here you can define custom actions
// all public methods declared in helper class will be available in $I
use Laracasts\TestDummy\Factory;
use Auth;
use DB;
class ApiHelper extends \Codeception\Module
{
public function haveAnAccount($data = [])
{
return Factory::create('Flarum\Core\Users\User', $data);
}
public function login($identifier, $password)
{
$this->getModule('REST')->sendPOST('/api/auth/login', ['identifier' => $identifier, 'password' => $password]);
$response = json_decode($this->getModule('REST')->grabResponse(), true);
if ($response && is_array($response) && isset($response['token'])) {
return $response['token'];
}
return false;
}
public function amAuthenticated()
{
$user = $this->haveAnAccount();
$user->groups()->attach(3); // Add member group
Auth::onceUsingId($user->id);
return $user;
}
}

View File

@ -1,4 +1,4 @@
<?php //[STAMP] 56e5f4700a805fa943ff8199ddb69b69
<?php //[STAMP] 93c972ae47d60c70b9045d971476f0bc
// This class was automatically generated by build task
// You should not change it manually as it will be overwritten on next build
@ -3029,4 +3029,37 @@ class ApiTester extends \Codeception\Actor
public function fail($message) {
return $this->scenario->runStep(new \Codeception\Step\Action('fail', func_get_args()));
}
/**
* [!] Method is generated. Documentation taken from corresponding module.
*
*
* @see \Codeception\Module\ApiHelper::haveAnAccount()
*/
public function haveAnAccount($data = null) {
return $this->scenario->runStep(new \Codeception\Step\Action('haveAnAccount', func_get_args()));
}
/**
* [!] Method is generated. Documentation taken from corresponding module.
*
*
* @see \Codeception\Module\ApiHelper::login()
*/
public function login($identifier, $password) {
return $this->scenario->runStep(new \Codeception\Step\Action('login', func_get_args()));
}
/**
* [!] Method is generated. Documentation taken from corresponding module.
*
*
* @see \Codeception\Module\ApiHelper::amAuthenticated()
*/
public function amAuthenticated() {
return $this->scenario->runStep(new \Codeception\Step\Condition('amAuthenticated', func_get_args()));
}
}

View File

@ -0,0 +1,55 @@
<?php
use \ApiTester;
use Laracasts\TestDummy\Factory;
class AuthCest
{
protected $endpoint = '/api/auth';
public function loginWithEmail(ApiTester $I)
{
$I->wantTo('login via API with email');
$user = $I->haveAnAccount([
'email' => 'foo@bar.com',
'password' => 'pass7word'
]);
$token = $I->login('foo@bar.com', 'pass7word');
$I->seeResponseCodeIs(200);
$I->seeResponseIsJson();
$loggedIn = User::where('remember_token', $token)->first();
$I->assertEquals($user->id, $loggedIn->id);
}
public function loginWithUsername(ApiTester $I)
{
$I->wantTo('login via API with username');
$user = $I->haveAnAccount([
'username' => 'tobscure',
'password' => 'pass7word'
]);
$token = $I->login('tobscure', 'pass7word');
$I->seeResponseCodeIs(200);
$I->seeResponseIsJson();
$loggedIn = User::where('remember_token', $token)->first();
$I->assertEquals($user->id, $loggedIn->id);
}
public function invalidLogin(ApiTester $I)
{
$user = $I->haveAnAccount([
'email' => 'foo@bar.com',
'password' => 'pass7word'
]);
$I->login('foo@bar.com', 'incorrect');
$I->seeResponseCodeIs(401);
$I->seeResponseIsJson();
}
}

View File

@ -42,7 +42,7 @@ class DiscussionsResourceCest {
{
$I->wantTo('create a discussion via API');
$I->haveHttpHeader('Authorization', 'Token 123456');
$I->amAuthenticated();
$I->sendPOST($this->endpoint, ['discussions' => ['title' => 'foo', 'content' => 'bar']]);
$I->seeResponseCodeIs(200);
@ -58,9 +58,9 @@ class DiscussionsResourceCest {
{
$I->wantTo('update a discussion via API');
$I->haveHttpHeader('Authorization', 'Token 123456');
$user = $I->amAuthenticated();
$discussion = Factory::create('Flarum\Core\Discussions\Discussion');
$discussion = Factory::create('Flarum\Core\Discussions\Discussion', ['start_user_id' => $user->id]);
$I->sendPUT($this->endpoint.'/'.$discussion->id, ['discussions' => ['title' => 'foo']]);
$I->seeResponseCodeIs(200);
@ -75,9 +75,10 @@ class DiscussionsResourceCest {
{
$I->wantTo('delete a discussion via API');
$I->haveHttpHeader('Authorization', 'Token 123456');
$user = $I->amAuthenticated();
$user->groups()->attach(4);
$discussion = Factory::create('Flarum\Core\Discussions\Discussion');
$discussion = Factory::create('Flarum\Core\Discussions\Discussion', ['start_user_id' => $user->id]);
$I->sendDELETE($this->endpoint.'/'.$discussion->id);
$I->seeResponseCodeIs(204);

View File

@ -1,9 +1,15 @@
<?php
$factory('Flarum\Core\Discussions\Discussion', [
'title' => $faker->sentence
'title' => $faker->sentence,
'start_time' => $faker->dateTimeThisYear,
'start_user_id' => 'factory:Flarum\Core\Users\User'
]);
$factory('Flarum\Core\Users\User', [
'username' => $faker->sentence
'username' => $faker->userName,
'email' => $faker->safeEmail,
'password' => 'password',
'join_time' => $faker->dateTimeThisYear,
'time_zone' => $faker->timezone
]);