FOSUserbundle + Additional HTTP auth without user getting ROLE_USER - security

I have a "little" problem with the Symfony2 security system. This is what I need to achieve:
/ needs a HTTP auth BEFORE any page can be seen. I want to protect the whole page with a constant user / pass pair. After the user has entered the right pair, he should be a guest (and not ROLE_USER) und be able to login via the FOSUserBundle form
/api needs a separate login via HTTP auth, independent from FOSUserBundle and the other HTTP auth
I already managed to provide a separate login for the API. This is my complete security.yml:
security:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
in_memory_http:
memory:
users:
User1: { password: PW1, roles: ROLE_HTTP }
in_memory_api:
memory:
users:
User2: { password: PW2, roles: ROLE_API }
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
api:
pattern: ^/api
http_basic:
provider: in_memory_api
realm: "API login"
http:
pattern: ^/
provider: in_memory_http
http_basic:
realm: "Hello"
context: primary_auth
main:
pattern: ^/
form_login:
provider: fos_userbundle
login_path: fos_user_security_login
csrf_provider: form.csrf_provider
check_path: fos_user_security_check
logout:
path: fos_user_security_logout
target: home
anonymous: true
context: primary_auth
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
security: false
secured_area:
anonymous: ~
access_control:
- { path: ^/api, roles: ROLE_API }
- { path: ^/user/login.html, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user/logout.html, roles: IS_AUTHENTICATED_REMEMBERED }
This works nearly as expected, but unfortunately not completely...
The /api part works like I wanted it to work. Nothing to do here, I hope.
But when I navigate to /, enter User1/PW1 and send the credentials, I get access to the page, just like expected. The only problem is that the User1 gets logged in! But I want User1 not to be handled like a normal user. He should just be required to access the normal login form and the rest of / except of /api. I can't even log out this user. If I navigate to /user/login.html while User1 is logged in (due to the required http auth) and enter valid user data of a real fosuserbundle user, I get: "You must configure the check path to be handled by the firewall using form_login in your security firewall configuration."
If I want to log out, I get: "You must activate the logout in your security firewall configuration."
What I want is kind of a two step authentication.
First HTTP Auth, then the FOSUserBundle form.
Can somebody help me? :) The documentation is not very good at this point...

So.....
After several hours of pure pain I gave up on trying it with the Symfony2 Security Component. I also did not manage to realize what I want with Apache (Tried SetEnvIf and FilesMatch).
So I wrote a request listener that does what I want. If anybody has the same problem, here is my solution!
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\Response;
class RequestListener
{
/**
* Returns true iff the specified $password belongs to the $user and the $user has access to the specified $area
*/
private function hasAccess($user, $password, $area) {
$users = array("User1" => array("password" => "PW1",
"areas" => array("portal")),
"User2" => array("password" => "PW2",
"areas" => array("API", "portal"))
);
return $user
&& array_key_exists($user, $users)
&& $users[$user]["password"] == $password
&& in_array($area, $users[$user]["areas"]);
}
/**
* Extracts the area out of the $path
*/
public function getArea($path) {
if (substr($path, 0, 4) == "/api") {
return "API";
} else {
return "portal";
}
}
/**
* Service handler
*/
public function onKernelRequest(GetResponseEvent $event) {
$request = $event->getRequest();
# $path cointains the path to the virtual resource the user requested!
# If the user calls app_dev.php/api/users, then $path is "/api/users"
# If the user calls app.php/api/users, then $path is "/api/users"
# If the user calls /api/users, then $path is "/api/users"
# If the user calls /app_dev.php, then $path is "/"
# If the user calls /, then $path is "/"
# and so on
#
# ==> $path abstracts the front controller away
$path = $request->getPathInfo();
$area = $this->getArea($path);
# $user, $password are null if no AUTH data is sent
$user = $request->server->get("PHP_AUTH_USER");
$password = $request->server->get("PHP_AUTH_PW");
# If the user has no access, he must log in as another user
if (!$this->hasAccess($user, $password, $area)) {
# In case the response already exists (in most cases not) we use it and modify it
$response = $event->hasResponse() ? $event->getResponse() : new Response();
$response->setStatusCode(Response::HTTP_UNAUTHORIZED); # Code 401
$response->headers->set("WWW-Authenticate", "Basic realm=\"".$area."\"");
$response->setContent("Please provide valid data");
$response->send();
die(); # To make sure the page is not shown!
}
}
}
Now everything seems to work...

