I have my security file configured as follows:
security:
...
pattern: ^/[members|admin]
form_login:
check_path: /members/auth
login_path: /public/login
failure_forward: false
failure_path: null
logout:
path: /public/logout
target: /
Currently if I access the members url without authenticating it redirects me to /public/login but I dont want it to redirect. I'm mainly responding with json on my controllers so I just want to show a warning on the restricted url such as {"error": "Access denied"}. If I take out the login_path: /public/login code it redirects to a default url /login. How do I do to stop it from redirecting?
You need to create a Listener and then trigger your response. My solution is based on - https://gist.github.com/xanf/1015146
Listener Code --
namespace Your\NameSpace\Bundle\Listener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
class AjaxAuthenticationListener
{
/**
* Handles security related exceptions.
*
* #param GetResponseForExceptionEvent $event An GetResponseForExceptionEvent instance
*/
public function onCoreException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
$request = $event->getRequest();
if ($request->isXmlHttpRequest()) {
if ($exception instanceof AuthenticationException || $exception instanceof AccessDeniedException || $exception instanceof AuthenticationCredentialsNotFoundException) {
$responseData = array('status' => 401, 'msg' => 'User Not Authenticated');
$response = new JsonResponse();
$response->setData($responseData);
$response->setStatusCode($responseData['status']);
$event->setResponse($response);
}
}
}
}
You need to create a service for the listener --
e_ent_int_baems.ajaxauthlistener:
class: Your\NameSpace\Bundle\Listener\AjaxAuthenticationListener
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onCoreException, priority: 1000 }
You can do like I did:
in security.yml
firewalls:
administrators:
pattern: ^/
form_login:
check_path: _security_check
login_path: _security_login
logout: true
security: true
anonymous: true
access_denied_url: access_denied
in routing.yml
access_denied:
path: /error403
defaults :
_controller: FrameworkBundle:Template:template
template: 'DpUserBundle:Static:error403.html.twig'
simply add to firewall section *access_denied_url* param
See this page for the full security.yml configuration reference. Also, this is an even better reference with explanations of each key.
I'd suggest creating your own listener class to handle returning JSON when a User needs to login. Example: https://gist.github.com/1015146
Related
I want members to log in from the frontend and I've defined my authentication handler below and added it as a service which gives me a json response as expected.
<?php
namespace AppBundle\Handler;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Router;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class AuthenticationHandler implements AuthenticationSuccessHandlerInterface, AuthenticationFailureHandlerInterface
{
protected $router;
//protected $security;
protected $userManager;
protected $service_container;
public function __construct(RouterInterface $router, $userManager, $service_container)
{
$this->router = $router;
//$this->security = $security;
$this->userManager = $userManager;
$this->service_container = $service_container;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token) {
if ($request->isXmlHttpRequest()) {
$result = array('success' => true);
$response = new Response(json_encode($result));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
else {
// Create a flash message with the authentication error message
$request->getSession()->set(SecurityContext::AUTHENTICATION_ERROR, $exception);
$url = $this->router->generate('fos_user_security_login');
return new RedirectResponse($url);
}
return new RedirectResponse($this->router->generate('anag_new'));
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception) {
if ($request->isXmlHttpRequest()) {
$result = array('success' => false, 'message' => $exception->getMessage());
$response = new Response(json_encode($result));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
return new Response();
}
}
However, I am getting same results regardless of whether a user is registered or not. Here is the response
{"success":false,"message":"Bad credentials."}
Here is my security.yml
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
admin:
pattern: /admin(.*)
context: user
form_login:
provider: fos_userbundle
login_path: /admin/login
use_forward: false
check_path: /admin/login_check
failure_path: null
logout:
path: /admin/logout
target: /admin/login
anonymous: true
main:
pattern: .*
context: user
form_login:
provider: fos_userbundle
login_path: /login
use_forward: false
check_path: fos_user_security_check
failure_path: null
success_handler: authentication_handler
failure_handler: authentication_handler
logout: true
anonymous: true
routing.yml
fos_user_security_check:
path: /login_check
defaults:
_controller: FOSUserBundle:Security:check
fos_user_security_logout:
path: /logout
defaults:
_controller: FOSUserBundle:Security:logout
The esiest way to implement API authentication for me is to implement the brand new Guard Authentication Interface
http://symfony.com/doc/current/cookbook/security/guard-authentication.html
This simple class allows you to define the process, which instantiate, handles and post-processes authentication.
Enabling the service is as easy as
# app/config/security.yml
security:
# ...
firewalls:
# ...
main:
anonymous: ~
logout: ~
guard:
authenticators:
- app.my_authenticator
# if you want, disable storing the user in the session
# stateless: true
# maybe other things, like form_login, remember_me, etc
# ...
You also need a user provide for this
http://symfony.com/doc/current/cookbook/security/custom_provider.html
Using the Guard you can handle any type of custom authentication (bearer, forms, cookies, GET tokens etc)
I'm trying to do functional test for the routes that are behind the firewall. I'm not sure what I'm doing wrong but the test for the route admin/dashboard fails. Any ideas?
<?php
namespace AppBundle\Tests;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
class ApplicationAvailabilityFunctionalTest extends WebTestCase
{
private $client;
public function setUp()
{
$this->client = self::createClient();
}
/**
* #dataProvider urlProvider
*/
public function testPageIsSuccessful($url)
{
$this->client->request('GET', $url);
$this->assertTrue($this->client->getResponse()->isSuccessful());
}
public function urlProvider()
{
$this->logIn();
return array(
array('/'),
array('/admin/login'),
array('/admin/dashboard'),
);
}
public function logIn()
{
$this->client = self::createClient();
$session = $this->client->getContainer()->get('session');
$firewall = 'our_db_provider';
$token = new UsernamePasswordToken('admin', 'admin', $firewall, array('ROLE_ADMIN'));
$session->set('_security_'.$firewall, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
}
//UPDATE
Here's the error I get
1) AppBundle\Tests\ApplicationAvailabilityFunctionalTest::testPageIsSuccessful with data set #2 ('/admin/dashboard')
Failed asserting that false is true.
/Users/me/Projects/cms/src/AppBundle/Tests/ApplicationAvailabilityFunctionalTest.php:27
//UPDATE 2
Here's the dump of $token variable
Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken {#488
-credentials: null
-providerKey: "security"
-user: "admin"
-roles: array:1 [
0 => Symfony\Component\Security\Core\Role\Role {#487
-role: "ROLE_ADMIN"
}
]
-authenticated: true
-attributes: []
}
//UPDATE 3
`security:
encoders:
AppBundle\Entity\Admin\User:
algorithm: bcrypt
providers:
our_db_provider:
entity:
class: AppBundle\Entity\Admin\User
property: username
access_control:
- { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, roles: ROLE_ADMIN }
firewalls:
default:
anonymous: ~
http_basic: ~
form_login:
login_path: /admin/login
check_path: /admin/login_check
csrf_provider: security.csrf.token_manager
logout:
path: /admin/logout
target: /admin/login
provider: our_db_provider
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~`
The route is not public
The failing test is on the /admin/dashboard route that probably is protected by authentication so the server response is not successfully (200 OK) but (403 access denied or 302 redirect)
So you must test your route differently: the route is protected so check for 403 or that redirect to login page
Check the doc about How to Simulate Authentication with a Token in a Functional Test
And test that an authenticated user see correctly the page
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'm developing a RESTful web service in Symfony2 with FOSRest and FOSOauthServer bundles (... and many others). My problem is that with an access token of other user, the api gives response instead of a 403 status code. For example:
I have two users stored on database
userA with tokenA
userB with tokenB
Example Request
http://example.com/api/v1/userA/products?access_token=tokenB
Current Response
{
products: {
0: { ... }
1: { ... }
}
}
But I'm requesting products of user A with an access token of user B. How could I check if access token provided is of the products' owner??
My security.yml file:
security:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
role_hierarchy:
MY_ROLE:
# ...
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_SONATA_ADMIN, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
SONATA:
- ROLE_SONATA_PAGE_ADMIN_PAGE_EDIT
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
admin:
pattern: /admin(.*)
context: user
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
login_path: /admin/login
use_forward: false
check_path: /admin/login_check
failure_path: null
logout:
path: /admin/logout
anonymous: true
# FOSOAuthBundle and FOSRestBundle
oauth_token:
pattern: ^/oauth/v2/token
security: false
# oauth_authorize: commented because there are not oauth login form on this app
# pattern: ^/oauth/v2/auth
# Add your favorite authentication process here
api:
pattern: ^/api
fos_oauth: true
stateless: true
anonymous: false
# This firewall is used to handle the public login area
# This part is handled by the FOS User Bundle
main:
# ...
access_control:
# ...
# API (FOSRestBundle and FOSOAuthBundle)
- { path: ^/api, roles: [IS_AUTHENTICATED_FULLY] }
My routing.yml on ApiBundle
# API Endpoints
app_api_user_get_products:
pattern: /{username}/products
defaults: { _controller: ApiBundle:User:getProducts, _format: json }
methods: GET
My UserController.php
<?php
namespace App\ApiBundle\Controller;
Use App\MainBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// ... more use statments
class UserController extends ApiController {
/**
* List user's products.
*
* #ApiDoc(
* resource = true,
* description="This method must have the access_token parameter. The parameters limit and offset are optional.",
* filters={
* {"name"="access_token", "dataType"="string", "required"="true"},
* {"name"="offset", "dataType"="integer", "default"="0", "required"="false"},
* {"name"="limit", "dataType"="integer", "default"="250", "required"="false"}
* },
* )
*
* #Annotations\QueryParam(name="offset", requirements="\d+", nullable=true, description="Offset from which to start listing products.")
* #Annotations\QueryParam(name="limit", requirements="\d+", default="500", description="How many products to return.")
*
* #Annotations\View()
*
* #param User $user the request object
* #param ParamFetcherInterface $paramFetcher param fetcher service
*
* #return array
*/
public function getProductsAction(User $user, ParamFetcherInterface $paramFetcher, Request $request) {
// $offset = $paramFetcher->get('offset');
// $offset = null == $offset ? 0 : $offset;
// $limit = $paramFetcher->get('limit');
try {
// estructure and exclude fields strategy http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies
$data = array('products' => array());
foreach ($user->getCatalog() as $p) {
if ($p->getAvailable() == true) {
$product = $p->getProduct();
$data['products'][] = array(
'id' => $product->getId(),
'reference' => $product->getReference(),
'brand' => $product->getBrand(),
'description' => $product->getDescription(),
// ...
);
}
}
} catch (\Exception $e) {
throw new HttpException(Codes::HTTP_INTERNAL_SERVER_ERROR, $e->getTraceAsString());
}
// New view
$view = new View($data);
$view->setFormat('json');
return $this->handleView($view);
}
}
Thank you very much for the help!
I've found the solution. It's easy, just I've added the following code in my rest controller and the configuration parameters on app/config.yml
UserController.php
...
public function getProductsAction(User $user, ParamFetcherInterface $paramFetcher, Request $request) {
// Check if the access_token belongs to the user making the request
$requestingUser = $this->get('security.context')->getToken()->getUser();
if (!$requestingUser || $requestingUser !== $user) {
throw new AccessDeniedHttpException();
}
...
~/app/config.yml
# FOSRestBundle
fos_rest:
routing_loader:
default_format: json
param_fetcher_listener: true
view:
view_response_listener: force
access_denied_listener: # I've added this
# all requests using the 'json' format will return a 403 on an access denied violation
json: true
You can also make it simpler using #Security annotation in Symfony >= 2.4 . In your case it'll look like
/**
* #Security("user.getId() == userWithProducts.getId()")
*/
and the action header:
...
public function getProductsAction(User $userWithProducts, ParamFetcherInterface $paramFetcher, Request $request) {
...