How to Build a Secure Authentication with PHP
PHP version required 7.3
The interfaces
UserInterface
<?php
namespace DevCoder\Authentication;
/**
* Interface UserInterface
* @package DevCoder\Authentication
*/
interface UserInterface
{
public function getUsername() :?string;
public function getPassword() :?string;
public function getRoles() : array;
public function isEnabled(): bool;
}
UserTokenInterface
<?php
namespace DevCoder\Authentication\Token;
use DevCoder\Authentication\UserInterface;
/**
* Interface UserTokenInterface
* @package DevCoder\Authentication\Token
*/
interface UserTokenInterface
{
const DEFAULT_PREFIX_KEY = 'user_security';
public function getUser(): UserInterface;
public function serialize(): string;
}
UserManagerInterface
<?php
namespace DevCoder\Authentication\Core;
use DevCoder\Authentication\Token\UserTokenInterface;
use DevCoder\Authentication\UserInterface;
interface UserManagerInterface
{
public function getUserToken(): ?UserTokenInterface;
public function hasUserToken(): bool;
public function createUserToken(UserInterface $user): UserTokenInterface;
public function logout(): void;
public function cryptPassword(string $plainPassword): string;
public function isPasswordValid(UserInterface $user, string $plainPassword): bool;
}
Now let's create the classes which will implement the above interfaces
PasswordTrait to Manage Passwords
<?php
namespace DevCoder\Authentication\Core;
use DevCoder\Authentication\UserInterface;
/**
* Trait PasswordTrait
* @package DevCoder\Authentication\Core
*/
trait PasswordTrait
{
private $cost = 10;
public function cryptPassword(string $plainPassword): string
{
return password_hash($plainPassword, PASSWORD_BCRYPT, ['cost' => $this->cost]);
}
public function isPasswordValid(UserInterface $user, string $plainPassword): bool
{
return password_verify($plainPassword, $user->getPassword());
}
public function setCost(int $cost): void
{
if ($cost < 4 || $cost > 12) {
throw new \InvalidArgumentException('Cost must be in the range of 4-31.');
}
$this->cost = $cost;
}
}
UserManager to Manage User
<?php
namespace DevCoder\Authentication\Core;
use DevCoder\Authentication\Token\UserToken;
use DevCoder\Authentication\Token\UserTokenInterface;
use DevCoder\Authentication\UserInterface;
/**
* Class UserManager
* @package DevCoder\Authentication\Core
*/
class UserManager implements UserManagerInterface
{
use PasswordTrait;
public function __construct()
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
public function getUserToken(): ?UserTokenInterface
{
$userToken = null;
if ($this->hasUserToken()) {
$userToken = unserialize($_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY]);
}
return $userToken;
}
public function hasUserToken(): bool
{
$key = UserTokenInterface::DEFAULT_PREFIX_KEY;
return (array_key_exists($key, $_SESSION) && unserialize($_SESSION[$key]) !== false);
}
public function isGranted(array $roles): bool
{
if (!is_null($userToken = $this->getUserToken())) {
return false;
}
if ($userToken->getUser() instanceof UserInterface) {
return (!empty(array_intersect($roles, $userToken->getUser()->getRoles())));
}
return false;
}
public function createUserToken(UserInterface $user): UserTokenInterface
{
$userToken = new UserToken($user);
$_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY] = $userToken->serialize();
return $userToken;
}
public function logout(): void
{
if ($this->hasUserToken()) {
unset($_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY]);
}
}
}
UserToken to get current User
<?php
namespace DevCoder\Authentication\Token;
use DevCoder\Authentication\UserInterface;
/**
* Class UserToken
* @package DevCoder\Authentication\Token
*/
class UserToken implements UserTokenInterface
{
/**
* @var UserInterface
*/
private $user;
public function __construct(UserInterface $user)
{
$this->user = $user;
}
public function getUser(): UserInterface
{
return $this->user;
}
public function serialize(): string
{
return serialize($this);
}
}
User class
<?php
namespace DevCoder\Authentication;
class User implements UserInterface
{
/**
* @var string
*/
private $userName;
/**
* @var string
*/
private $password;
/**
* @var array
*/
private $roles = [];
/**
* @var bool
*/
private $enabled = true;
/**
* @return null|string
*/
public function getUsername(): ?string
{
return $this->userName;
}
/**
* @return null|string
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* @return array
*/
public function getRoles(): array
{
return $this->roles;
}
/**
* @return bool
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* @param string $userName
* @return User
*/
public function setUserName(string $userName): self
{
$this->userName = $userName;
return $this;
}
/**
* @param string $password
* @return User
*/
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* @param array $roles
* @return User
*/
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @param bool $enabled
* @return User
*/
public function setEnabled(bool $enabled): self
{
$this->enabled = $enabled;
return $this;
}
}
How to use ?
Registration
<?php
use DevCoder\Authentication\Core\UserManager;
use DevCoder\Authentication\User;
// register
$userManager = new UserManager();
$password = $userManager->cryptPassword($_POST['password']);
$user = (new User())
->setUserName($_POST['username'])
->setPassword($password)
->setRoles(['ROLE_USER']);
$userManager->createUserToken($user);
// check Token in Session
var_dump($userManager->getUserToken());
// object(DevCoder\Authentication\Token\UserToken)[4]
// private 'user' =>
// object(DevCoder\Authentication\User)[5]
// private 'userName' => string 'username' (length=8)
// private 'password' => string '$2y$10$iWdcmebmikUFlgKMqW7/rOmUp1DjFAuWKqdUHBhL08FZ7LL6bwRey' (length=60)
// private 'roles' =>
// array (size=1)
// 0 => string 'ROLE_USER' (length=9)
// private 'enabled' => boolean true
Connected or not
<?php
use DevCoder\Authentication\Core\UserManager;
use DevCoder\Authentication\User;
$userManager = new UserManager();
if ($userManager->hasUserToken()) {
// connected
$token = $userManager->getUserToken();
$user = $token->getUser();
var_dump($user);
// object(DevCoder\Authentication\User)[5]
// private 'userName' => string 'username' (length=8)
// private 'password' => string '$2y$10$OBobeLhdvdiftuedlv1a6e4.qF6sCG/usq5WEV4E3uB.UiS1egv/m' (length=60)
// private 'roles' =>
// array (size=1)
// 0 => string 'ROLE_USER' (length=9)
// private 'enabled' => boolean true
}else {
// not connected
}
Access management
$userManager = new UserManager();
if ($userManager->isGranted(['ROLE_ADMIN'])) {
//is admin
// return Response 200
}else {
//is not admin
// return Response 403
}
Logout
$userManager = new UserManager();
$userManager->logout();
Login
<?php
use DevCoder\Authentication\Core\UserManager;
$stmt = $pdo->prepare("SELECT * FROM users WHERE username=?");
$stmt->execute([$_POST['username']]);
$userFromDataBase = $stmt->fetch();
/**
* Hydration
*/
$user = (new \Test\DevCoder\Authentication\User())
->setUserName($userFromDataBase['username'])
->setPassword($userFromDataBase['password'])
->setRoles(json_decode($userFromDataBase['roles']))
->setEnabled($userFromDataBase['active']);
$userManager = new UserManager();
if ($userManager->isPasswordValid($user, $_POST['password'])) {
// login OK, set Token in session
$userManager->createUserToken($user);
}else {
// login failed , return error
}
Ideal for small project
Simple and easy!
https://github.com/devcoder-xyz/php-user-authentication
Top comments (4)
This is awesome. Thanks a lot for sharing. I am not sure how to validate submited username and password with a User database columns. Can you please provide an example of this password validation, please?
i add login exemple with password validation
Why you decided to use trait here?
It Can be use anywhere to check password or crypte password ( example In a Controller ) without call UserManager if you want and I don't want to have a class with a lot of lines so that it is easily maintainable.