Related

Symfony access control forbids user with correct role

security.yml:
role_hierarchy:
admin: [test, simple]
providers:
database:
entity: { class: UserBundle:User, property: username }
firewalls:
dev:
pattern: ^/(_(profiler|wdt|error)|css|images|js)/
security: false
prod:
pattern: ^/
provider: database
anonymous: true
form_login:
login_path: public_login
check_path: public_login_check
default_target_path: dashboard
always_use_default_target_path: true
csrf_provider: form.csrf_provider
logout:
path: logout
target: public_login
access_control:
- { path: ^/(.+), roles: admin }
- { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
When i login, I get 403 forbidden exception. Then i check profiler/security, and roles looks like that:
Roles [ROLE_USER, admin]
When i switch access control to:
- { path: ^/(.+), roles: ROLE_USER }
It works fine.
Why the hell my access control doesn't allow me to access pages with "admin" role, but does with "ROLE_USER" ?
My goal is to drop built-in roles (like ROLE_USER, ROLE_ADMIN etc), because I'm writing application for existing database, which contains already defined roles for users, so i want to use them.
I have confirmed that 'ROLE_' prefix is required - its because symfony by default use its own RoleVoter implementation.
To get rid of this prefix, custom RoleVoter is needed. To do that you need to create custom class implementing RoleVoterInterface, make it service and tag it with 'security.voter', to enforce security layer to use it instead of default one.
Read more about implementing own RoleVoters on this example.
You are not using the right syntax for roles in the Security configuration
you should change
- { path: ^/(.+), roles: admin }
To:
- { path: ^/(.+), roles: ROLE_ADMIN }

Symfony2 lock full site with firewall

I would like to close my full site and only access to authenticated users, but I would like to keep some routes to public. Public routes would be:
/
/news
/registration
All otheres are locked.
I've made a firewall which looks like this:
firewalls:
user_login:
pattern: ^/
anonymous: ~
user_area:
pattern: ^/
form_login:
login_path: _main_index #this is a route to /
logout:
path: _main_logout #this is a route to /logout
target: _main_index #this is a route to /
invalidate_session: false
access_control:
- { path: ^/news, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/registration, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user-panel, roles: ROLE_ACTIVE_USER } #is this neccessary?
Then when I'm logging in to a restricted area (/user-panel) it sais I need to fully authenticate myself.
I store my roles in security.yml, not in database.
I hope you can help me! Thank you very much!
EDIT:
My loginCheckAction looks like this::
$encodedPassword = $this->get('user.user_service')->generatePasswordHash($user, $request->request->get('_password'));
if ($user->getPassword() == $encodedPassword) {
$user->setLastLoginOn(new \DateTime());
$this->em->user($rocker);
$this->em->flush();
$token = new UsernamePasswordToken($user, $user->getPassword(), 'user_area', array($user->getRoles()));
$request->getSession()->set('_security_user_area', serialize($token));
return $this->redirect($this->generateUrl('_user_panel'));
}
You need to add an catch-all firewall that requires authentication, like this:
- { path: ^/, roles: ROLE_ACTIVE_USER }
That will make all pages that aren't explicitely listed to be accessible without authentication (i.e. with IS_AUTHENTICATED_ANONYMOUSLY) inaccessible. However, since you didn't list your homepage yet, you'll need to add that too.
- { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Make sure to put it above the catch-all entry, as Symfony processes entries from top to bottom, and stops when it found a matching entry.
You need to allow anonymus token
user_area:
pattern: ^/
form_login:
login_path: _main_index #this is a route to /
logout:
path: _main_logout #this is a route to /logout
target: _main_index #this is a route to /
invalidate_session: false
anonymous: ~
Try this.
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/registration, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/news, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/(css|js), roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/(_wdt|_profiler), roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_ACTIVE_USER }
I think login_check is used internally by the symfony security system just like logout. This empty function should work.
/**
* #Route("/login_check", name="login_check")
*/
public function loginCheckAction()
{
}
If you want to redirect the user to the panel route after successful login you need to implement login event listener. Symfony documentation How to create an Event Listener.
Some resources for symfony loginlistener.
http://www.ens.ro/2012/03/14/symfony2-login-event-listener/
https://gist.github.com/smottt/1075753
First of all, one of the major problem was the multiple firewall on the same route.
Symfony uses /login route to login, /login_check to check credentials and /logout to log out by default. For me, all routes was good, except the login route, because I wanted to log in users from / route.
Because of this, I had to implement UserProviderInterface into my UserRepository like this: Authenticating Someone with a Custom Entity Provider - Symfony2 documentation
I just modified this on my way (I user email as username).
Logincheck and logout is working automatically, because I use the default routes.
And for all of this, you have to implement AdvancedUserInterface to Users entity!!

Symfony2 Security log user manually

I want to log user manually in Symfony2. (I use fosuserbundle).
The authentication will be triggered in custom route like this /login/auto
Here is my controller code which match with /login/auto
public function loginAction(){
$em = $this->container->get('doctrine')->getManager();
$users = $em->getRepository('MybundleMainBundle:User');
$user = $users->findOneByEmail("user#user.com");
$securityContext = $this->get('security.context');
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$securityContext->setToken($token);
$this->get('session')->set('_security_'.'main', serialize($token));
return new RedirectResponse($this->generateUrl('home'));
}
But after the redirection, I'm redirected automatically to /login and not /home so the authentification failed
Here is my security file config :
security:
providers:
fos_userbundle:
id: fos_user.user_provider.username
encoders:
FOS\UserBundle\Model\UserInterface: sha512
firewalls:
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
always_use_default_target_path: true
logout: true
anonymous: true
switch_user: true
remember_me:
key: %secret%
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
access_control:
- { path: ^/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: ROLE_USER }
role_hierarchy:
ROLE_USER: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
Thanks for your help
Looks very similar to mine. Maybe you should not write into the session. Or the token needs the (hashed) password. Try it, here's a working code ;)
public function demologinAction(Request $request)
{
$dm = $this->get('doctrine.odm.mongodb.document_manager');
$repo = $dm->getRepository('AcmeUserBundle:User');
$user = $repo->findOneByUsername('demo');
if (!$user) {
throw $this->createNotFoundException('No demouser found!');
}
$token = new UsernamePasswordToken($user, $user->getPassword(), 'main', $user->getRoles());
$context = $this->get('security.context');
$context->setToken($token);
$router = $this->get('router');
$url = $router->generate('dashboard_show');
return $this->redirect($url);
}
Why are you trying to log a user in manually?
I may be wrong, but if the reason is so that you can run some postLogin code, it would be easier to use the built in login functionality, but setup a listener on the login action as a service. Then add your login code in there.
Sevice definition would be:
user.login:
class: You\Bundle\EventListener\EventListener
arguments: [#doctrine.orm.entity_manager, #service_container]
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onLogin }
And your event listener might be:
public function onLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
$user->setLastLoggedInAt(new \DateTime());
$user->setLoginCount($user->getLoginCount() + 1);
$this->manager->flush();
}

