jasny/auth

Authentication and authorization

v1.0.1 2016-12-28 23:01 UTC

README

Build Status Scrutinizer Code Quality Code Coverage SensioLabsInsight Packagist Stable Version Packagist License

Authentication, authorization and access control for PHP.

Installation

Install using composer

composer require jasny/auth

Setup

Auth is an abstract class. You need to extend it and implement the abstract methods fetchUserById and fetchUserByUsername.

You also need to specify how the current user is persisted across requests. If you want to use normal PHP sessions, you can simply use the Auth\Sessions trait.

class Auth extends Jasny\Auth
{
    use Jasny\Auth\Sessions;

    /**
     * Fetch a user by ID
     * 
     * @param int $id
     * @return Jasny\Auth\User
     */
    public function fetchUserById($id)
    {
        // Database action that fetches a User object
    }

    /**
     * Fetch a user by username
     * 
     * @param string $username
     * @return Jasny\Auth\User
     */
    public function fetchUserByUsername($username)
    {
        // Database action that fetches a User object
    }
}

The fetch methods need to return a object that implements the Jasny\Auth\User interface.

class User implements Jasny\Auth\User
{
    /**
     * @var int
     */
    public $id;

    /**
     * @var string
     */
    public $username;

    /**
     * Hashed password
     * @var string
     */
    public $password;

    /**
     * @var boolean
     */
    public $active;


    /**
     * Get the user ID
     * 
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Get the usermame
     * 
     * @return string
     */
    public function getUsername()
    {
        return $this->username;
    }

    /**
     * Get the hashed password
     * 
     * @return string
     */
    public function getHashedPassword()
    {
        return $this->password;
    }


    /**
     * Event called on login.
     * 
     * @return boolean  false cancels the login
     */
    public function onLogin()
    {
        if (!$this->active) {
            return false;
        }

        // You might want to log the login
    }

    /**
     * Event called on logout.
     */
    public function onLogout()
    {
        // You might want to log the logout
    }
}

Authorization

By default the Auth class only does authentication. Authorization can be added by implementing the Authz interface.

Two traits are predefined to do Authorization: Authz\ByLevel and Authz\ByGroup.

By level

The Authz\ByLevel traits implements authorization based on access levels. Each user get permissions for it's level and all levels below.

class Auth extends Jasny\Auth implements Jasny\Authz
{
    use Jasny\Authz\ByLevel;

    protected function getAccessLevels()
    {
        return [
            'user' => 1,
            'moderator' => 10,
            'admin' => 100,
            'superadmin' => 500
        ];
    }
}

If you get the levels from a database, make sure to save them in a property for performance.

class Auth extends Jasny\Auth implements Jasny\Authz
{
    use Jasny\Authz\ByGroup;

    protected $levels;

    protected function getAccessLevels()
    {
        if (!isset($this->levels)) {
            $this->levels = [];
            $result = $this->db->query("SELECT name, level FROM access_levels");

            while (($row = $result->fetchAssoc())) {
                $this->levels[$row['name']] = (int)$row['level'];
            }
        }

        return $this->levels;
    }
}

For authorization the user object also needs to implement Jasny\Authz\User, adding the getRole() method. This method must return the access level of the user, either as string or as integer.

/**
 * Get the access level of the user
 * 
 * @return int
 */
public function getRole()
{
    return $this->access_level;
}

By group

The Auth\ByGroup traits implements authorization using access groups. An access group may supersede other groups.

You must implement the getGroupStructure() method which should return an array. The keys are the names of the groups. The value should be an array with groups the group supersedes.

class Auth extends Jasny\Auth implements Jasny\Authz
{
    use Jasny\Authz\ByGroup;

    protected function getGroupStructure()
    {
        return [
            'users' => [],
            'managers' => [],
            'employees' => ['user'],
            'developers' => ['employees'],
            'paralegals' => ['employees'],
            'lawyers' => ['paralegals'],
            'lead-developers' => ['developers', 'managers'],
            'firm-partners' => ['lawyers', 'managers']
        ];
    }
}

If you get the structure from a database, make sure to save them in a property for performance.

class Auth extends Jasny\Auth implements Jasny\Authz
{
    use Jasny\Authz\ByGroup;

    protected $groups;

    protected function getGroupStructure()
    {
        if (!isset($this->groups)) {
            $this->groups = [];
            $result = $this->db->query("SELECT ...");

            while (($row = $result->fetchAssoc())) {
                $this->groups[$row['group']] = explode(';', $row['supersedes']);
            }
        }

        return $this->groups;
    }
}

