FOSRestBundle not catching exceptions - symfony

I'm trying to setup the FOSRestBundle to catch authentication exceptions. My config:
fos_rest:
param_fetcher_listener: true
body_listener: true
format_listener: true
view:
default_engine: php
format_listener:
default_priorities: ['json']
fallback_format: json
prefer_extension: false
access_denied_listener:
json: true
exception:
codes:
Symfony\Component\Security\Core\Exception\AccessDeniedException: 403
twig:
exception_controller: FOS\RestBundle\Controller\ExceptionController::showAction
This config will catch exceptions when they are thrown in a controller, but not when they are thrown from within the security component (i.e. if a user auth fails). Is something wrong with my config, or is the FOSRestBundle simply not designed to intercept exceptions at that point in the stack?
It's worth mentioning that I'm using a custom auth provider based on the WSSE tutorial here:
http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html

authentication exceptions should normally be handled by the authentication failure handler service.
The authentication handlers can be found in your security.yml:
security:
firewalls:
firewall_name:
form_login:
# ...
failure_path: /foo
failure_forward: false
failure_path_parameter: _failure_path
failure_handler: some.service.id
success_handler: some.service.id
see the configuration reference.
See this question for information on how to implement your own failure handler.

I was having this exact same issue. My issue was that WsseListener wasnt actually throwing the exception. Their tutorial has these code:
try {
$authToken = $this->authenticationManager->authenticate($token);
$this->securityContext->setToken($authToken);
return;
} catch (AuthenticationException $failed) {
// ... you might log something here
// To deny the authentication clear the token. This will redirect to the login page.
// Make sure to only clear your token, not those of other authentication listeners.
// $token = $this->securityContext->getToken();
// if ($token instanceof WsseUserToken && $this->providerKey === $token->getProviderKey()) {
// $this->securityContext->setToken(null);
// }
// return;
// Deny authentication with a '403 Forbidden' HTTP response
$response = new Response();
$response->setStatusCode(403);
$event->setResponse($response);
}
// By default deny authorization
$response = new Response();
$response->setStatusCode(403);
$event->setResponse($response);
}
Note how the AuthenticationException is getting caught and then its returning a HTTP Response.
I fixed mine by just throwing $failed:
catch (AuthenticationException $failed) {
throw $failed
}

Related

hwi_oauth Remember Me and IS_AUTHENTICATED_FULLY

