Last active
June 20, 2017 18:36
-
-
Save gnat42/8104a7f23fc08d4ed9d59609aeea70f2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Created by PhpStorm. | |
* User: gnat | |
* Date: 2017-06-19 | |
* Time: 11:18 PM | |
*/ | |
namespace NS\CoreDomain\Practice\Credentials; | |
use NS\CoreDomain\Practice\User; | |
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; | |
class CredentialEncoder | |
{ | |
/** | |
* Symfony provided service | |
* Given a user instance, it returns an encoder that will transform their password into a hash | |
* Currently we've configured pbkdf2, with algo: sha256 and iterations: 1000 | |
* | |
* @var EncoderFactoryInterface | |
*/ | |
private $encoderFactory; | |
/** | |
* Currently we use aes-256-cbc | |
* @var string | |
*/ | |
private $method; | |
/** | |
* System encryption key ( initially generated via base64_encode(random_bytes(128)) ) | |
* @var string | |
*/ | |
private $key; | |
/** | |
* CredentialEncoder constructor. | |
* @param EncoderFactoryInterface $encoderFactory | |
* @param string $method | |
* @param string $key | |
*/ | |
public function __construct(EncoderFactoryInterface $encoderFactory, $method, $key) | |
{ | |
$this->encoderFactory = $encoderFactory; | |
$this->method = $method; | |
$this->key = $key; | |
} | |
/** | |
* @param User $user | |
* @param CredentialStore $credentialStore | |
* @param $password | |
* @return string | |
*/ | |
public function encodeUserCredentials(User $user, CredentialStore $credentialStore, $password) | |
{ | |
$encoder = $this->encoderFactory->getEncoder($user); | |
// Resets the User's stored salt via bin2hex(random_bytes(8)); The salt is used as the IV for openssl | |
$user->resetSalt(); | |
// generates key based off of pbkdf2 hash of provided password | |
$key = $encoder->encodePassword($password, $user->getSalt()); | |
return openssl_encrypt(serialize($credentialStore), $this->method, $key, false, $user->getSalt()); | |
} | |
/** | |
* @param User $user | |
* @param $currentPassword | |
* @param $newPassword | |
* @return string | |
*/ | |
public function reEncodeUserCredential(User $user, $currentPassword, $newPassword) | |
{ | |
$decoded = $this->decodeUserCredentials($user, $currentPassword); | |
return $this->encodeUserCredentials($user, $decoded, $newPassword); | |
} | |
/** | |
* @param User $user | |
* @param $password | |
* @return CredentialStore|false | |
*/ | |
public function decodeUserCredentials(User $user, $password) | |
{ | |
$encoder = $this->encoderFactory->getEncoder($user); | |
$key = $encoder->encodePassword($password, $user->getSalt()); | |
$data = openssl_decrypt($user->getCredentialStore(), $this->method, $key, false, $user->getSalt()); | |
if ($data === false) { | |
throw new \RuntimeException("Unable to decode via password"); | |
} | |
return unserialize($data); | |
} | |
/** | |
* Given unencrypted user credential data, encrypt using system key. | |
* This allows us access to the credential data we're storing on behalf of the user | |
* in background processes initiated by the user on the front end. | |
* | |
* TODO: Investigate changing this to a libsodium create_box where we encrypt it for | |
* the backend system specifically. Only backend servers could decrypt it then. Thus | |
* requiring two systems to be compromised. This of course is a moot point if someone gains | |
* root on this system as they could modify this code and output the decrypted credentials. | |
* Should these be stored using something like Vault instead?? | |
* | |
* @param $credential | |
* @return string | |
*/ | |
public function encodeSystemCredentials($credential) | |
{ | |
$iv = random_bytes(16); | |
return base64_encode("$iv::" . openssl_encrypt(serialize($credential), $this->method, $this->key, false, $iv)); | |
} | |
/** | |
* @param string $encryptedStr | |
* @return mixed | |
*/ | |
public function decodeSystemCredentials(string $encryptedStr) | |
{ | |
if (strpos($encryptedStr, '::') === false) { | |
$encryptedStr = base64_decode($encryptedStr); | |
if (strpos($encryptedStr, '::') === false) { | |
throw new \RuntimeException('Unable to decrypt'); | |
} | |
} | |
$data = explode('::', $encryptedStr); | |
return unserialize(openssl_decrypt($data[1], $this->method, $this->key, false, $data[0])); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Created by PhpStorm. | |
* User: gnat | |
* Date: 19/06/17 | |
* Time: 1:48 PM | |
*/ | |
namespace NS\SiteBundle\Authentication; | |
use NS\CoreDomain\Practice\Credentials\CredentialEncoder; | |
use Symfony\Component\HttpFoundation\RedirectResponse; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Session\SessionInterface; | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
use Symfony\Component\Security\Core\User\UserProviderInterface; | |
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; | |
use Symfony\Component\Security\Http\Util\TargetPathTrait; | |
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
use TargetPathTrait; | |
/** @var CredentialEncoder */ | |
private $credentialEncoder; | |
/** @var string */ | |
private $loginUrl; | |
/** @var string */ | |
private $successUrl; | |
/** | |
* FormLoginAuthenticator constructor. | |
* @param CredentialEncoder $credentialEncoder | |
* @param string $loginUrl | |
* @param string $successUrl | |
*/ | |
public function __construct(CredentialEncoder $credentialEncoder, string $loginUrl, string $successUrl) | |
{ | |
$this->credentialEncoder = $credentialEncoder; | |
$this->loginUrl = $loginUrl; | |
$this->successUrl = $successUrl; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
protected function getLoginUrl() | |
{ | |
return $this->loginUrl; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function getCredentials(Request $request) | |
{ | |
if ($request->request->has('_username')) { | |
return [ | |
'username' => $request->request->get('_username'), | |
'password' => $request->request->get('_password'), | |
]; | |
} | |
return null; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
$user = $userProvider->loadUserByUsername($credentials['username']); | |
return $user; | |
} | |
/** | |
* @var string|null | |
*/ | |
private $credentials = false; | |
/** | |
* @inheritDoc | |
*/ | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
$this->credentials = $this->credentialEncoder->decodeUserCredentials($user, $credentials['password']); | |
// If the user's password was correct, take the decrypted data and re-encrypt using system key | |
// For use in background non-session based access | |
if ($this->credentials !== false) { | |
$this->credentials = $this->credentialEncoder->encodeSystemCredentials($this->credentials); | |
return true; | |
} | |
return false; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function createAuthenticatedToken(UserInterface $user, $providerKey) | |
{ | |
$token = new EncryptedStoreToken($user, $providerKey, $user->getRoles()); | |
// System key encrypted data | |
$token->setCredentials($this->credentials); | |
$this->credentials = false; | |
return $token; | |
} | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) | |
{ | |
$targetPath = null; | |
// if the user hit a secure page and start() was called, this was | |
// the URL they were on, and probably where you want to redirect to | |
if ($request->getSession() instanceof SessionInterface) { | |
$targetPath = $this->getTargetPath($request->getSession(), $providerKey); | |
} | |
if (!$targetPath) { | |
$targetPath = $this->successUrl; | |
} | |
return new RedirectResponse($targetPath); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
We have a situation where we are interacting with API's that don't use OAuth, but expect username/passwords. They were designed to be interacted via desktop applications not web applications. Since most of our interactions with these services will be handled by background processes without access to the session data. We need to
The pseudo implementation is as follows:
Thoughts? Weaknesses? Anything you think I'm doing wrong or the cipher+mode issues I should know about?
I have considered changing my re-encode function to use lib sodium so that its encrypted such that only the backend system can read the data. Not sure how much more security that adds and what type of attack it would protect me from.