I am working on a new Symfony 5.3.6 project and want to implement authentication, based on the new system as stated in:
https://symfony.com/doc/current/security/authenticator_manager.html#creating-a-custom-authenticator
I do not have any users and just want to check if the sent api token is correct, so when implementing this method:
public function authenticate(Request $request): PassportInterface
{
$apiToken = $request->headers->get('X-AUTH-TOKEN');
if (null === $apiToken) {
// The token header was empty, authentication fails with HTTP Status Code 401 "Unauthorized"
throw new CustomUserMessageAuthenticationException('No API token provided');
}
return new SelfValidatingPassport(new UserBadge($apiToken));
}
where exactly is the checking done? Have i forgotten to implement another Class somewhere?
If I leave the code as is it lands directly in onAuthenticationFailure.
I understand, that I could implement Users/UserProvider with an attribute $apiToken and then the system would check if the database entry corresponds with the token in the request. But i do not have users.
It should be possible without having users, because on the above URL, it says:
Self Validating Passport
If you don’t need any credentials to be checked (e.g. when using API
tokens), you can use the
Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport.
This class only requires a UserBadge object and optionally Passport
Badges.
But that is a little thin. How do I "use" it?
Ok, I think I got the point, in any case, you need to handle some User & then you need to create a customer Userprovider.
Here my logic:
App\Security\UserProvider:
class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
public function loadUserByIdentifier($identifier): UserInterface
{
if ($identifier === 'YOUR_API_KEY') {
return new User();
}
throw new UserNotFoundException('API Key is not correct');
}
...
App\Security\ApiKeyAuthenticator:
class ApiKeyAuthenticator extends AbstractAuthenticator
{
private UserProvider $userProvider;
public function __construct(UserProvider $userProvider)
{
$this->userProvider = $userProvider;
}
public function supports(Request $request): ?bool
{
// allow api docs page
return trim($request->getPathInfo(), '/') !== 'docs';
}
public function authenticate(Request $request): Passport
{
$apiToken = $request->headers->get('X-API-KEY');
if (null === $apiToken) {
// The token header was empty, authentication fails with HTTP Status
// Code 401 "Unauthorized"
throw new CustomUserMessageAuthenticationException('No API token provided');
}
return new SelfValidatingPassport(
new UserBadge($apiToken, function () use ($apiToken) {
return $this->userProvider->loadUserByIdentifier($apiToken);
})
);
}
It works for me, my API is protected by a basic API Key in the header. I don't know if it's the best way, but seems ok.
And define in your security.yaml:
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
id: App\Security\UserProvider
You can use next validation
return new SelfValidatingPassport(
new UserBadge($apiToken, function() use ($apiToken) {
// TODO: here you can implement any check
})
);
Related
I'm trying to set a response on an eventsubscriber that checks if an API authorization token it's correct
class TokenSubscriber implements EventSubscriberInterface
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if ($controller[0] instanceof TokenAuthenticatedController) {
$apiKey = $this->em->getRepository('AppBundle:ApiKey')->findOneBy(['enabled' => true, 'name' => 'apikey'])->getApiKey();
$token = $event->getRequest()->headers->get('x-auth-token');
if ($token !== $apiKey) {
//send response
}
}
}
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => 'onKernelController',
];
}
}
But I cant stop the current request and return a respone as a controller, what is the correct way to send a response with an error message and stop the current request
You can not do that using the FilterControllerEvent Event. On that moment, symfony already decided which controller to execute. I think you might want to look into the Symfony Security component. It can protect routes like what you want, but in a slightly different way (access_control and/or annotations).
If you want to block access to an API (eg. JSON), you easily follow this doc. You can also mix it using the Security annotations on your controllers or actions using this doc
I think you can throw an error here
throw new AccessDeniedHttpException('Your message here!');
In my application, users with the role ROLE_ADMIN are able to manually disable other user accounts, by setting enabled on the user account to false.
Using a user checker, the user will not be able to log in the next time they try:
public function checkPostAuth(UserInterface $user)
{
if (!$user->isEnabled()) {
throw new CustomUserMessageAuthenticationException(
'Account is not enabled.'
);
}
}
My issue with this, it only works when a user tries to log in. If a user is currently logged in (could also be using the remember-me-functionality), nothing happens until they logout.
Is there any way to immediately disable the user from making requests to routes it no longer should have access to, even when this user is currently logged in?
What I could do is check if the account is enabled for every route that requires this user access, like so:
if ($this->getUser()->isEnabled() === false) {
throw new \Exception("Account disabled");
}
But this seems like a terrible solution because I would need this in many places.
If you implement the Symfony\Component\Security\Core\User\EquatableInterface in your User class, the User will be logged out when isEqualTo() returns false.
class User implements Symfony\Component\Security\Core\User\EquatableInterface
{
/* ... */
public function isEqualTo(UserInterface $user)
{
if (!$user instanceof User) {
return false;
}
if ($this->enabled !== $user->enabled) {
// Forces the user to log in again if enabled
// changes from true to false or vice-versa.
// This could be replaced by a more sophisticated comparison.
return false;
}
// do a bunch of other checks, such as comparing the
// password hash: this will cause the user to be logged
// out when their password is changed.
return true;
}
/* ... */
}
Relevant documentation
I followed Cerad's advice, and added a listener as can be seen below. However, for a better (built-in) solution, see Pete's answer.
namespace App\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
class RequestListener
{
private $security; // needed to get the user
private $router; // needed to generate the logout-url
public function __construct(Security $security, UrlGeneratorInterface $router)
{
$this->security = $security;
$this->router = $router;
}
public function __invoke(RequestEvent $event) : void
{
$user = $this->security->getUser();
// Don't do anything if no user is logged in.
if ($user === null) {
return;
}
if ($user->isEnabled() === false) {
// Generate the logout url based on the path name and redirect to it.
$url = $this->router->generate('app_logout');
$event->setResponse(new RedirectResponse($url));
}
}
}
For the record, I'm using PHP 7.0.0, in a Vagrant Box, with PHPStorm. Oh, and Symfony 3.
I'm following the API Key Authentication documentation. My goal is:
To allow the user to provide a key as a GET apiKey parameter to authenticate for any route, except the developer profiler etc obviously
To allow the developer to write $request->getUser() in a controller to get the currently logged in user
My problem is that, although I believe I've followed the documentation to the letter, I'm still getting a null for $request->getUser() in the controller.
Note: I've removed error checking to keep the code short
ApiKeyAuthenticator.php
The thing that processes the part of the request to grab the API key from it. It can be a header or anything, but I'm sticking with apiKey from GET.
Differences from documentation, pretty much 0 apart from that I'm trying to keep the user authenticated in the session following this part of the docs.
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
$apiKey = $request->query->get('apiKey');
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
// The part where we try and keep the user in the session!
$user = $token->getUser();
if ($user instanceof ApiKeyUser) {
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
$user = $userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
}
ApiKeyUserProvider.php
The custom user provider to load a user object from wherever it can be loaded from - I'm sticking with the default DB implementation.
Differences: only the fact that I have to inject the repository into the constructor to make calls to the DB, as the docs allude to but don't show, and also returning $user in refreshUser().
class ApiKeyUserProvider implements UserProviderInterface
{
protected $repo;
// I'm injecting the Repo here (docs don't help with this)
public function __construct(UserRepository $repo)
{
$this->repo = $repo;
}
public function getUsernameForApiKey($apiKey)
{
$data = $this->repo->findUsernameByApiKey($apiKey);
$username = (!is_null($data)) ? $data->getUsername() : null;
return $username;
}
public function loadUserByUsername($username)
{
return $this->repo->findOneBy(['username' => $username]);
}
public function refreshUser(UserInterface $user)
{
// docs state to return here if we don't want stateless
return $user;
}
public function supportsClass($class)
{
return 'Symfony\Component\Security\Core\User\User' === $class;
}
}
ApiKeyUser.php
This is my custom user object.
The only difference I have here is that it contains doctrine annotations (removed for your sanity) and a custom field for the token. Also, I removed \Serializable as it didn't seem to be doing anything and apparently Symfony only needs the $id value to recreate the user which it can do itself.
class ApiKeyUser implements UserInterface
{
private $id;
private $username;
private $password;
private $email;
private $salt;
private $apiKey;
private $isActive;
public function __construct($username, $password, $salt, $apiKey, $isActive = true)
{
$this->username = $username;
$this->password = $password;
$this->salt = $salt;
$this->apiKey = $apiKey;
$this->isActive = $isActive;
}
//-- SNIP getters --//
}
security.yml
# Here is my custom user provider class from above
providers:
api_key_user_provider:
id: api_key_user_provider
firewalls:
# Authentication disabled for dev (default settings)
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# My new settings, with stateless set to false
secured_area:
pattern: ^/
stateless: false
simple_preauth:
authenticator: apikey_authenticator
provider:
api_key_user_provider
services.yml
Obviously I need to be able to inject the repository into the provider.
api_key_user_repository:
class: Doctrine\ORM\EntityRepository
factory: ["#doctrine.orm.entity_manager", getRepository]
arguments: [AppBundle\Security\ApiKeyUser]
api_key_user_provider:
class: AppBundle\Security\ApiKeyUserProvider
factory_service: doctrine.orm.default_entity_manager
factory_method: getRepository
arguments: ["#api_key_user_repository"]
apikey_authenticator:
class: AppBundle\Security\ApiKeyAuthenticator
public: false
Debugging. It's interesting to note that, in ApiKeyAuthenticator.php, the call to $user = $token->getUser(); in authenticateToken() always shows an anon. user, so it's clearly not being stored in the session.
Also note how at the bottom of the authenticator we do actually return a new PreAuthenticatedToken with a user found from the database:
So it's clearly found me and is returning what it's supposed to here, but the user call in the controller returns null. What am I doing wrong? Is it a failure to serialise into the session because of my custom user or something? I tried setting all the user properties to public as somewhere in the documentation suggested but that made no difference.
So it turns out that calling $request->getUser() in the controller doesn't actually return the currently authenticated user as I would have expected it to. This would make the most sense for this object API imho.
If you actually look at the code for Request::getUser(), it looks like this:
/**
* Returns the user.
*
* #return string|null
*/
public function getUser()
{
return $this->headers->get('PHP_AUTH_USER');
}
That's for HTTP Basic Auth! In order to get the currently logged in user, you need to do this every single time:
$this->get('security.token_storage')->getToken()->getUser();
This does, indeed, give me the currently logged in user. Hopefully the question above shows how to authenticate successfully by API token anyway.
Alternatively, don't call $this->get() as it's a service locator. Decouple yourself from the controller and inject the token service instead to get the token and user from it.
To get the currently logged in User inside your Controller simply call:
$this->getUser();
This will refer to a method in Symfony's ControllerTrait, which basically wraps the code provided in Jimbo's answer.
protected function getUser()
{
if (!$this->container->has('security.token_storage')) {
throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".');
}
if (null === $token = $this->container->get('security.token_storage')->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return;
}
return $user;
}
I'm currently using Laravel 5 Authentification, but I have edited it to allow me to connect to an API server instead of an Eloquent model.
Here is the code of my custom UserProvider:
<?php namespace App\Auth;
use Illuminate\Contracts\Auth\UserProvider as UserProviderInterface;
use WDAL;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Auth\GenericUser;
use Session;
class WolfUserProvider implements UserProviderInterface {
private $_loggedUser;
public function __construct()
{
$this->_loggedUser = null;
$user = Session::get('user');
if (!empty($user)) {
$this->_loggedUser = unserialize($user);
}
}
public function retrieveById($id)
{
return $this->_loggedUser;
}
public function retrieveByToken($identifier, $token)
{
return null;
}
public function updateRememberToken(Authenticatable $user, $token)
{
//dd('updateRememberToken');
}
public function retrieveByCredentials(array $credentials)
{
$user = WDAL::getContactCredentials($credentials['login']);
return $user;
}
public function validateCredentials(Authenticatable $user, array $credentials)
{
if($user->username == $credentials['login'] && $user->password == $credentials['password']){
$this->_loggedUser = $user;
Session::set('user', serialize($user));
return true;
}
else{
return false;
}
}
}
?>
This code might not be perfect as it still in early development ;-) (feel free to suggest me some ideas of improvement if you want to)
So when the user is logged, it has access to the whole platform and to several views and can communicate with the API server to display and edit data.
Sometimes, the API server can return "Invalid Session ID" and when my Model gets this message, the user should be redirected to the login page.
From a Controller it's really easy to handle I can use this code (logout link):
public function getLogout()
{
$this->auth->logout();
Session::flush();
return redirect('/');
}
But do you know how I should proceed from a Model ? I could of course edit all my controllers to check for the value returned by the Model to logout, but cannot it be done thanks to middlewares?
It seems to be really long to edit all my controllers, and this will imply a lot of duplicated code.
One of my tries was to throw an exception from the Controller, and catch in from the auth middleware.
It was not working, because I didn't write use Exception;
I'm now catching the exception, and can now redirect the user from the middleware.
Thank you anyway!
First of all sorry about my english, I'll try to do my best.
Im new to Laravel, im trying to implement custom auth throught a SOAP WS, I declare new class that implement UserProviderInterface. I success on implement retrieveByCredentials and validateCredentials methods but since i dont have access to database or global users information i cant implement retrieveByID method. Is there any way to make custom Auth not based on users id's ?
I need:
- Login and validate user throught SOAP WS
- Store User Info returned by WS.
- Remember me functionality
- Secure routes based on logged user and level of access
- Logout
Implemented class:
<?php
namespace Spt\Common\Providers;
use Illuminate\Auth\UserProviderInterface;
use Illuminate\Auth\GenericUser;
use Illuminate\Auth\UserInterface;
class AuthUserProvider implements UserProviderInterface{
private $user;
public function __construct(){
$this->user = null;
}
public function retrieveByID($identifier){
return $this->user;
}
public function retrieveByCredentials(array $credentials){
$client = new \SoapClient('webserviceurl');
$res = $client->Validar_Cliente($credentials);
$res = $res->Validar_ClienteResult;
if($res->infoError->bError === true){
return;
}
$res->id = $res->id_cliente;
$user = new GenericUser((array) $res);
return $user;
}
public function validateCredentials(UserInterface $user, array $credentials){
//Assumed that if WS returned a User is validated
return true;
}
}
I think that re-implement UserProviderInterface its not the solution but i googled and not found other way
Any Idea?
You're almost done, apart from the fact that private variable $user of AuthUserProvider doesn't survive the current http request. If you cannot "retrieve by id" from your web service, I guess the only way is to store the entire user in the session - Laravel itself stores the user's id in the session and the fact that it stores only the id (not the entire user) is one of the reasons why a retrieveByID method is needed.
The following is only to clarify and is untested.
class AuthUserProvider implements UserProviderInterface {
public function retrieveByCredentials(array $credentials) {
$client = new \SoapClient('webserviceurl');
$res = $client->Validar_Cliente($credentials);
$res = $res->Validar_ClienteResult;
if($res->infoError->bError === true) {
return;
}
$res->id = $res->id_cliente;
Session::put('entireuser', $res);
$user = new GenericUser((array) $res);
return $user;
}
public function retrieveByID($identifier) {
$res = Session::get('entireuser');
return new GenericUser((array) $res);
}
// ...
}
If you can't retrieve by id from your web service, I guess you cannot either retrieve by remember token, so it may be impossible for you to implement the "remember me" functionality, unless you store part of users data in a second database (which at that point could be used in place of the session above).