Advertisement
Guest User

Cookie Only Authentication Prototype

a guest
Jul 4th, 2016
539
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 13.14 KB | None | 0 0
  1. <?php
  2.  
  3. namespace CirclicalUser\Service;
  4.  
  5. use CirclicalUser\Entity\Authentication;
  6. use CirclicalUser\Entity\User;
  7. use CirclicalUser\Exception\BadPasswordException;
  8. use CirclicalUser\Exception\NoSuchUserException;
  9. use CirclicalUser\Mapper\AuthenticationMapper;
  10. use CirclicalUser\Mapper\UserMapper;
  11. use ParagonIE\Halite\KeyFactory;
  12. use ParagonIE\Halite\Symmetric\Crypto;
  13. use ParagonIE\Halite\Symmetric\EncryptionKey;
  14.  
  15.  
  16. /**
  17.  * Cookie-based authentication service that gives the option of using transient sessions.
  18.  *
  19.  * Class Authentication
  20.  *
  21.  * @package LDP\Service
  22.  */
  23. class AuthenticationService
  24. {
  25.     /**
  26.      * User cookie, which is verified by COOKIE_VERIFY_A, and contains the name of a randomly generated cookie
  27.      */
  28.     const COOKIE_USER = '_sessiona';
  29.  
  30.     /**
  31.      * SHA256 hmac combination that verifies COOKIE_VERIFY_A
  32.      */
  33.     const COOKIE_VERIFY_A = '_sessionb';
  34.  
  35.     /**
  36.      * SHA256 hmac combination that verifies a randomly generated cookie
  37.      */
  38.     const COOKIE_VERIFY_B = '_sessionc';
  39.  
  40.     /**
  41.      * Stores the user identity after having been authenticated.
  42.      *
  43.      * @var User
  44.      */
  45.     private $identity;
  46.  
  47.     /**
  48.      * @var AuthenticationMapper
  49.      */
  50.     private $authenticationMapper;
  51.  
  52.     /**
  53.      * @var UserMapper
  54.      */
  55.     private $userMapper;
  56.  
  57.     /**
  58.      * @var string A config-defined key that's used to encrypt ID cookie
  59.      */
  60.     private $systemEncryptionKey;
  61.  
  62.     /**
  63.      * @var bool Should the cookie expire at the end of the session?
  64.      */
  65.     private $transient;
  66.  
  67.     /**
  68.      * @var bool Should the cookie be marked as https only?
  69.      */
  70.     private $secure;
  71.  
  72.  
  73.     /**
  74.      * AuthenticationService constructor.
  75.      *
  76.      * @param AuthenticationMapper $authenticationMapper
  77.      * @param UserMapper $userMapper
  78.      * @param string $systemEncryptionKey The raw material of a Halite-generated encryption key, stored in config.
  79.      * @param bool $transient True if cookies should expire at the end of the session (zero value, for expiry)
  80.      * @param bool $secure True if cookies should be marked as 'Secure', enforced as 'true' in production by this service's Factory
  81.      */
  82.     public function __construct(AuthenticationMapper $authenticationMapper, UserMapper $userMapper, $systemEncryptionKey, $transient, $secure)
  83.     {
  84.         $this->authenticationMapper = $authenticationMapper;
  85.         $this->userMapper = $userMapper;
  86.         $this->systemEncryptionKey = $systemEncryptionKey;
  87.         $this->transient = $transient;
  88.         $this->secure = $secure;
  89.         $this->identity = null;
  90.     }
  91.  
  92.     /**
  93.      * Check to see if a user is logged in
  94.      * @return bool
  95.      */
  96.     public function hasIdentity() : bool
  97.     {
  98.         return $this->getIdentity() != null;
  99.     }
  100.  
  101.     /**
  102.      * Authenticate a user
  103.      * @param User $user
  104.      */
  105.     private function setIdentity(User $user)
  106.     {
  107.         $this->identity = $user;
  108.     }
  109.  
  110.  
  111.     /**
  112.      * Passed in by a successful form submission, should set proper auth cookies if the identity verifies.
  113.      * The login should work with both username, and email address.
  114.      *
  115.      * @param $username
  116.      * @param $password
  117.      * @return User
  118.      *
  119.      * @throws BadPasswordException Thrown when the password doesn't work
  120.      * @throws NoSuchUserException Thrown when the user can't be identified
  121.      */
  122.     public function authenticate($username, $password) : User
  123.     {
  124.         $auth = $this->authenticationMapper->findByUsername($username);
  125.  
  126.         if (!$auth) {
  127.             if ($user = $this->userMapper->findByEmail($username)) {
  128.                 $auth = $this->authenticationMapper->findByUserId($user->getId());
  129.             }
  130.         }
  131.  
  132.         if (!$auth) {
  133.             throw new NoSuchUserException();
  134.         }
  135.  
  136.         if (password_verify($password, $auth->getHash())) {
  137.             $user = $this->userMapper->getUser($auth->getUserId());
  138.             if ($user) {
  139.                 $this->resetAuthenticationKey($auth);
  140.                 $this->setSessionCookies($auth);
  141.                 $this->setIdentity($user);
  142.  
  143.                 return $user;
  144.             } else {
  145.                 throw new NoSuchUserException();
  146.             }
  147.         }
  148.  
  149.         throw new BadPasswordException();
  150.     }
  151.  
  152.  
  153.     /**
  154.      * Change an auth record username given a user id and a new username.
  155.      * Note - in this case username is email.
  156.      *
  157.      * @param User $user
  158.      * @param $username
  159.      * @throws NoSuchUserException Thrown when the user's authentication records couldn't be found
  160.      */
  161.     public function changeUsername(User $user, $username)
  162.     {
  163.         /** @var Authentication $auth */
  164.         $auth = $this->authenticationMapper->findByUserId($user->getId());
  165.  
  166.         if (!$auth) {
  167.             throw new NoSuchUserException();
  168.         }
  169.  
  170.         $auth->setUsername($username);
  171.         $this->authenticationMapper->update($auth);
  172.     }
  173.  
  174.  
  175.     /**
  176.      * Set the auth session cookies that can be used to regenerate the session on subsequent visits
  177.      * @param Authentication $authentication
  178.      */
  179.     private function setSessionCookies(Authentication $authentication)
  180.     {
  181.         $systemKey = new EncryptionKey($this->systemEncryptionKey);
  182.         $userKey = new EncryptionKey($authentication->getSessionKey());
  183.         $hashCookieName = hash_hmac('sha256', $authentication->getSessionKey() . $authentication->getUsername(), $systemKey);
  184.         $userTuple = base64_encode(Crypto::encrypt($authentication->getUserId() . ":" . $hashCookieName, $systemKey));
  185.         $hashCookieContents = base64_encode(Crypto::encrypt(time() . ':' . $authentication->getUserId() . ':' . $authentication->getUsername(), $userKey));
  186.  
  187.         //
  188.         // 1 - Set the cookie that contains the user ID, and hash cookie name
  189.         //
  190.         $this->setCookie(
  191.             self::COOKIE_USER,
  192.             $userTuple
  193.         );
  194.  
  195.         //
  196.         // 2 - Set the cookie with random name, that contains a verification hash, that's a function of the switching session key
  197.         //
  198.         $this->setCookie(
  199.             $hashCookieName,
  200.             $hashCookieContents
  201.         );
  202.  
  203.         //
  204.         // 3 - Set the sign cookie, that acts as a safeguard against tampering
  205.         //
  206.         $this->setCookie(
  207.             self::COOKIE_VERIFY_A,
  208.             hash_hmac('sha256', $userTuple, $systemKey)
  209.         );
  210.  
  211.         //
  212.         // 4 - Set a sign cookie for the hashCookie's values
  213.         //
  214.         $this->setCookie(
  215.             self::COOKIE_VERIFY_B,
  216.             hash_hmac('sha256', $hashCookieContents, $userKey)
  217.         );
  218.     }
  219.  
  220.     /**
  221.      * Set a cookie with values defined by configuration
  222.      * @param $name
  223.      * @param $value
  224.      */
  225.     private function setCookie($name, $value)
  226.     {
  227.         $expiry = $this->transient ? 0 : (time() + 2629743);
  228.         $sessionParameters = session_get_cookie_params();
  229.         setcookie(
  230.             $name,
  231.             $value,
  232.             $expiry,
  233.             '/',
  234.             $sessionParameters['domain'],
  235.             $this->secure,
  236.             true
  237.         );
  238.     }
  239.  
  240.     /**
  241.      * Rifle through 4 cookies, ensuring that all details line up.  If they do, we accept that the cookies authenticate
  242.      * a specific user.
  243.      *
  244.      * Some notes:
  245.      *
  246.      *  - COOKIE_VERIFY_A is a do-not-decrypt check of COOKIE_USER
  247.      *  - COOKIE_VERIFY_B is a do-not-decrypt check of the random-named-cookie specified by COOKIE_USER
  248.      *  - COOKIE_USER has its contents encrypted by the system key
  249.      *  - the random-named-cookie has its contents encrypted by the user key
  250.      *
  251.      * @see self::setSessionCookies
  252.      * @return User|null
  253.      */
  254.     public function getIdentity()
  255.     {
  256.         if ($this->identity) {
  257.             return $this->identity;
  258.         }
  259.  
  260.         if (!isset($_COOKIE[self::COOKIE_VERIFY_A])) {
  261.             return null;
  262.         }
  263.  
  264.         if (!isset($_COOKIE[self::COOKIE_USER])) {
  265.             return null;
  266.         }
  267.  
  268.         $systemKey = new EncryptionKey($this->systemEncryptionKey);
  269.         $verificationCookie = $_COOKIE[self::COOKIE_VERIFY_A];
  270.         $hashPass = hash_equals(
  271.             hash_hmac('sha256', $_COOKIE[self::COOKIE_USER], $systemKey),
  272.             $verificationCookie
  273.         );
  274.  
  275.         //
  276.         // 1. Is the verify cookie still equivalent to the user cookie, if so, do not decrypt
  277.         //
  278.         if (!$hashPass) {
  279.             return null;
  280.         }
  281.  
  282.         //
  283.         // 2. If the user cookie was not tampered with, decrypt its contents with the system key
  284.         //
  285.         try {
  286.  
  287.             $userTuple = Crypto::decrypt(base64_decode($_COOKIE[self::COOKIE_USER]), $systemKey);
  288.  
  289.             if (strpos($userTuple, ':') == false) {
  290.                 return null;
  291.             }
  292.  
  293.             // paranoid, make sure we have everything we need
  294.             @list($cookieUserId, $hashCookieName) = @explode(":", $userTuple, 2);
  295.             if (!isset($cookieUserId) || !isset($hashCookieName) || !is_numeric($cookieUserId) || !trim($hashCookieName)) {
  296.                 return null;
  297.             }
  298.  
  299.             /** @var Authentication $auth */
  300.             if (!($auth = $this->authenticationMapper->findByUserId($cookieUserId))) {
  301.                 return null;
  302.             }
  303.  
  304.             //
  305.             // 2. Check the hashCookie for corroborating data
  306.             //
  307.             if (!isset($_COOKIE[$hashCookieName])) {
  308.                 return null;
  309.             }
  310.  
  311.             $userKey = new EncryptionKey($auth->getSessionKey());
  312.             $hashPass = hash_equals(
  313.                 hash_hmac('sha256', $_COOKIE[$hashCookieName], $userKey),
  314.                 $_COOKIE[self::COOKIE_VERIFY_B]
  315.             );
  316.  
  317.             if (!$hashPass) {
  318.                 return null;
  319.             }
  320.  
  321.             //
  322.             // 3. Decrypt the hash cookie with the user key
  323.             //
  324.             $hashedCookieContents = Crypto::decrypt(base64_decode($_COOKIE[$hashCookieName]), $userKey);
  325.             if (!substr_count($hashedCookieContents, ':') == 2) {
  326.                 return null;
  327.             }
  328.  
  329.             list(, $hashedUserId, $hashedUsername) = explode(':', $hashedCookieContents);
  330.             if ($hashedUserId != $cookieUserId) {
  331.                 return null;
  332.             }
  333.  
  334.             if ($hashedUsername != $auth->getUsername()) {
  335.                 return null;
  336.             }
  337.  
  338.             $user = $this->userMapper->getUser($auth->getUserId());
  339.             if ($user) {
  340.                 $this->setIdentity($user);
  341.  
  342.                 return $this->identity;
  343.             }
  344.  
  345.         } catch (\Exception $x) {
  346.             return null;
  347.         }
  348.  
  349.         return null;
  350.     }
  351.  
  352.  
  353.     /**
  354.      * Reset this user's password
  355.      *
  356.      * @param User $user The user to whom this password gets assigned
  357.      * @param string $newPassword Cleartext password that's being hashed
  358.      * @throws NoSuchUserException
  359.      */
  360.     public function resetPassword(User $user, $newPassword)
  361.     {
  362.         $auth = $this->authenticationMapper->findByUserId($user->getId());
  363.         if (!$auth) {
  364.             throw new NoSuchUserException();
  365.         }
  366.  
  367.         $hash = password_hash($newPassword, PASSWORD_DEFAULT);
  368.         $auth->setHash($hash);
  369.         $this->resetAuthenticationKey($user->getId());
  370.         $this->authenticationMapper->update($auth);
  371.     }
  372.  
  373.  
  374.     /**
  375.      * Register a new user into the auth tables, and, log them in
  376.      *
  377.      * @param User $user
  378.      * @param      $password
  379.      */
  380.     public function create(User $user, $password)
  381.     {
  382.         $hash = password_hash($password, PASSWORD_DEFAULT);
  383.         $auth = new Authentication(
  384.             $user->getId(),
  385.             $user->getEmail(),
  386.             $hash,
  387.             KeyFactory::generateEncryptionKey()->getRawKeyMaterial()
  388.         );
  389.  
  390.         $this->authenticationMapper->save($auth);
  391.         $this->setSessionCookies($auth);
  392.         $this->setIdentity($user);
  393.     }
  394.  
  395.  
  396.     /**
  397.      * Resalt a user's authentication table salt
  398.      *
  399.      * @param int $userId
  400.      *
  401.      * @return Authentication
  402.      * @throws \Exception
  403.      */
  404.     private function resetAuthenticationKey($userId) : Authentication
  405.     {
  406.         /** @var Authentication $auth */
  407.         $auth = $this->authenticationMapper->findByUserId($userId);
  408.  
  409.         if ($auth) {
  410.             $key = KeyFactory::generateEncryptionKey();
  411.             $auth->setSessionKey($key->getRawKeyMaterial());
  412.             $this->authenticationMapper->update($auth);
  413.  
  414.             return $auth;
  415.         }
  416.  
  417.         throw new \Exception("Couldn't reset session for a user that doesn't exist");
  418.  
  419.     }
  420.  
  421.  
  422.     /**
  423.      * Logout.  Reset the user authentication key, and delete all cookies.
  424.      */
  425.     public function clearIdentity()
  426.     {
  427.         if ($user = $this->getIdentity()) {
  428.             $this->resetAuthenticationKey($user->getId());
  429.         }
  430.  
  431.         $sp = session_get_cookie_params();
  432.         foreach ([self::COOKIE_USER, self::COOKIE_VERIFY_A, self::COOKIE_VERIFY_B] as $cookieName) {
  433.             setcookie($cookieName, null, null, '/', $sp['domain'], false, true);
  434.         }
  435.  
  436.         $this->identity = null;
  437.     }
  438.  
  439. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement