I'm a little stuck and unable to find the answer to this.
In my app test I've created two Entities User and Comment both are mapped correctly.
I have created a small controller which depending on the user will add the comment and the data to the ACL tables, if I create my comment as a standard user with the associated for of 'ROLE_USER', and Try to access it as user with the role 'ROLE_ADMIN' I get access denied, it seems to completely ignore the security.yml hierarchy.
I know this works by adding instead of the userid the ROLE_USER etc but I don't want to do it this way.
Examples of my code are below.
CommentController
<?php
namespace ACL\TestBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use ACL\TestBundle\Forms\Type\commentType;
use ACL\TestBundle\Entity\Comment;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
class DefaultController extends Controller
{
/**
* #Route("/", name="_default")
* #Template()
*/
public function indexAction()
{
die('success');
}
/**
* #Route("/comment/new/")
* #Template()
*/
public function newAction(Request $request)
{
$comment = new Comment();
$form = $this->createForm(new commentType(), $comment);
$form->handleRequest($request);
if ($form->isValid()) {
$comment->setUsers($this->getUser());
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
// creating the ACL
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($comment);
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$securityIdentity = UserSecurityIdentity::fromAccount($this->getUser());
// grant owner access
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
}
return array(
'form' => $form->createView(),
);
}
/**
* #Route("/comment/{id}/", requirements={"id":"\d+"})
* #Template()
*/
public function editAction(Request $request,$id)
{
$em = $this->getDoctrine()->getManager();
$comment = $em->find('ACLTestBundle:Comment', $id);
$securityContext = $this->get('security.context');
// check for edit access
if (false === $securityContext->isGranted('EDIT',$comment)) {
throw new AccessDeniedException();
}
$form = $this->createForm(new commentType(), $comment);
$form->handleRequest($request);
if($form->isValid()){
$em->persist($comment);
$em->flush();
}
return array('form' => $form->createView());
}
}
security.yml
security:
encoders:
ACL\TestBundle\Entity\User: plaintext
acl:
connection: default
providers:
database:
entity: { class: ACLTestBundle:User }
role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_ALLOWED_TO_SWITCH]
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
provider: database
anonymous: true
logout: true
switch_user: true
form_login:
login_path: _security_login
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
I appreciate any advice!
The problem is that you are adding adding ACL base on UserIdentity and want to check the gran base on RoleIdentity. If you want to do it Role base change the creating ACL as below
// creating the ACL
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($comment);
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$securityIdentity = UserSecurityIdentity::fromAccount($this->getUser());
// grant owner access
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
// grant EDIT access to ROLE_ADMIN
$securityIdentity = new RoleSecurityIdentity('ROLE_ADMIN');
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_EDIT);
$aclProvider->updateAcl($acl);
As you see I kept the owner access for the specific user then I added Edit access for ROLE_ADMIN. You can keep the controller as is.
If you don't want to make it Role base but just want to give an exception for admin users you can change your controller as
// check for edit access
if (false === $securityContext->isGranted('EDIT',$comment) && false === $securityContext->isGranted('ROLE_ADMIN') ) {
throw new AccessDeniedException();
}
Related
We've updated a base project from Symfony 2.8 to 3.4. This has largely gone well, blah blah, but I've noticed quite an important issue.
It seems that logging in is instantly forgotten after the request for "check_path" has completed unless the user chooses the "Remember Me" option. -- We do not provide this option for management interfaces to ensure the user has authenticated properly, therefore the management interface can't be accessed at all.
Request flow goes as shown in Symfony profiler:
Attempt to access firewalled route.
401 response showing login form with Anonymous token.
Submit login form.
302 response with UsernamePassword token. -- This shows the username and password has been accepted.
Redirected to original firewalled route.
200 response with Anonymous token. -- UsernamePassword token has gone!
This response does not appear in the web browser's network debugger.
Redirected to login form again.
401 response showing login form with Anonymous token.
Contents of "app/config/security.yml":
security:
encoders:
App\UserBundle\Entity\User:
algorithm: bcrypt
cost: 16
providers:
local_db:
entity: { class: AppUserBundle:User }
firewalls:
dev:
pattern: ^/(_(profiler|wdt))/
security: false
assets:
pattern: ^/(css|images|js)/
security: false
admin:
pattern: ^/admin
provider: local_db
anonymous: ~
logout_on_user_change: true
form_login:
csrf_token_generator: security.csrf.token_manager
login_path: user_admin_login
check_path: user_admin_login
default_target_path: dashboard
use_forward: true
use_referer: true
logout:
path: user_admin_logout
target: dashboard
handler: auth_listener
invalidate_session: true
switch_user:
role: ROLE_TOP_ADMIN
parameter: _login_as_user
# remember_me:
# secret: "%secret%"
front:
pattern: ^/
provider: local_db
anonymous: ~
logout_on_user_change: true
form_login:
csrf_token_generator: security.csrf.token_manager
# login_path should be "user_account_login" or "user_account_auth" depending on which view you want.
login_path: user_account_login
check_path: user_account_login
default_target_path: user_account
use_forward: true
use_referer: true
logout:
path: user_account_logout
target: home
handler: auth_listener
invalidate_session: true
switch_user:
role: ROLE_ADMIN
parameter: _login_as_user
remember_me:
secret: "%secret%"
access_control:
- { path: ^/admin/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: [ROLE_CONTRIBUTOR, ROLE_EDITOR, ROLE_ADMIN, ROLE_TOP_ADMIN] }
- { path: ^/account/auth$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/register$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/forgot_password$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/change_password$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account, roles: [ROLE_USER, ROLE_CONTRIBUTOR, ROLE_EDITOR, ROLE_ADMIN, ROLE_TOP_ADMIN] }
# Change the below "IS_AUTHENTICATED_ANONYMOUSLY" to "ROLE_USER" if this is going to be a private website.
# This will ensure users have to login on the landing page.
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Routes:
user_admin_login points to "/admin/login"
user_admin_logout points to "/admin/logout"
user_account_auth points to "/account/auth".
user_account_login points to "/account/login".
user_account_logout points to "/account/logout".
I'm wondering if this is cookie related. I did notice that the Symfony session ID cookie ("webapp" below) value is changing after logging in, but it does remain consistent between page navigations elsewhere. It only changes twice when submitting the login form. Using the response references above:
Attempt to access firewalled route:
"webapp" value is "h76n9kcra43stmjb5accnqlg70itnavf" on 401 response.
Submit login form:
"webapp" value is "15iscbl51k2mjs14bck5m54f4m8qhtme" on 302 response.
Redirected to firewalled route:
"webapp" value is "ciibpf8h54u2vp3gdi31bvdm5oj3r3ts" on 200 response.
Redirected to login form:
"webapp" value is "ciibpf8h54u2vp3gdi31bvdm5oj3r3ts" on 401 response.
Contents of "app/config/config.yml" session section:
session:
storage_id: "session.storage.native"
handler_id: "session.handler.native_file"
name: "webapp"
cookie_lifetime: 604800
gc_divisor: 10
gc_probability: 1
gc_maxlifetime: 14400
save_path: "%kernel.root_dir%/../var/sessions"
I tried using different web browsers with very default cookie settings in case something was up with Chrome, no different though.
If it helps, when successfully logged in with the "remember me" option ticked, the token is a RememberMeToken -- not a UsernamePasswordToken.
Please let me know if any further information is required.
The goal here is to be able to login without needing a "remember me" option enabled.
Edit: User entity model
As requested, here is some detail about the user entity model. It's quite big (2016 lines) so I'll just paste in the parts relevant to Symfony's user interface.
Declaration
class User implements AdvancedUserInterface, UserPermissionInterface, DataContentEntityInterface, \Serializable
Interfaces UserPermissionInterface and DataContentEntityInterface are custom for our application. (Irrelevant.)
Serializable relevant parts
/**
* #see \Serializable::serialize()
*/
public function serialize()
{
return serialize([
$this->id,
$this->userName,
$this->email,
$this->password,
// $this->salt,
]);
}
/**
* #see \Serializable::unserialize()
*/
public function unserialize($serialized)
{
list(
$this->id,
$this->userName,
$this->email,
$this->password,
// $this->salt,
) = unserialize($serialized, ["allowed_classes" => false]);
}
UserInterface relevant parts
/**
* #inheritDoc
*/
public function getSalt()
{
return null;
}
/**
* #inheritDoc
*/
public function getRoles()
{
if (!$this->group) {
return [];
}
$rolesArray = array();
foreach ($this->getGroup()->getPermissions() as $k => $permission) {
$role = strtoupper($permission);
$role = str_replace('.', '_', $role);
$role = sprintf("ROLE_%s", $role);
$rolesArray[$k] = $role;
}
$rolesArray[] = $this->getGroup()->etRole();
// If user is top admin, also give admin group
if ($this->getGroup()->getRole() === "ROLE_TOP_ADMIN") {
$rolesArray[] = "ROLE_ADMIN";
}
return $rolesArray;
}
/**
* #inheritDoc
*/
public function eraseCredentials()
{
}
/**
* Get userName
*
* #return string
*/
public function getUserName()
{
return $this->userName;
}
/**
* Get password
*
* #return string
*/
public function getPassword()
{
return $this->password;
}
AdvancedUserInterface relevant parts
public function isAccountNonExpired()
{
if (!$this->expires) {
return true;
}
if (new \DateTime() <= $this->expires) {
return true;
}
return false;
}
public function isAccountNonLocked()
{
return $this->status === self::STATUS_VERIFIED;
}
public function isCredentialsNonExpired()
{
if (!$this->passwordExpires) {
return true;
}
if (new \DateTime() <= $this->passwordExpires) {
return true;
}
return false;
}
public function isEnabled()
{
return $this->isAccountNonLocked() && !$this->activationCode;
}
Entity repository UserRepository declaration
class UserRepository extends EntityRepository implements UserLoaderInterface
Function to load user
/**
* UserLoaderInterface
* #param string $userName User to look for
* #return User|null User entity, or null if not found
*/
public function loadUserByUsername($userName)
{
$qb = $this
->createQueryBuilder("u")
->where("u.userName = :userName OR u.email = :userName")
->setParameter("userName", $userName)
->andWhere("u.status != :statusDeleted")
->setParameter("statusDeleted", User::STATUS_DELETED)
->andWhere("u.status = :statusVerified")
->setParameter("statusVerified", User::STATUS_VERIFIED)
->orderBy("u.status", "DESC")
->addOrderBy("u.group", "ASC")
->addOrderBy("u.created", "ASC")
->setMaxResults(1)
;
$query = $qb->getQuery();
try {
// The Query::getSingleResult() method throws an exception
// if there is no record matching the criteria.
$user = $query->getSingleResult();
} catch (NoResultException $e) {
throw new UsernameNotFoundException(sprintf("Unable to find an active user identified by \"%s\".", $username), 0, $e);
} catch (NonUniqueResultException $e) {
throw new UsernameNotFoundException(sprintf("Unable to find a unique active user identified by \"%s\".", $username), 0, $e);
}
return $user;
}
This function works fine. A valid user entity is definitely returned.
In your security.yml, remove:
logout_on_user_change: true
or set it to false.
This will solve the instant logout issue, though it will also bypass a security feature of Symfony.
It seems as if something in the serialize() and unserialize() isn't matching, and Symfony then logs the user out as a precaution. With AdvancedUserInterface, Symfony also checks that the AdvancedUserInterface methods match too. If you have anything else going on in those methods which could cause the users to not match (like some bespoke roles management), that could be triggering the logout. To debug, I would suggest returning true in each of the AdvancedUserInterface methods, then re-adding your functionality until the logout gets triggered.
From Symfony's documentation:
If you're curious about the importance of the serialize() method inside
the User class or how the User object is serialized or deserialized,
then this section is for you. If not, feel free to skip this.
Once the user is logged in, the entire User object is serialized into
the session. On the next request, the User object is deserialized.
Then, the value of the id property is used to re-query for a fresh
User object from the database. Finally, the fresh User object is
compared to the deserialized User object to make sure that they
represent the same user. For example, if the username on the 2 User
objects doesn't match for some reason, then the user will be logged
out for security reasons.
Even though this all happens automatically, there are a few important
side-effects.
First, the Serializable interface and its serialize() and
unserialize() methods have been added to allow the User class to be
serialized to the session. This may or may not be needed depending on
your setup, but it's probably a good idea. In theory, only the id
needs to be serialized, because the refreshUser() method refreshes the
user on each request by using the id (as explained above). This gives
us a "fresh" User object.
But Symfony also uses the username, salt, and password to verify that
the User has not changed between requests (it also calls your
AdvancedUserInterface methods if you implement it). Failing to
serialize these may cause you to be logged out on each request. If
your user implements the EquatableInterface, then instead of these
properties being checked, your isEqualTo() method is called, and you
can check whatever properties you want. Unless you understand this,
you probably won't need to implement this interface or worry about it.
Resolved this with insight from #jedge.
Turns out the logout_on_user_change option being true was the cause. Changing this to false resolved the issue. -- I'm not sure what this does as there is little documentation on it, and worryingly this has become true by default in Symfony 4...
Other things we tried were the temporary removal of CSRF, forwarding, and logout event. -- None of these turned out to conflict. We were also able to login programmatically by manually creating a token for a specific user and dispatching an InteractiveLoginEvent, which led us on to the firewall configuration.
I am following the Symfony book and cookbook recipes and I met problem with simple login form - no matter if entered login/pass are valid, message shows up - 'Invalid credentials'. Users are loaded via Doctrine (User class which implements UserInterface). Source codes :
Security file:
providers:
user_provider:
entity:
class: BakaMainBundle:User
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
default:
anonymous: ~
http_basic: ~
provider: user_provider
form_login:
login_path: /login
check_path: /login_check
target_path_parameter: /index/welcome
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
encoders:
Baka\MainBundle\Entity\User:
algorithm: bcrypt
cost: 12
Controller :
class SecurityController extends Controller
{
/**
* #Route("/login", name="login_route")
*/
public function loginAction()
{
$authUtils = $this->get('security.authentication_utils');
$error = $authUtils->getLastAuthenticationError();
$enteredUsername = $authUtils->getLastUsername();
return $this->render('BakaMainBundle::Login.html.twig',
array
(
'last_username' => $enteredUsername,
'error' => $error,
'site' => 'login'
));
}
/**
* #Route("/login_check", name="login_check")
*/
public function loginCheckAction()
{
}
}
User repository :
class UserRepository extends \Doctrine\ORM\EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$user = $this->createQueryBuilder('u')
->where('u.username = :username OR u.email = :email')
->setParameter('username', $username)
->setParameter('email', $username)
->getQuery()
->getOneOrNullResult();
if ($user === null)
{
$returnMessage = sprintf(
'%s - such username of email adress does not exist in database! Try again with other login data.',
$username);
throw new UnsupportedUserException($returnMessage);
}
return $user;
}
public function refreshUser(UserInterface $user)
{
$userClass = get_class($user);
if (!$this->supportsClass($userClass))
{
throw new UnsupportedUserException
(sprintf('Ops! Something goes wrong. Your user class is not supported by security system.'));
}
return $this->find($user->getId());
}
public function supportsClass($userclass)
{
return $this->getEntityName() === $userclass || is_subclass_of($userclass, $this->getEntityName());
}
And the form html tag :
<form action="{{ path('login_check') }}" method="post">
Any suggestions? I will be grateful for resolving my problem.
I think you should use the class namespace instead of the bundle name, when specifying the provider class. Also, you need to specify which property you will be selecting as the "username" from your Entity:
security:
providers:
user_provider:
entity:
class: Baka\MainBundle\Entity\User
property: username (this should be an existing property of your entity class)
Also, your User entity needs to implement Symfony\Component\Security\Core\User\UserInterface (or AdvancedUserInterface). Once you're done with that, everything should work if you have users in the database with a properly encoded password.
You should read:
How to Load Security Users from the Database (the Entity Provider) to understand how to load users from the database
Security to get better understanding of how the security component works and how it should be configured.
I've already identified the reason of issue, and it transpired to be trivial -
field which serves as an Encoded Password row in the DB had 15 characters long limit :
/**
* #ORM\Column(type="string", length=15)
*/
protected $password;
And since '12 rounds' bcrypt needs much more digits to represent plain password, Doctrine was forced to shorten encrypted pass so it was impossible to decode later. After changing to suggested by Symfony size the problem has gone :
/**
* #ORM\Column(type="string", length=4096)
*/
protected $password;
Thank you for all support.
I have a Symfony 2.7.6 project with custom Simple Form authentication provider and support for remember me functionality as well as impersonalization feature. Everything works as expected.
However, I want to introduce another authentication provider that will allow requests regardless of session state using two HTTP headers for authentication (e.g. API-Client-Id and API-Client-Token) for third-party applications.
I've created a Simple Pre-Auth authentication provider that validates these header fields and creates authentication token with empty User instance on success.
However, it looks like Symfony is trying to remember those API authentications using session, so I'm getting the following error on the second request: "You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine.".
I can set stateless: true flag in my firewall configuration to disable session support, but it will disable it for both auth providers.
SO, how do I preserve existing functionality with my Simple Form authenticator and yet create another layer of authentication to be used for single stateless API requests?
I'm not sure if my approach is conceptually correct. I will gladly accept any suggestions and will provide any relevant information on first request.
Here's my security.yml config:
security:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: ~
form_login:
login_path: app.login
check_path: app.session.sign_in
username_parameter: username
password_parameter: password
success_handler: app.security.login_handler
failure_handler: app.security.login_handler
require_previous_session: false
logout:
path: app.session.sign_out
invalidate_session: false
success_handler: app.security.logout_success_handler
# Simple form auth provider
simple_form:
authenticator: app.security.authenticator.out_service
# Token provider
simple_preauth:
authenticator: app.security.authenticator.api_client
remember_me:
name: "%app.session.remember_me.name%"
key: "%secret%"
lifetime: 1209600 # 14 days
path: /
domain: ~
always_remember_me: true
switch_user: { role: ROLE_ADMIN }
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/recover-password, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /, roles: IS_AUTHENTICATED_REMEMBERED }
providers:
main:
entity:
class: App\AppBundle\Model\User
property: id
encoders:
App\AppBundle\Model\User: plaintext
role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_ACTIVE]
ROLE_API_CLIENT: ~
ROLE_USER: ~
ROLE_ACTIVE: ~
ApiClientAuthenticator.php:
<?php
namespace App\AppBundle\Security;
use Symfony\Component\Security\Core\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use App\AppBundle\Model\User;
class ApiClientAuthenticator implements SimplePreAuthenticatorInterface
{
/** #var LoggerInterface */
protected $logger;
/** #var array */
protected $clients;
/**
* #param array $clients
*/
public function __construct(array $clients)
{
$this->clients = $clients;
}
public function createToken(Request $request, $providerKey)
{
$clientId = $request->headers->get('Api-Client-Id');
$clientSecret = $request->headers->get('Api-Client-Secret');
if (!$clientId || !$clientSecret) {
return null;
}
return new PreAuthenticatedToken(
'anon.',
[$clientId, $clientSecret],
$providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
list ($clientId, $clientSecret) = $token->getCredentials();
$foundClient = null;
foreach ($this->clients as $client) {
if ($client['id'] == $clientId) {
if ($client['secret'] == $clientSecret) {
$foundClient = $client;
break;
}
}
}
if (!$foundClient) {
throw new AuthenticationException;
}
$user = new User;
$user->setApiClient(true);
return new PreAuthenticatedToken(
$user,
$foundClient,
$providerKey,
['ROLE_API_CLIENT']
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return ($token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey);
}
}
I am trying to combine FOSUserBundle and HWIOAuthBundle following articles like https://gist.github.com/danvbe/4476697. However, I do not want the automatic registration of OAuth2 authenticated new users: additional information should be provided by the user.
Desired result
I would for example want the following information for a registered user:
(Username, although I'd rather just use e-mail)
Display name (required)
Profile picture (required)
Email address (required if no Facebook-id)
Password (required if no Facebook-id)
Facebook-id (required if no email address)
Now, when a user authenticates through Facebook and the user does not exist yet, I want a registration form to fill out the missing information (display name and profile picture). Only after this, the new FOSUser should be created.
In most tutorials, fields like Profile picture and Email address are automatically populated with the Facebook information. This is not always desirable nor possible.
Also, think of things like accepting Terms of Agreement and rules you wish to show before the user is created.
Possible approaches
A solution would be, I think, to create a new sort-of AnonymousToken, the OAuthenticatedToken, which holds the relevant OAuth2 information but does not count an authenticaton. Then, make all pages check for this kind of authentication and let other pages redirect to OAuth-registration-page. However, this seems an unnecessarily complicated solution to me.
Another solution would probably be to write the code from scratch and not use the two bundles mentioned. I really hope this is not necessary.
Q: How can I insert the registration-completion-code in the rest of the login flow?
(I'd love to share some code, but since it's the very concept I need help at, I don't have a lot to show.)
Edit: Solution
Following Derick's adivce, I got the basics working like this:
The Custom user provider saves the information (sadly, no access to the raw token so I cannot yet log the user in after registering):
class UserProvider extends FOSUBUserProvider {
protected $session;
public function __construct(Session $session, UserManagerInterface $userManager, array $properties) {
$this->session = $session;
parent::__construct( $userManager, $properties );
}
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
try {
return parent::loadUserByOAuthUserResponse($response);
}
catch ( AccountNotLinkedException $e ) {
$this->session->set( 'oauth.resource', $response->getResourceOwner()->getName() );
$this->session->set( 'oauth.id', $response->getResponse()['id'] );
throw $e;
}
}
}
Custom failure handler:
<?php
// OAuthFailureHandler.php
class OAuthFailureHandler implements AuthenticationFailureHandlerInterface {
public function onAuthenticationFailure( Request $request, AuthenticationException $exception) {
if ( !$exception instanceof AccountNotLinkedException ) {
throw $exception;
}
return new RedirectResponse( 'fb-register' );
}
}
Both are registered as a service:
# services.yml
services:
app.userprovider:
class: AppBundle\Security\Core\User\UserProvider
arguments: [ "#session", "#fos_user.user_manager", {facebook: facebookID} ]
app.oauthfailurehandler:
class: AppBundle\Security\Handler\OAuthFailureHandler
arguments: ["#security.http_utils", {}, "#service_container"]
And configured in security config:
# security.yml
security:
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
main:
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
login_path: /login
check_path: /login_check
default_target_path: /profile
oauth:
login_path: /login
check_path: /login_check
resource_owners:
facebook: hwi_facebook_login
oauth_user_provider:
service: app.userprovider
failure_handler: app.oauthfailurehandler
anonymous: true
logout:
path: /logout
target: /login
At /fb-register, I let the user enter a username and save the user myself:
/**
* #Route("/fb-register", name="hwi_oauth_register")
*/
public function registerOAuthAction(Request $request) {
$session = $request->getSession();
$resource = $session->get('oauth.resource');
if ( $resource !== 'facebook' ) {
return $this->redirectToRoute('home');
}
$userManager = $this->get('fos_user.user_manager');
$newUser = $userManager->createUser();
$form = $this->createForm(new RegisterOAuthFormType(), $newUser);
$form->handleRequest($request);
if ( $form->isValid() ) {
$newUser->setFacebookId( $session->get('oauth.id') );
$newUser->setEnabled(true);
$userManager->updateUser( $newUser );
try {
$this->container->get('hwi_oauth.user_checker')->checkPostAuth($newUser);
} catch (AccountStatusException $e) {
// Don't authenticate locked, disabled or expired users
return;
}
$session->remove('oauth.resource');
$session->remove('oauth.id');
$session->getFlashBag()
->add('success', 'You\'re succesfully registered!' );
return $this->redirectToRoute('home');
}
return $this->render( 'default/register-oauth.html.twig', array(
'form' => $form->createView()
) );
}
The user is not logged in afterwards, which is too bad. Also, the normal fosub functionality (editing profile, changing password) does not work out of the box anymore.
I'm simply using the username as the displayname, not sure why I didn't see that before.
Step 1:
Create your own user provider. Extend the OAuthUserProvider and customize to your needs. If the user successfully oauthed in, throw a specific exception (probably the accountnotlinkedException) and toss all relevant data about the login somewhere
Step 2:
Create your own authentication failure handler. Check to make sure the error being thrown is the specific one you threw in step 1.
In here you will redirect to your fill in additional info page.
This is how to register you custom handlers:
#security.yml
firewall:
main:
oauth:
success_handler: authentication_handler
failure_handler: social_auth_failure_handler
#user bundle services.yml (or some other project services.yml)
services:
authentication_handler:
class: ProjectName\UserBundle\Handler\AuthenticationHandler
arguments: ["#security.http_utils", {}, "#service_container"]
tags:
- { name: 'monolog.logger', channel: 'security' }
social_auth_failure_handler:
class: ProjectName\UserBundle\Handler\SocialAuthFailureHandler
arguments: ["#security.http_utils", {}, "#service_container"]
tags:
- { name: 'monolog.logger', channel: 'security' }
Step 3:
Create your fill in additional info page. Pull all relevant data that you stored back in step 1 and create the user if everything checks out.
I have read many posts on stackoverflow about this. But most of the methods not useful in Symfony 2.3.
So I have try to log in user manually in test to make some actions in back-end.
Here is my security.yml
security:
...
role_hierarchy:
ROLE_SILVER: [ROLE_BRONZE]
ROLE_GOLD: [ROLE_BRONZE, ROLE_SILVER]
ROLE_PLATINUM: [ROLE_BRONZE, ROLE_SILVER, ROLE_GOLD]
ROLE_ADMIN: [ROLE_BRONZE, ROLE_SILVER, ROLE_GOLD, ROLE_PLATINUM, ROLE_ALLOWED_TO_SWITCH]
providers:
database:
entity: { class: Fox\PersonBundle\Entity\Person, property: username }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/person/login$
security: false
main:
pattern: ^/
provider: database
form_login:
check_path: /person/login-check
login_path: /person/login
default_target_path: /person/view
always_use_default_target_path: true
logout:
path: /person/logout
target: /
anonymous: true
access_control:
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/person/registration, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/person, roles: ROLE_BRONZE }
Here is my test:
class ProfileControllerTest extends WebTestCase
{
public function setUp()
{
$kernel = self::getKernelClass();
self::$kernel = new $kernel('dev', true);
self::$kernel->boot();
}
public function testView()
{
$client = static::createClient();
$person = self::$kernel->getContainer()->get('doctrine')->getRepository('FoxPersonBundle:Person')->findOneByUsername('master');
$token = new UsernamePasswordToken($person, $person->getPassword(), 'main', $person->getRoles());
self::$kernel->getContainer()->get('security.context')->setToken($token);
self::$kernel->getContainer()->get('event_dispatcher')->dispatch(
AuthenticationEvents::AUTHENTICATION_SUCCESS,
new AuthenticationEvent($token));
$crawler = $client->request('GET', '/person/view');
}
And when I run this test, $person = $this->get(security.context)->getToken()->getUser(); method is not working in testing Controller. Say if in controller call $person->getId(); I will have an error Call to a member function getId() on a non-object in... .
So can you tell the properly way to log in user in functional test in Symfony 2.3?
Thanks!
EDIT_1:
If I change Symfony/Component/Security/Http/Firewall/ContextListener.php and comment one string:
if (null === $session || null === $token = $session->get('_security_'.$this->contextKey)) {
// $this->context->setToken(null);
return;
}
all tests going on without errors.
EDIT_2:
This is reference that i have trying to use:
first
second
third
fourth
fifth
sixth
seventh
eighth
nineth
Finaly i solve it! This is example of working code:
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\BrowserKit\Cookie;
class ProfileControllerTest extends WebTestCase
{
protected function createAuthorizedClient()
{
$client = static::createClient();
$container = static::$kernel->getContainer();
$session = $container->get('session');
$person = self::$kernel->getContainer()->get('doctrine')->getRepository('FoxPersonBundle:Person')->findOneByUsername('master');
$token = new UsernamePasswordToken($person, null, 'main', $person->getRoles());
$session->set('_security_main', serialize($token));
$session->save();
$client->getCookieJar()->set(new Cookie($session->getName(), $session->getId()));
return $client;
}
public function testView()
{
$client = $this->createAuthorizedClient();
$crawler = $client->request('GET', '/person/view');
$this->assertEquals(
200,
$client->getResponse()->getStatusCode()
);
}
Hope it helps to save your time and nerves ;)
As an addition to the accepted solution I will show my function to login user in controller.
// <!-- Symfony 2.4 --> //
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
private function loginUser(UsernamePasswordToken $token, Request $request) {
$this->get('security.context')->setToken($token);
$s = $this->get('session');
$s->set('_security_main', serialize($token)); // `main` is firewall name
$s->save();
$ed = $this->get('event_dispatcher');
$ed->dispatch(
AuthenticationEvents::AUTHENTICATION_SUCCESS,
new AuthenticationEvent($token)
);
$ed->dispatch(
"security.interactive_login",
new InteractiveLoginEvent($request, $token)
);
}