Install spiral/auth
module in order to enable token based authorization of your users.
$ composer require spiral/auth
$ spiral register spiral/auth
First of all we have to define what is actually represent user in your application, in order to do that create Record User
and repository UserRepository
:
class User extends Record
{
use TimestampsTrait;
const SCHEMA = [
'id' => 'primary',
'email' => 'string',
'password' => 'string'
];
const INDEXES = [
[self::UNIQUE, 'email']
];
const SECURED = ['password', 'roles'];
}
class UserRepository extends RecordSource
{
const RECORD = User::class;
}
To link your repository and user to auth component we have to implement 2 interfaces.
This interface indicates that user entity contain hashed password:
class User extends Record implements PasswordAwareInterface
{
/**
* @inheritdoc
*/
public function getPasswordHash(): string
{
return $this->password;
}
}
In order to enable credentials based authentication in your application your repository must implement UsernameSourceInterface
:
class UserRepository extends RecordSource implements UsernameSourceInterface
{
const RECORD = User::class;
/**
* @inheritdoc
*/
public function findByUsername(string $username)
{
return $this->findOne(['email' => $username]);
}
}
To allow auth module locate your users via repository - create container binding in one of your bootloaders:
const BINDINGS = [
UsernameSourceInterface::class => UserRepository::class,
];
To properly create user use Spiral\Auth\Hashing\PasswordHasher
dependency to handle password hashing:
public function createUser(PasswordHasher $hasher)
{
$user = new User();
$user->email = 'email@domain.com';
$user->password = $hasher->hash('password');
$user->save();
}
To enable access to authorization scope in your application add AuthMiddleware
into your http config or specific route. Once completed you can access current user context using shortcut auth
or via dependency Spiral\Auth\ContextInterface
.
public function indexAction()
{
dump($this->auth->isAuthenticated());
}
To authorize users use CredentialsAuthenticator
which will handle password comparision logic for us:
public function loginAction(LoginRequest $request, CredentialsAuthenticator $authenticator)
{
if (!$request->isValid()) {
return [
'status' => 400,
'errors' => $request->getErrors()
];
}
try {
$user = $authenticator->getUser($request->username, $request->password);
} catch (CredentialsException $e) {
throw new ForbiddenException('unable to authorize');
}
//Create auth token (using default token operator, see below)
$this->auth->init($this->tokens->createToken($user));
//Redirect and etc
}
Where LoginRequest
:
class LoginRequest extends RequestFilter
{
const SCHEMA = [
'username' => 'data:username',
'password' => 'data:password'
];
const SETTERS = [
'username' => 'trim',
'password' => 'trim'
];
const VALIDATES = [
'username' => [
['notEmpty', 'error' => '[[Please enter your username]]']
],
'password' => [
['notEmpty', 'error' => '[[Please enter your password]]']
],
];
}
You can write your own authenticators, see how to manually create auth tokens below.
Note that auth module uses token based authentication for all users, you are able to write your own token operator (modules/auth config):
return [
//Default token provider
'defaultOperator' => 'cookie',
/*
* Set of auth providers/operators responsible for user session support
*/
'operators' => [
/*
* Uses active session storage to store user information
*/
'session' => [
'class' => Operators\SessionOperator::class,
'options' => [
'section' => 'auth'
]
],
/*
* Utilized default HTTP basic auth protocol to authenticate user
*/
'basic' => [
'class' => Operators\HttpOperator::class,
'options' => []
],
/*
* Reads token hash from a specified header
*/
'header' => [
'class' => Operators\PersistentOperator::class,
'options' => [
//Token lifetime
'lifetime' => 86400 * 14,
//Persistent token storage
'source' => bind(\Spiral\Auth\Database\Sources\AuthTokenSource::class),
//How to read and write tokens in request
'bridge' => bind(Operators\Bridges\HeaderBridge::class, [
'header' => 'X-Auth-Token',
])
]
],
/*
* Stores authentication token into cookie
*/
'cookie' => [
'class' => Operators\PersistentOperator::class,
'options' => [
//Cookie and token lifetime
'lifetime' => 86400 * 7,
//Persistent token storage
'source' => bind(\Spiral\Auth\Database\Sources\AuthTokenSource::class),
//How to read and write tokens in request
'bridge' => bind(Operators\Bridges\CookieBridge::class, [
'cookie' => 'auth-token',
])
]
],
/*
* Stores authentication token into cookie as a remember-me cookie
*/
'long' => [
'class' => Operators\PersistentOperator::class,
'options' => [
//Cookie and token lifetime
'lifetime' => 86400 * 30,
//Persistent token storage
'source' => bind(\Spiral\Auth\Database\Sources\AuthTokenSource::class),
//How to read and write tokens in request
'bridge' => bind(Operators\Bridges\CookieBridge::class, [
'cookie' => 'long-token',
])
]
],
/*{{operators}}*/
]
];
For example, we can create token to be stored in cookie and verified using ORM models represented by Spiral\Auth\Database\Sources\AuthTokenSource
:
$this->auth->init($this->tokens->createToken($user, 'long'));
Auth middleware will detect what token and operator automatically.
To log user out use close
method of your context:
$this->auth->close();
It's recommended to use authorized user as actor for your security component, you can do that by implementing additional interface in your user entity and by configuring container binding:
class User extends Record implements PasswordAwareInterface, ActorInterface
{
//...
public function getRoles(): array
{
return ['user'];
}
}
To indicate that our user have to be used as actor:
const BINDINGS = [
ActorInterface::class => [self::class, 'getActor'],
]
Where getActor
is IoC specific proxy:
public function getActor(\Spiral\Auth\ContextInterface $context): ActorInterface
{
if ($context->isAuthenticated()) {
return $context->getUser();
}
return new Guest();
}