I'm working on adding Oauth login to a Symfony2 site. I have the bundle working and configured with paypal and Facebook. I have remember me working. My issue is when a user comes back via remember me and tries to reauthenticate via oauth it tells me the accounts were connected fully but doesn't authenticate me at all. Re logging in with a user name and password works fine.
Config.yml
hwi_oauth:
# name of the firewall in which this bundle is active, this setting MUST be set
firewall_name: main
connect:
account_connector: app.provider.oauth
confirmation: true
resource_owners:
facebook:
type: facebook
client_id: %facebook_client_id%
client_secret: %facebook_client_secret%
scope: "email, public_profile"
infos_url: "https://graph.facebook.com/me?fields=id,name,first_name,last_name,email,picture.type(large)"
paths:
email: email
options:
csrf: true
paypal:
type: paypal
client_id: %paypal_client_id%
client_secret: %paypal_client_secret%
scope: 'openid profile email'
options:
csrf: true
Security.yml
firewalls:
main:
pattern: ^/
anonymous: ~
oauth:
failure_path: /login
login_path: /login
check_path: /login
provider: fos_userbundle
remember_me: true
always_use_default_target_path: false
default_target_path: /login
resource_owners:
facebook: "/external-login/check-facebook"
paypal: "/external-login/check-paypal"
amazon: "/external-login/check-amazon"
oauth_user_provider:
service: app.provider.oauth
remember_me:
key: %secret%
lifetime: 31536000
path: /
domain: ~
always_remember_me: true
form_login:
login_path: /login
check_path: /login_check
success_handler: authentication_handler
failure_handler: authentication_handler
csrf_provider: form.csrf_provider
remember_me: true
logout: true
anonymous: true
switch_user: { role: ROLE_ALLOWED_TO_SWITCH, parameter: _new_user }
remember_me:
key: %secret%
lifetime: 31536000
path: /
domain: ~
always_remember_me: true
Routing.yml
hwi_oauth_redirect:
resource: "#HWIOAuthBundle/Resources/config/routing/redirect.xml"
prefix: /connect
hwi_oauth_login:
resource: "#HWIOAuthBundle/Resources/config/routing/login.xml"
prefix: /external-login/
hwi_oauth_connect:
resource: "#HWIOAuthBundle/Resources/config/routing/connect.xml"
prefix: /external-login/
facebook_login:
pattern: /external-login/check-facebook
paypal_login:
pattern: /external-login/check-paypal
Thanks!
My Own Answer (Solved)
so, I finally found the solution to my problem, maybe it will help others to solve it.
This is why the token failed to be authenticated.
For a returning user, on my home page, I called the is_granted('ROLE_ADMIN'), so the token was authenticated by the method authenticate of the AuthenticationManager, which was called in the AuthorizationChecker, as you can see below
final public function isGranted($attributes, $object = null)
{
if (null === ($token = $this->tokenStorage->getToken())) {
throw new AuthenticationCredentialsNotFoundException('The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.');
}
if ($this->alwaysAuthenticate || !$token->isAuthenticated()) {
$this->tokenStorage->setToken($token = $this->authenticationManager->authenticate($token));
}
if (!\is_array($attributes)) {
$attributes = array($attributes);
}
return $this->accessDecisionManager->decide($token, $attributes, $object);
}
The AuthenticationManager then uses the method authenticate of the OAuthProvider, which is one of its dependencies.This method looks like this:
* {#inheritdoc}
*/
public function authenticate(TokenInterface $token)
{
if (!$this->supports($token)) {
return;
}
// fix connect to external social very time
if ($token->isAuthenticated()) {
return $token;
}
/* #var OAuthToken $token */
$resourceOwner = $this->resourceOwnerMap->getResourceOwnerByName($token->getResourceOwnerName());
$oldToken = $token->isExpired() ? $this->refreshToken($token, $resourceOwner) : $token;
$userResponse = $resourceOwner->getUserInformation($oldToken->getRawToken());
try {
$user = $this->userProvider->loadUserByOAuthUserResponse($userResponse);
} catch (OAuthAwareExceptionInterface $e) {
$e->setToken($oldToken);
$e->setResourceOwnerName($oldToken->getResourceOwnerName());
throw $e;
}
if (!$user instanceof UserInterface) {
throw new AuthenticationServiceException('loadUserByOAuthUserResponse() must return a UserInterface.');
}
$this->userChecker->checkPreAuth($user);
$this->userChecker->checkPostAuth($user);
$token = new OAuthToken($oldToken->getRawToken(), $user->getRoles());
$token->setResourceOwnerName($resourceOwner->getName());
$token->setUser($user);
$token->setAuthenticated(true);
$token->setRefreshToken($oldToken->getRefreshToken());
$token->setCreatedAt($oldToken->getCreatedAt());
return $token;
}
As you can see, for a returning user, the OAuthToken is expired, so the OAuthProvider will try to refresh it
$oldToken = $token->isExpired() ? $this->refreshToken($token, $resourceOwner) : $token;
My issue was that my previous OAuthToken had a null refresh_token option. Although I had set the access_type to offline on my config.yml for the HWIOAuthBundle, it would not work, the refresh_token would still be null
hwi_oauth
resource_owners:
google:
type: google
client_id: "%google_app_id%"
client_secret: "%google_app_secret%"
scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/plus.me https://www.googleapis.com/auth/user.birthday.read"
options:
access_type: offline
prompt: consent
So why was my refresh_token always null? I fell on this other question on stack over flow
Not receiving Google OAuth refresh token
The answers tell you exactly why google does not send you a refresh_token.
Once you used the OAuth authentication without the refresh_token, google, will never send you a refresh token, unless you prompt the APi for consent.
This is why I added the prompt: consent to my configuration file.
So why did all of this generate a bug?
If I don't get the refresh_token from the google API, the userResponse of the OAuthProvider, which is based on the google API response, would have a null username attribute.
$userResponse = $resourceOwner->getUserInformation($oldToken->getRawToken());
Therefore, when the OAuthProvider tries to load a user thanks to the OAuthUserProvider, the username would be null so the OAuthUserProvider would not find the user.
$user = $this->userProvider->loadUserByOAuthUserResponse($userResponse);
Therefore, I needed to get the refresh_token back from google in order to be able to have a correct userResponse with a non-empty username attribute.
Changing my configuration for hwi_oauth and adding these two options to google ressource_owners solved my problem
options:
access_type: offline
prompt: consent

LexikJWTAuthenticationBundle returning 401 for invalid token on anonymous route

I'm using this LexikJWTAuthenticationBundle with FosUserBundle.
I have this in security.yml :
firewalls:
app:
pattern: ^/api
stateless: true
anonymous: true
lexik_jwt: ~
with the following access_control :
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/api/user/action2, roles: IS_AUTHENTICATED_ANONYMOUSLY }
The behaviour I was expecting for /api/user/action2 is having access no matter what is inside the request header. However I'm getting a 401 in the case where the Authorization Bearer is set but not valid (it is ok with valid token or no Authorization Bearer at all).
My use case is I need to check in my controller if the user is logged in but if not, I still want to let that anonymous user access the route.
You have to create a specific firewall for the route/pattern you want allow for anonymous users :
action2:
pattern: ^/api/user/action2
anonymous: true
lexik_jwt: ~
Then, just move your less-protected access_control just before the fully-protected :
- { path: ^/api/user/action2, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
In this way, you are application doesn't care about an Authorization header, and all users can access the resource without JWT.
Update
Change the anonymous route's firewall to :
action2:
pattern: ^/api/user/action2
anonymous: true
lexik_jwt: ~
And make the access_control accepting anonymous And fully authenticated users :
- { path: ^/api/user/action2, roles: [IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_FULLY] }
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
Please use the same order and clear your cache correctly.
It's working well in my JWT/FOSUB application, if it doesn't work for you I'll give you a working ready-to-use example.
And the controller :
$currentToken = $this->get('security.token_storage')->getToken();
if (is_object($currentToken->getUser())) {
// Do your logic with the current user
return new JsonResponse(['user' => $currentToken->getUser()->getUsername()]);
} else {
return new JsonResponse(['user' => 'Anonymous']);
}
Hope it works for you.
I resolved your problem in this way:
api_public:
pattern: ^/api/v1/public
anonymous: true
lexik_jwt:
authorization_header:
enabled: false
prefix: Bearer
query_parameter:
enabled: false
name: bearer
api:
pattern: ^/api
stateless: true
anonymous: true
lexik_jwt:
authorization_header:
enabled: true
prefix: Bearer
query_parameter:
enabled: true
name: bearer
*** For those landing here in 2022 ***
To allow anonymous access with JWT
You must write your own JWTAuthenticator class -
(Code Source)
// src/Security/JWTAuthenticator.php
namespace App\Security;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
// use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; // For Symfony 4.4 and above
final class JWTAuthenticator extends JWTTokenAuthenticator
{
private $firewallMap;
public function __construct(
JWTTokenManagerInterface $jwtManager,
EventDispatcherInterface $dispatcher,
TokenExtractorInterface $tokenExtractor,
// TokenStorage $tokenStorage, // For Symfony 4.4 and above
FirewallMap $firewallMap
) {
parent::__construct($jwtManager, $dispatcher, $tokenExtractor);
// For Symfony 4.4 and above, use the next line instead of the above one
// parent::__construct($jwtManager, $dispatcher, $tokenExtractor, $tokenStorage);
$this->firewallMap = $firewallMap;
}
/* For Symfony 3.x and below */
public function getCredentials(Request $request)
{
try {
return parent::getCredentials($request);
} catch (AuthenticationException $e) {
$firewall = $this->firewallMap->getFirewallConfig($request);
// if anonymous is allowed, do not throw error
if ($firewall->allowsAnonymous()) {
return;
}
throw $e;
}
}
/* For Symfony 4.x and above */
public function supports(Request $request) {
try {
return parent::supports($request) && parent::getCredentials($request);
} catch (AuthenticationException $e) {
$firewall = $this->firewallMap->getFirewallConfig($request);
// if anonymous is allowed, skip authenticator
if ($firewall->allowsAnonymous()) {
return false;
}
throw $e;
}
}
}
Register this class as a service by adding the following to your services.yaml file
app.jwt_authenticator:
#autowire: false # uncomment if you had autowire enabled.
autoconfigure: false
public: false
parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator
class: App\Security\JWTAuthenticator
arguments: ['#security.firewall.map']
Then update the firewall in security.yaml to use the newly registered service
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- app.jwt_authenticator
Lastly, here's a complete tutorial to setup Lexik JWT bundle with Symfony.
I had to add token extractor to config. I was wrong thinking this is enabled by default.
# lexic_jwt_authentication.yaml
token_extractors:
authorization_header:
enabled: true
prefix: Bearer
name: Authorization

Symfony2 Getting the Current User

I'm developing an api, and I can't get the current user when I try the BasicAuth :
FatalErrorException: Error: Call to a member function getUser() on a non-object
Below you'll find the security.yml part that cause the error (I think it does) :
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: true
stateless: true
http_basic:
realm: "REST Service Realm"
provider: fos_userbundle
access_control:
- { path: ^/users/me, role: IS_AUTHENTICATED_FULLY }
I just put the path in access_control I'm testing. I've several more.
I'm using FOSRestBundle as well as FOSUserBundle (as you can see) and I didn't want to put a prefix, because it would be redundant :
// Routing.yml
rest :
type : rest
resource : "routing_rest.yml"
Now the part where the error occur is in the UserController (extending the FOSUser one) :
private function response($data, $status, $groups = array())
{
$currentUser = $this->container->get('security.context')->getToken()->getUser();
if (!$currentUser)
$groups = array("anon");
else
{
if ($currentUser->hasRole("ROLE_SUPER_ADMIN"))
array_push($groups, "admin");
else if ($currentUser->hasRole("ROLE_ADMIN"))
array_push($groups, "admin");
else if ($currentUser->hasRole("ROLE_USER"))
array_push($groups, "user");
}
return $this->view($data, $status)->setSerializationContext(SerializationContext::create()->setGroups($groups));
}
Any idea what's wrong ?
The problem is that your token is null. You have an unknown user if your token does not exist.
$token = $this->container->get('security.context')->getToken();
if (!$token) {
return ('anon.');
}
$user = $token->getUser();

Changing default check_path behavior in Symfony2 [duplicate]

I need to disable redirection after login check, because I need to get only that the login was success or not. After submission /login_check url give me the right data, but keep redirecting to /login (on failure).
/login is blank after that.
I am trying to set up login form using extjs 4 so I need to validate trough an ajax post request.
login_check should authenticate, create user session and return whether it was success or failure, but no forwarding anywhere.
my login.html.twig looks like:
{% if is_granted("IS_AUTHENTICATED_REMEMBERED") %}
{ success:true }
{% else %}
{ success: false }
{% endif %}
and in security.yml:
firewalls:
main:
form_login:
provider: fos_userbundle
failure_path: null
failure_forward: false
Create an authentication handler:
namespace YourVendor\UserBundle\Handler;
// "use" statements here
class AuthenticationHandler
implements AuthenticationSuccessHandlerInterface,
AuthenticationFailureHandlerInterface
{
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
if ($request->isXmlHttpRequest()) {
$result = array('success' => true);
return new Response(json_encode($result));
} else {
// Handle non XmlHttp request here
}
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
if ($request->isXmlHttpRequest()) {
$result = array('success' => false);
return new Response(json_encode($result));
} else {
// Handle non XmlHttp request here
}
}
}
Register the handler as a service:
services:
authentication_handler:
class: YourVendor\UserBundle\Handler\AuthenticationHandler
Register the service in the firewall:
firewalls:
main:
form_login:
success_handler: authentication_handler
failure_handler: authentication_handler
This is a rough example to give you the general idea — you'll need to figure out the details by yourself. If you're stuck and need further clarifications, put your questions in the comments and I'll try to elaborate the example.
The normal symfony flow is to redirect you to a login page if you are not logged in, which works fine for humans. But you seems to be looking for a programmatic solution.
Have you tried setting _target_path in your form, to declare what the "next page" should be? Symfony is always going to forward you somewhere, but you can set that somewhere to wherever you want.
I found these two pages useful for describing the inner workings of the login form:
How to customize your form login (search for _target_path)
Security page in the handbook