Symfony2 version 2.0.23, login with users from database always return: 'The presented password is invalid'

I'm trying to set up 2 firewall with 2 different providers and encoders in my security.yml that looks like this:
security:
encoders:
Devsign\UserBundle\Entity\AgentUser:
algorithm: sha512
iterations: 5000
encode_as_base64: true
Devsign\UserBundle\Entity\PressUser:
algorithm: sha512
iterations: 5000
encode_as_base64: true
providers:
agent_secured_area:
entity: {class: Devsign\UserBundle\Entity\AgentUser } # using a custom repository to login with username or email, details in AgentUserRepository.php
press_secured_area:
entity: {class: Devsign\UserBundle\Entity\PressUser, property: username }
firewalls:
agent_secured_area:
pattern: /(it|en)/reserved/
provider: agent_secured_area
anonymous: ~
form_login:
check_path: /it/reserved/login-check
login_path: /reserved/login
logout:
path: /reserved/logout
target: /
press_secured_area:
pattern: /(it|en)/press/
provider: press_secured_area
anonymous: ~
form_login:
check_path: /it/press/login-check
login_path: /press/login
logout:
path: /press/logout
target: /
access_control:
agent_login:
path: /reserved/login
roles: IS_AUTHENTICATED_ANONYMOUSLY
agent_register:
path: /reserved/register
roles: IS_AUTHENTICATED_ANONYMOUSLY
agent_area:
path: /(it|en)/reserved/.*
roles: ROLE_AGENT
press_login:
path: /press/login
roles: IS_AUTHENTICATED_ANONYMOUSLY
press_register:
path: /press/register
roles: IS_AUTHENTICATED_ANONYMOUSLY
press_area:
path: /(it|en)/press/.*
roles: ROLE_PRESS
I get no exception but when I try to login against agent_secured_area i get always: 'The presented password is invalid'.
I created the first user password and salt using this code in a controller:
$factory = $this->get('security.encoder_factory');
$user = new \Devsign\UserBundle\Entity\AgentUser();
$encoder = $factory->getEncoder($user);
$salt = $user->getSalt();
$password = $encoder->encodePassword('grab', $salt);
die("pwd: $password - salt: $salt");
And then i fill the database field password and salt with the echoed values.
Can someone spot the error?
UPDATE 1
I made some test setting in the config_dev.yml:
web_profiler:
toolbar: true
intercept_redirects: true
verbose: true
1) I try to go to /it/reserved/info but it is access protected so I'm redirected correctly to /it/reserved/login
2) I try to login from /it/reserved/login posting the form to /it/reserved/login-check
3) Thanks to web_profile: intercepts_redirects: true I can see the debug toolbar in /it/reserved/login-check and I'm correctly authenticated with the correct Role: ROLE_AGENT. By the way looking in the doctrine section of the profiler I see two queries against my user table, the first one with parameter username NULL and a second one with the correct username.
4) Then I'm redirected to /it/reserved/info where for some reason I see a single query against my user table with parameter username NULL. Infact in /it/reserved/info I'm not authenticated anymore. So I'm redirected again to /it/reserved/login.
So I think the problem is that query with parameter username null, someone knows where it's coming from? Maybe from some misconfiguration on security.yml?

Security in Symfony2

I've created a Bundle named "User". In the general routing I added /user to every UserBundle url. So, for example, when I define the route /list in the UserBundle the real path is http://myapp.loc/user/list.
I'm trying to force user to login to use this website, an I'm doing this:
security:
firewalls:
login_firewall:
pattern: ^/user/login
anonymous: ~
secured_area:
pattern: ^/
form_login:
login_path: user_login
check_path: user_login_check
default_target_path: /
logout:
path: /user_logout
target: user_login
remember_me:
key: atipics-soft2012
lifetime: 3600
access_control:
- { path: ^/, roles: ROLE_USER }
providers:
users:
entity: { class: MyApp\UserBundle\Entity\User, property:email }
encoders:
MyApp\UserBundle\Entity\User: { algorithm: sha512, iterations: 10 }
I'm getting an error like this:
InvalidConfigurationException: Invalid configuration for path
"security.firewalls.secured_area": The check_path "user_login_check"
for login method "form_login" is not matched by the firewall pattern
"^/".
What could be the problem?
Of couse I've added this route in the routes file.
I don't know if it's a bug or was intented this way, but check_path doesn't accept route names. You have to set it to a path.

Resources