FOSRestBundle, FOSOAuthServerBundle, FOSUserBundler - how to integrate them? - symfony

On my symfony2 project, I'm using FOSUSerBundle for login, register, etc on a Website. Works fine, as I expected.
But now I'd like to build a REST API, so that a android app can act as client to and work with the data.
I found FOSRestBundle to build the REST API inside the symfony2 Project. I want to use FOSOAuthServerBundle for handle the tokens for accessing the api.
The User should login via the API and then he can user other methods provided by the api.
I read a lot of Blogs and other documentation, but can't find, how to build the REST Api.
I set up each Bundle and I generated a Client with a public ID and the secure code. Over the Website I can use the login.
But what steps / methods will I have to define in mein REST API Controller to use the token auth?
Thank you!

In this link, you will find a good example for developing your first REST api.
Good luck.

I have recently setup an API using the FOSUser FOSOAuthServer and FOSRest Bundles.
In my security.yml I have the following:
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: fos_user.user_provider.username
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
oauth_token: # Everyone can access the access token URL.
pattern: ^/login
security: false
api:
pattern: / # All URLs are protected
fos_oauth: true # OAuth2 protected resource
stateless: true # Do no set session cookies
anonymous: false
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
This allows anonymous access to the login route while every other route requires Authentication.
I have created a login route that proxies the request over to the OAuth Client. This way the user never knows the client secret: (note i have removed the client id and secret in the example)
/**
* #Post("/login")
*/
public function postLoginAction(Request $request){
$request->request->add( array(
'grant_type' => 'password',
'client_id' => 'clientID_clientRandomID',
'client_secret' => 'clientSecret'
));
return($this->get('fos_oauth_server.controller.token')->tokenAction($request));
}
This will return the OAuth Token if valid user/pass is submitted.
Once I have this token I can add it to the Headers for any requests
Authorization: Bearer OAuth_TOKEN
Once the user is validated you can always check their roles, if needed, in any api calls. Something like the following:
public function getUserAction()
{
$this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!');
$user = $this->getUser();
$view = $this->view($user);
return $this->handleView($view);
}
Another approach for checking roles could be done in security.yml
# app/config/security.yml
security:
# ...
access_control:
- path: "^/api/users/\d+$"
allow_if: "'DELETE' == request.getMethod() and has_role('ROLE_ADMIN')"
I found this in the following post: RESTFul OAuth with FOSOAuthServer / FOSRest & FOSUser
This is how I approached things for a Symfony3 build, some syntax (checking a user role) may be different for Symfony2
I used this post as a reference while building my api: http://williamdurand.fr/2012/08/02/rest-apis-with-symfony2-the-right-way/

Related

Symfony 6 Custom login strange behavior

I have a custom login system where using username/password against LDAP server. Based on the response i know that they are correct. The response is array with user details. I don't use the password hasher, the only information i store about the user is email/name which i recieve from the LDAP response.
My Authenticator
class CustomAuthenticator extends AbstractLoginFormAuthenticator
{
public function authenticate(Request $request): Passport
{
$uname = $request->request->get('uname', '');
$password = $request->request->get('password', '');
// check them against the LDAP server
$ldapData = $this->ldapChecker($uname, $password); // binds ldap server with username/password from login form. If the bind is success the ldap returns array containing user details (name, email etc)
if(is_array($ldapData)){
// if its array the submitted username/password are correct.
$email = $ldapData[0]['email'] // getting the user email from the ldap response
// here i'm checking if the username exist in the database if not i create new user
$checkUserExist = $this->user->findOneBy(['uname'=>$uname]);
if(!$checkUserExist){
// creating new user with the data from ldap response;
// storing name, uname, email
}
return new Passport(
new UserBadge($email, function($userIdentifier) {
$user = $this->user->findOneBy(['email' => $userIdentifier]);
if (!$user) {
throw new UserNotFoundException('passport fail');
}
return $user;
}),
new CustomCredentials(function($credentials, User $user) {
return true;
}, $password)
);
}
// here is logic to handle wrong username password
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new RedirectResponse($this->urlGenerator->generate('app_home'));
}
My security.yaml
security:
# enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
role_hierarchy:
ROLE_ADMIN: ROLE_USER
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\CustomAuthenticator
logout:
path: app_logout
# where to redirect after logout
# target: app_any_route
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/login$, roles: PUBLIC_ACCESS }
- { path: ^/logout$, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: ROLE_USER }
when#test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
The problem is sometimes when i open the app it loads user details from another user / or after loggin it shows details of another user or when i open incognito tab and open app it shows that i'm logged. I disabled the twig cache because i thought is cache problem but the problem persist.