Symfony 2 FOSUserBundle with rest login and registration

I have gone through lots of stackoveflow question and articles, but can't find a suitable answer.
I'm using fosuserbundle, hwiouthbundle and lexikjwt bundle.
I'm developing an api based on symfony which will be consumed by an android app and angular app.
Now I need the register and login system with fosuserbundle facebook login with hwiouthbundle and api protection with lexikjwt bundle.
I have implemented fosuserbundle and hwiouthbundke and both working without even writing user controller. But I need this with rest not with form. But I can't out type : rest in router.
Now how can I login, register user with fosuserbundle with rest? I don't want to use fosouth server. Just need registration and login with api not rest from web.
So, if you want register user manually using FOSUserBundle, create a controller and add a register method :
// Acme/AppBundle/Controller/SecurityController
public function registerAction(Request $request)
{
$userManager = $this->get('fos_user.user_manager');
$entityManager = $this->get('doctrine')->getManager();
$data = $request->request->all();
// Do a check for existing user with userManager->findByUsername
$user = $userManager->createUser();
$user->setUsername($data['username']);
// ...
$user->setPlainPassword($data['password']);
$user->setEnabled(true);
$userManager->updateUser($user);
return $this->generateToken($user, 201);
}
And, the generateToken method
protected function generateToken($user, $statusCode = 200)
{
// Generate the token
$token = $this->get('lexik_jwt_authentication.jwt_manager')->create($user)
$response = array(
'token' => $token,
'user' => $user // Assuming $user is serialized, else you can call getters manually
);
return new JsonResponse($response, $statusCode); // Return a 201 Created with the JWT.
}
And the route
security_register:
path: /api/register
defaults: { _controller: AcmeAppBundle:Security:registerAction }
methods: POST
Configure the firewall same as login
// app/config/security.yml
firewalls:
// ...
register:
pattern: ^/api/register
anonymous: true
stateless: true
// ...
access_control:
// ...
- { path: ^/api/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
For login, juste use the check_path of your FOSUser login firewall.
For more information about the token generation, see JWTManager.
Hope this help you.
EDIT
If you want a full example of LexikJWTAuthenticationBundle + FOSUserBundle + FOSRestBundle implementation see my symfony-rest-api

Resources