I'm implementing Lexik JWT library with Sf 4.1.
In my case I have to create a JWT Token when needed for several applications through custom authenticator.
I have followed the lexik documentation, however I am facing an issue for couple of hours for signing my token.
The only thing different than casual case : I use doctrine-odm insteand of doctrine-orm for using MongoDb.
Here the files :
security.yaml :
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
form_login:
provider: fos_userbundle
csrf_token_generator: security.csrf.token_manager
anonymous: ~
logout:
path: /logout
target: /login
remember_me:
secret: '%env(APP_SECRET)%'
guard:
authenticators:
- App\Security\GuardAuthenticator\LoginFormAuthenticator
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
lexik_jwt_authentication.yaml :
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(resolve:JWT_PASSPHRASE)%'
token_ttl: 3600
LoginFormAuthenticator.php (onAuthenticationSuccess method) :
/**
* #param Request $request
* #param TokenInterface $token
* #param string $providerKey
*
* #return null|JsonResponse
*/
public function onAuthenticationSuccess(
Request $request,
TokenInterface $token,
$providerKey
): ?JsonResponse {
/** #var User $user */
$user = $token->getUser();
$apiToken = $this->jwtTokenManager->create($user);
$user->setApiToken($apiToken);
$this->documentManager->persist($user);
$this->documentManager->flush();
return new JsonResponse(['Authorization' => $apiToken]);
}
private.pem :
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,F05739F4D47EE90DADA678BA60000AE4
<sensitive data>
-----END RSA PRIVATE KEY-----
I tried to inspect parameters passed to create or sign method in vendor :
The "key" parameter passed is the path string to the file, and it is not working, getting "
Unable to create a signed JWT from the given configuration." error
Do you have any piece of advice to help me please ?
use this method to create jwt token
public function getTokenUser(JWTTokenManagerInterface $JWTManager,ManagerRegistry $mr,UserPasswordHasherInterface $hasher)
{
$em = $mr->getManager();
$user = $em->getRepository(User::class)->findOneBy(['email' => 'user']);
if($hasher->isPasswordValid($user, 'user')){
$token = $JWTManager->create($user);
return new JsonResponse(['token' => $token]);
}
return new JsonResponse(['error' => 'Invalid credentials'], Response::HTTP_UNAUTHORIZED);
// ...
}
Related
I watched the latest symfony cast on api platform and security (chapter 2: API Plateform Security) I'm blocking in chapter 5 (Login Success & the Session) of the latter. When I send a POST request with axios from my Vuejs application with the user's credentials, the API does send me the IRI of my user. Until all is well but when I try to send a GET request to my API to display the information of the user in question it returns me a 401 error because yes I have set up a voting system so that there is only the owner of the data that it draws to access it. So my user is not logged in and I am blocking myself there.
Here is my SecurityController login function:
/**
* #Route("/api/login", name="api_login", methods={"POST"})
*/
public function login(IriConverterInterface $iriConverter)
{
if(!$this->isGranted('IS_AUTHENTICATED_FULLY')){
return $this->json([
'error' => 'Invalid login reuqest'
], 400);
}
return $this->json([
'location' => $iriConverter->getIriFromItem($this->getUSer())
]);
}
Here is my security.yaml :
security:
encoders:
App\Entity\User:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: app_user_provider
logout:
path: api_logout
stateless: false
json_login:
check_path: api_login
username_path: email
password_path: password
And here is the annotation my entity User and my user_voter :
/**
* #ORM\Entity(repositoryClass=UserRepository::class)
* #UniqueEntity(fields={"email"}, message="Email existant")
* #ApiResource(
* normalizationContext={"groups"={"user:read"}},
* denormalizationContext={"groups"={"user:write"}},
* collectionOperations={
* "GET",
* "POST",
* },
* itemOperations={
* "GET"= {"security" = "is_granted('USER_VOTER', object)"},
* "PATCH"= {"security" = "is_granted('USER_VOTER', object)"},
* "DELETE"
* },
* )
*/
public function vote(TokenInterface $token, $subject, array $attributes)
{
if(!$subject instanceof User){
return self::ACCESS_ABSTAIN;
}
if(!in_array('USER_VOTER', $attributes)){
return self::ACCESS_ABSTAIN;
}
$user = $token->getUser();
if(!$user instanceof UserInterface){
return self::ACCESS_DENIED;
}
if($subject !== $user){
return self::ACCESS_DENIED;
}
return self::ACCESS_GRANTED;
}
PS: Sorry I started since january on symfony
Use lexik_jwt_authentication for generate jwt token.
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api/v1/
stateless: true
anonymous: true
provider: app_user_provider
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
main:
anonymous: true
json_login:
check_path: /login
username_path: username
password_path: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
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'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) {
...
I am trying to configure remember me feature of symfony using their default mechanism as described here; but couldn't make it work.
Cookie named REMEMBERME is created, but is set to deleted and its expire date is 1970. This is why I suppose remember me function is not working. However, when I use (always_remember_me: true) in security.yml, the code works nicely but it doesn't suite my purpose. Using (always_remember_me: true) even if user doesn't check REMEMBER ME checkbox in UI the cookie gets created.
Any help is highly appreciated
I am using the version 2.3.7
This is my complete security.yml file:
security:
firewalls:
main:
pattern: ^/
anonymous: ~
remember_me:
key: "%secret%"
lifetime: 31536000 # a year
domain: ~
path: /
remember_me_parameter: _remember_me
#always_remember_me: true
form_login:
login_path: _my_login
check_path: _my_login_check
always_use_default_target_path: true
default_target_path: /out/homepage
remember_me: true
logout:
path: _my_logout
target: _my_login
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }
providers:
chain_provider:
chain:
providers: [in_memory, user_db]
in_memory:
memory:
users:
user: { password: user, roles: 'ROLE_USER' }
admin: { password: admin, roles: ['ROLE_ADMIN', 'ROLE_USER'] }
user_db:
entity: {class: ProjectName\StoreBundle\Entity\User, property: username}
encoders:
ProjectName\StoreBundle\Entity\User:
algorithm: sha1
iterations: 1
encode_as_base64: false
Symfony\Component\Security\Core\User\User: plaintext
LoginController.php
class LoginController extends Controller
{
/**
* #Route("/login", name="_my_login")
* #Template()
*/
public function loginAction()
{
$request = $this->getRequest();
$session = $request->getSession();
// get the login error if there is one
if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(
SecurityContext::AUTHENTICATION_ERROR
);
} else {
$error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
$session->remove(SecurityContext::AUTHENTICATION_ERROR);
}
return array(
// last username entered by the user
'last_username' => $session->get(SecurityContext::LAST_USERNAME),
'error' => $error,
);
}
/**
* #Route("/login_check", name="_my_login_check")
*/
public function loginCheckAction()
{
}
I added the value attribute inside the input tag. After removing it, it worked for now. :)
<input type="checkbox" id="remember_me" name="_remember_me" checked /> Remember me
I suppose the checkbox is named incorrectly. It should have the 'name' attribute set to '_remember_me' to make the magic happen. Can you post the content of the form template?