I'm writing a bundle for Symfony2 and I need to write a custom authentication system with guards. The whole system is based on tokens. Let's assume I need to send a POST request. In headers I have to include 'TOKEN: testtoken'.
I send a request without 'TOKEN: testtoken' in headers and I get
{ "message": "Authentication Required" }
I send a request with 'TOKEN: badtoken' and I get
{ "message": "Username could not be found." }
Don't look at 'username'. It's mistake.
I send request with 'TOKEN: testtoken' and I get
{ "token": "testtoken" }
It's just example page.
Now I delete 'TOKEN: testtoken' from headers (I use Postman for testing REST APIs) and I get
{ "token": "testtoken" }
I have no idea why. In my opinion in this case my system should return
{ "message": "Authentication Required" }
Here’s my TokenAuthenticator.php
<?php
namespace WLN\AuthTokenBundle\Security;
use Doctrine\ORM\EntityManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
class TokenAuthenticator extends AbstractGuardAuthenticator
{
private $em;
private $user_repository;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function setConfig($user_repo)
{
$this->user_repository = $user_repo;
}
public function getCredentials(Request $request)
{
if($token = $request->headers->get('WLN-AUTH-TOKEN'))
{
return [
'token' => $token
];
}
return null;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = $credentials['token'];
return $this->em->getRepository($this->user_repository)
->findOneBy(array('token' => $token));
}
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = array(
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
);
return new JsonResponse($data, 403);
}
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
'message' => 'Authentication Required'
);
return new JsonResponse($data, 401);
}
public function supportsRememberMe()
{
return false;
}
}
P.S. My app is on shared-hosting. May caching cause it or something like that?
I've figured out what was wrong. When I was requesting valid token in my sessions formed PHPSESSID. Because of that I was getting so freaky behavior of my API. In my case the solution is to set security.firewalls.main.stateless to true :).
Related
I am using PHPUnit test to write functional test for specific endpoint.
Problem with it is that for the authorization process I have to set X-API-KEY in headers section of the request.
I keep getting an error:
Authentication Required
I am using an valid api key form my test database table and it return this specific error I mentioned above.
public function testDoItSuccessful()
{
$client = static::createClient(
[],
['HTTP_x-api-key' => 'clWD0ggquG1Ok2xOVLIcMmPJtu1uYWG']
);
$client->request(
Request::METHOD_POST,
'/api/v1/do-it',
[],
[],
[
'CONTENT_TYPE' => 'application/json',
'ACCEPT' => 'application/json',
],
json_encode($myArray)
);
return $client;
}
As I am new, primarily with authorization process in test env any help is highly appreciated.
Note: I am using Symfony 4.4
I tried THIS.
Error is coming for these two functions in TokenAuthenticator class.
public function supports(Request $request)
{
$hasApiKey = true;
$requestHeaders = $this->getLowerCasedHeaders();
if (!isset($requestHeaders['x-api-key'])) {
$hasApiKey = false;
}
return $hasApiKey;
}
private function getLowerCasedHeaders()
{
$requestHeaders = getallheaders();
return array_change_key_case($requestHeaders, CASE_LOWER);
}
public function getCredentials(Request $request)
{
$requestHeaders = $this->getLowerCasedHeaders();
return $requestHeaders['x-api-key'];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
if (null === $credentials) {
// Code 401 "Unauthorized"
return null;
}
return $this->entityManager->getRepository(Client::class)->findOneBy(['apiKey' => $credentials]);
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($user->getStatus() != Client::STATUS_ACTIVE) {
throw new AuthenticationException("USER_NOT_ACTIVE", 403);
}
$user->setLastSeen(new DateTime('now'));
$this->entityManager->persist($user);
$this->entityManager->flush();
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = [
'message' => 'Authentication failed, wrong api key'
];
if ($exception->getCode() == 403) {
$data = ['message' => $exception->getMessage()];
}
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function start(Request $request, AuthenticationException $authException = null)
{
$data = [
// you might translate this message
'message' => 'Authentication Required'
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
I have changed apache_request_headers() to getallheaders() and tests are passing with Authentication Required message.
When print_r() the:
$requestHeaders = $this->getLowerCasedHeaders();
I can not see my defined headers from request? It returns Array()..
You need to check how the Client Class parse the headers, specially on the test, there is a part where the code execute a search for the word HTTP_ for the custom header. So basically on your test instead of call only x-api-key you need to add the prefix HTTP_. Try with this:
TokenAuthenticator
public function supports(Request $request)
{
return $request->headers->has('x-api-key');
}
On your test:
public function testDoItSuccessful()
{
crawler = $this->client->request(
Request::METHOD_POST,
'/api/v1/do-it',
[],
[],
['HTTP_x-api-key' => self::TOKEN, 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json']
);
$status = $this->client->getResponse()->getStatusCode();
}
For me what worked is to return a UserInterface with the role defined in your security:
access_control:
- { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: ROLE_API }
My firewall:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api/
custom_authenticators:
- App\CrediFastCore\Infrastructure\Security\ApiKeyAuthenticator
Since we need ROLE_API
public function authenticate(Request $request): Passport
{
$apiToken = $request->headers->get('X-AUTH-TOKEN');
if (null === $apiToken) {
throw new CustomUserMessageAuthenticationException('No API token provided');
}
if ($this->parameters->get('api_key') != $apiToken) {
//I compare api token with the one I've set on my parameters.yaml
throw new CustomUserMessageAuthenticationException('Wrong API token provided');
}
$user = new User();
$user->setName('api');
$user->setRoles(['ROLE_API']);
return new SelfValidatingPassport(
new UserBadge($apiToken, function () use ($user, $apiToken) {
return $user;
})
);
}
This part was the "fix"
return new SelfValidatingPassport(
new UserBadge($apiToken, function () use ($user, $apiToken) {
return $user;
})
);
So basically I want to create something like #IsGranted.
I used #IsGranted on my application to access control to prevent a simple user from accessing an admin page for example.
On my entity, I have a boolean field called is_Active
if it's true (1) then the user can use his account
if it's false (0) then he gets redirected to an error page!
In this case, I am not going to test on the Rolesfield of the user but I am gonna test on the is_Active field that's why I can't use the #IsGranted.
I created an error twig page active.html.twig
and I place it on templates folder, and I found myself FORCED to add those 2 lines on every controller function.
if ($this->getUser()->getIsActive()==false) {
return $this->render('active.html.twig');}
Here is an example:
/**
* #IsGranted("ROLE_ADMIN")
* #Route("/", name="user_index", methods={"GET"})
*/
public function index(UserRepository $userRepository): Response
{
if ($this->getUser()->getIsActive()==false) {
return $this->render('active.html.twig');}
return $this->render('user/index.html.twig', [
'users' => $userRepository->findAll(),
]);
}
This is very heavy and bad to add this if statement on every function (I have +30 functions on the app)
Maybe I can create something similar to #IsGranted and use it on the annotation of each function instead?
You can keep using #IsGranted with a custom voter. https://symfony.com/doc/current/security/voters.html#creating-the-custom-voter
Create new voter like in the documentation
public const ACTIVE = 'active';
protected function supports(string $attribute, $subject)
{
return $attribute === self::ACTIVE;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if ($user instanceof User && !$user->isActive()) {
throw new InactiveUserException();
}
return true;
}
Then you can create a listener for InactiveUserException and show what ever you want to the client.
In your controller you'll need to put #IsGranted("active") or #Security(expression="is_granted('active')") before the route method or controller
I would use the authentication for this then you don't have to touch your controllers. You can check if they are logged in and active then they can view the content or if they fail auth then you can direct them to another route with your active.html.twig.
You can also just have this set on certain routes or all of them.
https://symfony.com/doc/current/security/guard_authentication.html
Sample Authenticator and set this just for your admin routes then you can have a normal authenticator without checking for an active user on the checkCredentials for all other routes.
<?php
namespace App\Security;
use App\Entity\User;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Twig\Environment;
class AdminAuthenticator extends AbstractGuardAuthenticator
{
/** #var Environment */
private $twig;
public function __construct(Environment $twig)
{
$this->twig = $twig;
}
public function supports(Request $request): bool
{
$email = $request->request->get('email');
$password = $request->request->get('password');
return $email && $password;
}
public function getCredentials(Request $request)
{
$email = $request->request->get('email');
$password = $request->request->get('password');
return [
'email' => $email,
'password' => $password
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$email = $credentials['email'];
return $userProvider->loadUserByUsername($email);
}
public function checkCredentials($credentials, UserInterface $user)
{
$password = $credentials['password'];
if (!$this->passwordEncoder->isPasswordValid($user, $password)) {
throw new CustomUserMessageAuthenticationException(
'Sorry, you\'ve entered an invalid username or password.'
);
}
if (!$user->isActive()) {
throw new NotActiveUserException(
'This account is not active'
);
}
return true;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
if ($exception instanceof NotActiveUserException) {
// You should redirect here but you get the idea!
$this->twig->render('active.html.twig');
}
// Do something else for any other failed auth
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new JsonResponse('success', Response::HTTP_OK);
}
public function start(Request $request, AuthenticationException $authException = null)
{
return new JsonResponse('Not Authorized', Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
Then in your security.yaml
firewalls:
admin:
pattern: ^/admin
provider: user
guard:
authenticators:
- App\Security\AdminAuthenticator
I'm developing an API that uses not standard database connection that is not supported by Laravel by default. Because of that I am not able to use Eloquent Models to create JWT tokens and authenticate users. I have already implemented a custom user model:
use App\Repositories\Technician as TechnicianRepository;
use Illuminate\Contracts\Auth\Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class Technician implements Authenticatable, JWTSubject {
public function __construct(array $data) {
$this->repository = new TechnicianRepository;
foreach ($this->repository->create($data) as $attribute => $value) {
$this->{$attribute} = $value;
}
}
public function getAuthIdentifierName() {
return 'id';
}
public function getAuthIdentifier() {
return $this->{$this->getAuthIdentifierName()};
}
public function getAuthPassword() {
return decrypt($this->password);
}
public function getRememberToken() {}
public function setRememberToken($value) {}
public function getRememberTokenName() {}
public function getJWTIdentifier()
{
return $this->id;
}
public function getJWTCustomClaims()
{
return [];
}
}
Using this model I am able to successfully generate a JWT token in my AuthController like this:
$technician = new Technician([
'email' => $request->email,
'phone' => $request->phone,
'password' => encrypt($request->password)
]);
$token = app('auth')->login($technician);
However I have no idea how to furtherly authenticate users based on the generated JWT token that is passed with a request.
For now I have the following contents in boot method of AuthServiceProvider:
public function boot()
{
$this->app['auth']->viaRequest('api', function ($request) {
return app('auth')->setRequest($request)->user();
});
}
And the following logic in Authenticate middleware:
if ($this->auth->guard($guard)->guest()) {
return Response::fail([
'auth' => array('Access denied - authorization is required')
], 401);
}
return $next($request);
Even if providing the right JWT token for a user, the middleware denies access.
Any help is appreciated, because I have no idea how to develop user authentication furtherly.
I was able to login and get user token using JWT in laravel.However, while tring to get authenticated user (getAuthUser) by passing that token, I get following error:
"SQLSTATE[42S22]: Column not found: 1054 Unknown column '' in 'where
clause' (SQL: select * from user where `` = 12 limit 1)"
AuthenticationController:
<?php
namespace Modules\Authentication\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests;
use JWTAuth;
use JWTAuthException;
use Modules\Settings\Entities\Users;
use Modules\Authentication\Http\Requests\Authentication;
class AuthenticationController extends Controller
{
public function __construct()
{
// $this->user = new Users;
$this->guard = \Auth::guard('api');
}
public function login(Authentication $request){
$credentials = $request->only('username', 'password');
try {
// verify the credentials and create a token for the user
$token = JWTAuth::attempt($credentials);
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// something went wrong
return response()->json(['error' => 'could_not_create_token'], 500);
}
// if no errors are encountered we can return a JWT
return response()->json(compact('token'));
}
public function getAuthUser(Request $request){
$user = JWTAuth::user($request->token);
// dd($user);
return response()->json(['result' => $user]);
}
}
Users Model:
namespace Modules\Settings\Entities;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class Users extends Model implements JWTSubject,Authenticatable{
Protected $table="user";
// protected $primaryKey = 'id';
protected $fillable = ['id','username','password','user_status_type_id','client_id','created_userid'];
protected $hidden = [
'password', 'remember_token',
];
public function user_status(){
return $this->belongsTo('Modules\Settings\Entities\UserStatusType','user_status_type_id');
}
public function user_client(){
return $this->belongsTo('Modules\Settings\Entities\Client','client_id');
}
public function role()
{
return $this->belongsToMany('Modules\Settings\Entities\Role','user_role','user_id','role_type_id');
}
public function getAuthPassword() {
return $this->password;
}
public function getJWTIdentifier() {
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
public function getAuthIdentifierName(){}
public function getAuthIdentifier(){}
// public function getAuthPassword(){}
public function getRememberToken(){}
public function setRememberToken($value){}
public function getRememberTokenName(){}
}
Route:
Route::group(['middleware' => 'web', 'prefix' => 'api/v1/authentication', 'namespace' => 'Modules\Authentication\Http\Controllers'], function(){
Route::post('auth/login', 'AuthenticationController#login');
// Route::group(['middleware' => 'jwt.auth'], function () {
Route::get('user', 'AuthenticationController#getAuthUser');
// });
});
I am testing it in postman by
GET: ..../api/v1/authentication/user?token={Token}
EDIT:
Now my method for getAuthUser in the controller looks like this:
public function getAuthUser(Request $request){
// $token = JWTAuth::getToken();
// dd($token);
$input = $request->all();
JWTAuth::setToken($input['token']);
// dd($input['token']);
$user = JWTAuth::toUser($input['token']);
// dd($user);
return response()->json(['result' => $user]);
}
and In JWTAuth.php
public function authenticate()
{
// dd($this->getPayload()->get('sub'));
$id = $this->getPayload()->get('sub');
// dd($id);
// dd($this->auth->byId($id));
if (! $this->auth->byId($id)) {
return false;
}
return $this->user();
}
here by doing dd($id), value of id comes but if I try to do dd($this->auth->byId($id)) I get the same error as before.
Try this to retrieve the user instead:
public function getAuthUser()
{
try {
if (! $user = JWTAuth::parseToken()->authenticate()) {
return response()->json(['user_not_found'], 404);
}
} catch (Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
return response()->json(['token_expired'], $e->getStatusCode());
} catch (Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
return response()->json(['token_invalid'], $e->getStatusCode());
} catch (Tymon\JWTAuth\Exceptions\JWTException $e) {
return response()->json(['token_absent'], $e->getStatusCode());
}
// the token is valid and we have found the user via the sub claim
return response()->json(['result' => $user]);
}
Taken from docs, Retreiving the Authenticated user from a token
EDIT:
May not make a difference but just looking at your Model again and I would say that it needs to be
class Users extends Authenticatable implements JWTSubject
rather than
class Users extends Model implements JWTSubject,Authenticatable
I write a custom user provider to validate my user in a external API on external server.
CustomUserProvider:
<?php
namespace App\Auth;
use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable as Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Psr\Http\Message\ResponseInterface;
use Illuminate\Http\Request;
use App\XXX\Facades\XXXX;
use Log;
class CustomUserProvider implements UserProvider {
public $errors = [];
public function getErrorMessages ( ) {
return $this->errors;
}
public function retrieveById($identifier)
{
// TODO: Implement retrieveById() method.
}
public function retrieveByToken($identifier, $token)
{
// TODO: Implement retrieveByToken() method.
return null;
}
public function updateRememberToken(Authenticatable $user, $token)
{
// TODO: Implement updateRememberToken() method.
}
public function retrieveByCredentials(array $credentials)
{
$response =APIService::get( 'GET', $credentials, 'tokens');
if ( array_key_exists('code', $response) ) {
/* Exception o Credentials Error */
$this->errors = $response;
return null;
} else {
$apiCredentials = json_decode( $response->getBody(), true );
if( $apiCredentials ) {
$attributes = array (
'id' => 1,
'name' => 'Name',
'lastname' => 'LastName',
'email' => 'email#email.com'
);
$user = new GenericUser( $attributes );
return $user;
}
}
return null;
}
public function validateCredentials(Authenticatable $user, array $credentials)
{
if( $user->email == $credentials['email'] ) {
return true;
}
return false;
}
}
The problem is that the user if logged fine, but in the next request Auth::user() returns false. Debugging in the middleware Auth:user() is returning false too.