For authorization the user object also needs to implement Jasny\Authz\User, adding the getRole() method. This method must return the role of the user or array of roles.

/**
 * Get the access groups of the user
 * 
 * @return string[]
 */
public function getRoles()
{
    return $this->roles;
}

Confirmation

By using the Auth\Confirmation trait, you can generate and verify confirmation tokens. This is useful to require a use to confirm signup by e-mail or for a password reset functionality.

You need to add a getConfirmationSecret() that returns a string that is unique and only known to your application. Make sure the confirmation secret is suffiently long, like 20 random characters. For added security, it's better to configure it through an environment variable rather than putting it in your code.

class Auth extends Jasny\Auth
{
  use Jasny\Auth\Confirmation;

  public function getConfirmationSecret()
  {
    return getenv('AUTH_CONFIRMATION_SECRET');
  }
}

Security

The confirmation token exists of the user id and a checksum, which is obfuscated using hashids.

A casual user will be unable to get the userid from the hash, but hashids is not a true encryption algorithm and with enough tokens a hacker might be able to determine the salt and extract the user id and checksum from tokens. Note that knowing the salt doesn't mean you know the configured secret.

The checksum is the first 16 bytes of the sha256 hash of user id + secret. For better security you might add want to use more than 12 characters. This does result in a larger string for the token.

class Auth extends Jasny\Auth
{
  ...

  protected function getConfirmationChecksum($id, $len = 32)
  {
    return parent::getConfirmationChecksum($id, $len);
  }

  ...
}

Usage

Authentication

Verify username and password

boolean verify(User $user, string $password)

Login with username and password

User|null login(string $username, string $password);

Set user without verification

User|null setUser(User $user)

If $user->onLogin() returns false, the user isn't set and the function returns null.

Logout

void logout()

Get current user

User|null user()

Authorization

Check if a user has a specific role or superseding role

boolean is(string $role)
if (!$auth->is('admin')) {
    http_response_code(403);
    echo "You're not allowed to see this page";
    exit();
}

Access control (middleware)

Check if a user has a specific role or superseding role

Jasny\Authz\Middleware asMiddleware(callback $getRequiredRole)

You can apply access control manually using the is() method. Alteratively, if you're using a PSR-7 compatible router with middleware support (like Jasny Router]).

The $getRequiredRole callback should return a boolean, string or array of string.

Returning true means a the request will only be handled if a user is logged in.

$auth = new Auth(); // Implements the Jasny\Authz interface

$router->add($auth->asMiddleware(function(ServerRequest $request) {
    return strpos($request->getUri()->getPath(), '/account/') === 0; // `/account/` is only available if logged in
}));

If the Auth class implements authorization (Authz) and the callback returns a string, the middleware will check if the user is authorized for that role. If an array of string is returned, the user should be authorized for at least one of the roles.

$auth = new Auth(); // Implements the Jasny\Authz interface

$router->add($auth->asMiddleware(function(ServerRequest $request) {
    $route = $request->getAttribute('route');
    return isset($route->auth) ? $route->auth : null;
}));

Confirmation

Signup confirmation

Get a verification token. Use it in an url and set that url in an e-mail to the user.

// Create a new $user

$auth = new Auth();
$confirmationToken = $auth->getConfirmationToken($user, 'signup');

$host = $_SERVER['HTTP_HOST'];
$url = "http://$host/confirm.php?token=$confirmationToken";
    
mail(
  $user->getEmail(),
  "Welcome to our site",
  "Please confirm your account by visiting $url"
);

Use the confirmation token to fetch and verify the user

// --- confirm.php

$auth = new Auth();
$user = $auth->fetchUserForConfirmation($_GET['token'], 'signup');

if (!$user) {
    http_response_code(400);
    echo "The token is not valid";
    exit();
}

// Process the confirmation
// ...

Forgot password

Get a verification token. Use it in an url and set that url in an e-mail to the user.

Setting the 3th argument to true will use the hashed password of the user in the checksum. This means that the token will stop working once the password is changed.

// Fetch $user by e-mail

$auth = new MyAuth();
$confirmationToken = $auth->getConfirmationToken($user, 'reset-password', true);

$host = $_SERVER['HTTP_HOST'];
$url = "http://$host/reset.php?token=$confirmationToken";

mail(
  $user->getEmail(),
  "Password reset request",
  "You may reset your password by visiting $url"
);

Use the confirmation token to fetch and verify resetting the password

$auth = new MyAuth();
$user = $auth->fetchUserForConfirmation($_GET['token'], 'reset-password', true);

if (!$user) {
    http_response_code(400);
    echo "The token is not valid";
    exit();
}

// Show form to set a password
// ...