Reload user role after change without re-logging

How to refresh logged in user role e.g. when it has been changed by admin user? I've found the always_authenticate_before_granting security option (it's not included in Symfony 4 documentation) and set it to true.
security.yaml:
security:
always_authenticate_before_granting: true
encoders:
App\Entity\Main\User:
algorithm: bcrypt
providers:
app_user_provider:
entity:
class: App\Entity\Main\User
property: email
role_hierarchy:
ROLE_ADMIN: ROLE_USER
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
guard:
authenticators:
- App\Security\LoginFormAuthenticator
form_login:
login_path: login
check_path: login
logout:
path: logout
target: homepage
remember_me:
secret: '%kernel.secret%'
path: /
access_control:
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/.*, roles: ROLE_USER }
but it doesn't take effect.
UPDATE
I've created onRequest subscriber:
class RequestSubscriber implements EventSubscriberInterface
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'onRequest'
];
}
public function onRequest(GetResponseEvent $event): void
{
if (!$event->isMasterRequest()) {
return;
}
if(!$token = $this->tokenStorage->getToken()) return;
$sessionUser = $token->getUser();
if ($sessionUser instanceof User) {
$this->tokenStorage->setToken(new PostAuthenticationGuardToken($sessionUser, 'main', $sessionUser->getRoles()));
}
}
}
and now I can refresh the updated roles on every request, but comparing sessionUser to databaseUser is pointless, because the sessionUser always contains newly updated roles, though in Symfony Profiler > Security Token are listed the old ones (in case when I don't set the new token, of course).
Tl;dr I'm afraid you will have to introduce a mechanism of your own in order to make this work.
The session token is stored inside the user's session. This will have quite an impact on your application's performance, because each time a call to the database will be required in order to check if the role had changed.
So you will need a request listener which will compare database role with current user role, and if it is not same, replace the token in the session, this time with new role list, eg. (pseudo code):
sessionUser = tokenStorage.token.user
databaseUser = userRepository.find(sessionUser.id)
if sessionUser.roles !== databaseUser.roles
tokenStorage.setToken(new PostAuthenticationGuardToken(…))
or use a cache as a flag carrier to notify the user about the change. This method is going to be much quicker for sure.
sessionUser = tokenStorage.token.user
// of course the flag has to be set when the user's role gets changed
cacheToken = 'users.role_reload.' . sessionUser.id
if cache.has(cacheToken)
databaseUser = userRepository.find(sessionUser.id)
tokenStorage.setToken(new PostAuthenticationGuardToken(…))
cache.remove(cacheToken)
Either way the user has to ask the application has there been role change, on each request.

symfony/skeleton logged in User persisted in session, but not available in getUser()

I've set up a simple symfony/skeleton application that contains user entity and authentication.
When after successful authentication I call a route that contains the following code:
public function index(Security $security)
{
$username = $security->getUser()->getUsername();
This results in an error:
Call to a member function getUsername() on null
So obviously the security component does not now about the currently logged in user.
The session however does contain the current UsernamePasswordToken (I checked using dump).
The same code works fine when I use website-skeleton.
What is missing in skeleton?
My firewall is setup like that:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
logout:
path: ^/user/logout
json_login:
check_path: /user/login
You can get the authenticated user from a controller that extends AbstractController just by using $this->getUser();.

authentication in symfony api with ldaptools+jwt+fosuser

In my api I'm trying to authenticate a user using LdapToolsBundle, FOSUserBundle and LexikJWTAuthenticationBundle. Doing things step by step and following the integration docs for fosuser and ldaptools and later the jwt docs I manage to acomplish the following:
FosUserBundle + LdapToolsBundle was successfull
Api Integration + FosUserBundle + LdapToolsBundle was successfull
Jwt + FosUserBundle + LdapToolsBundle on Api failed.
the problem is that I just can only log in against my database but not ldap.
in my database I have one user record which I created with fos command and any password(making shure the authentication is on ldap and not fosuser). So far so good. but once instroduced JWT the authentication is made by fosuser instead of ldap authentication guard. When I change the password with fos command I can get the token with out problems.
this is my config:
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
ldap:
id: ldap_tools.security.user.ldap_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api_login:
pattern: ^/login
stateless: true
provider: fos_userbundle
anonymous: true
form_login:
check_path: /login
require_previous_session: false
username_parameter: username
password_parameter: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
guard:
authenticators:
- ldap_tools.security.ldap_guard_authenticator
logout: true
api:
pattern: ^/
stateless: true
lexik_jwt: ~
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: IS_AUTHENTICATED_FULLY }
My question is how can I get to work the ldap authenticator? I'll show the wanted workflow
Auth request is received (username and password)
The fosuser provider finds the user or user not found(bad credentials-->end)
The ldap authenticator guard authenticate the user against the domain server or bad credentials -->end
The user is logged in successfully and token is received
but I still getting bad credentials with a registered user in database and domain server (the user credential works)
Thanks in advance!
Diggin into the ldap_tools.security.ldap_guard_authenticator authenticator(namespace LdapTools\Bundle\LdapToolsBundle\Security;) I found this
public function getUser($credentials, UserProviderInterface $userProvider)
{
$domain = $this->ldap->getDomainContext();
try {
$credDomain = isset($credentials['ldap_domain']) ? $credentials['ldap_domain'] : '';
$this->switchDomainIfNeeded($credDomain);
$this->setLdapCredentialsIfNeeded($credentials['username'], $credentials['password'], $userProvider);
$user = $userProvider->loadUserByUsername($credentials['username']);
$this->userChecker->checkPreAuth($user);
return $user;
} catch (UsernameNotFoundException $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} catch (BadCredentialsException $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} catch (LdapConnectionException $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} catch (\Exception $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} finally {
$this->switchDomainBackIfNeeded($domain);
}
}
This method loads an user given an userprovider and some credentials.
So, the getUser relies in the userprovider in order to load any user, but since you are using fos_userbundle as userprovider for your api_login firewall you are, in fact, authenticating against your local database. Try using the ldap userprovider in your config instead.
Of course, doing this you will authenticate against the ldap server and not the flow you descibed above. To do so consider handling authentication by yourself, so you can handle the flow as you want.
Also you can do like this
customauthenticator and in your public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
you can check credentials against your ldap server with the service "#ldap_tools.ldap_manager"->authenticate($user, $password, &$errorMessage = false, &$errorNumber = false).
Here is my answer: Just force the auth after checking the ldap server.
public function onAuthenticationFailure(AuthenticationFailureEvent $event)//when auth fails on DB(Allways!!!)
{
$userToken = $event->getAuthenticationToken();
$username = $userToken->getUsername();
$password = $this->requestStack->getCurrentRequest()->get('password');
if ($this->ldapManager->authenticate($username, $password)) {//good credentials
$token = new UsernamePasswordToken($userToken, 'yes', "public", $userToken->getRoles());
$this->container->get('security.token_storage')->setToken($token);//set a token
$event = new InteractiveLoginEvent($this->requestStack->getCurrentRequest(), $token); //dispatch the auth event
$event->stopPropagation();
$this->container->get('event_dispatcher')->dispatch(SecurityEvents::INTERACTIVE_LOGIN,$event);
}
//symfony takes care of the response
}
So far this is the best answer I've found

Symfony2 auth denied and getToken()->getRoles() different from getToken()->getUser()->getRoles()

I'm using FOSUserBundle to manager users, now I'm trying to secure some controller actions with JMS\SecurityExtraBundle. I have set ROLE_SUPER_USER on my user and protected a method with #Secure(roles="SUPER_ADMIN") but I am not allawed to call the method.
After a LOT of digging up into the code of Symfony2 I think I have traced the issue to the fact that getToken()->getRoles() only returns ROLE_USER whereas getToken()->getUser()->getRoles() correctly returns the user roles, including ROLE_SUPER_USER.
So what could be happening there?
HAHA
I was missing the lines :
providers:
main:
entity: { class: FM\SymSlateBundle\Entity\User, property: username }
in security.yml :
security:
providers:
main:
entity: { class: FM\SymSlateBundle\Entity\User, property: username }
fos_userbundle:
id: fos_user.user_provider.username
Thus the Context was using the default user class which only has USER_ROLE

Resources