I want to put an authenticator in my API, this anthenticator have to check if the ID given in the headers is in my database. The problem is that I don't understand how this work.
I have read the docs but keep don't understand. (Maybe because of my english).
My API don't need login and password, I just want him to check at every request.
For now I have an error :
Uncaught PHP Exception UnexpectedValueException: "The App\Security\AppAuthenticator::getUser() method must return a UserInterface. You returned boolean
But i'm sure that I have a lot of another problem in my code.
There is my AppAuthenticator :
class AppAuthenticator extends AbstractGuardAuthenticator
{
public function supports(Request $request)
{
return true;
}
public function getCredentials(Request $request)
{
return [
'token' => $request->headers->has('DDMDL-AUTH-TOKEN'),
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$apiToken = $credentials['token'];
if (null === $apiToken) {
return;
}
require __DIR__ . DIRECTORY_SEPARATOR . ".." . DIRECTORY_SEPARATOR . "Entity" . DIRECTORY_SEPARATOR . "sqlconnect.php";
$auth = 'SELECT ID FROM MOBILEDDMDL WHERE ID = :idtel ';
$prepauth = $pdo->prepare($auth);
$prepauth->execute(array(':idtel' => $apiToken));
$user = $prepauth->fetch();
if ($user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Pas autorisé');
}
return $user;
//return $this->em->getRepository(User::class)->findOneBy(['apiToken' => $apiToken]);
}
public function checkCredentials($credentials, UserInterface $user)
{
// todo
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// todo
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
}
public function start(Request $request, AuthenticationException $authException = null)
{
// todo
}
public function supportsRememberMe()
{
// todo
}
}
getUser() want you to return a UserInterface instead instead of your boolean. I believe your User::class extends UserInterface so the commented line was the correct variable to return:
//return $this->em->getRepository(User::class)->findOneBy(['apiToken' => $apiToken]);
My getUser looks like :
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
if(strpos($credentials['identifiant'], '#') !== false){
$user = $this->entityManager->getRepository(Utilisateur::class)->findOneBy(['email' => $credentials['identifiant']]);
} else {
$user = $this->entityManager->getRepository(Utilisateur::class)->findOneBy(['identifiant' => $credentials['identifiant']]);
}
if (!$user) {
throw new CustomUserMessageAuthenticationException('Aucun utilisateur trouvé avec ce nom de compte');
}
return $user;
}
checkCredential is the method which verify the password or whatever and this method will return true of false (if the user connexion is ok or not)
My checkCredential looks like :
public function checkCredentials($credentials, UserInterface $user)
{
$passOk1 = password_verify($credentials['password'], $user->getMdp());
$passOk = md5($credentials['password']) === $user->getMdp();
$credential = false;
if($passOk || $passOk1) {
$user->setDerniereConnexion(new DateTime(date('Y-m-d H:i:s')));
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->entityManager->clear();
$credential = true;
}
return $credential;
}
Related
I've set an OIDC authentication with a custom authenticator as follow:
class SsoAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface
{
private UserProviderInterface $user_provider;
private ?LoggerInterface $logger;
public function __construct(LdapUserRepository $ldap_user_repository, UserProviderInterface $user_provider, LoggerInterface $logger = null)
{
$this->user_provider = $user_provider;
$this->logger = $logger;
$this->ldap_user_repository = $ldap_user_repository;
}
public function start(Request $request, AuthenticationException $authException = null): Response
{
$response = new Response();
$response->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', 'emmaus.example.com'));
$response->setStatusCode(401);
return $response;
}
public function supports(Request $request): ?bool
{
return $request->headers->has('Authorization') || $request->get('token');
}
public function authenticate(Request $request): Passport
{
$oidc = new Oidc($this->ldap_user_repository);
$token = $request->get('token');
$decoded_token = $oidc->decodeToken($token);
$user_name = $oidc = $oidc->getUserName($decoded_token);
if(!(is_a($this->user_provider, UserProviderInterface::class)))
{
throw new AuthenticationException('error forbidden buddy');
}
$user_badge = new UserBadge($user_name);
$credentials = new BadgeCredentials();
return new Passport($user_badge, $credentials);
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles());
}
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
return new OidcToken(['ROLE_USER'], null);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$oidc = new Oidc($this->ldap_user_repository);
$token = $request->get('token') ? $request->get('token') : $request->get('Authorization');
$decoded_token = $oidc->decodeToken($token);
$user_identifier = $oidc->getUserName($decoded_token);
$user = $this->ldap_user_repository->findOneBy(['username' => $user_identifier]);
$last_name = $user->getLastName();
$first_name = $user->getFirstName();
$roles = $user->getRoles();
$email = $user->getEmail();
$group_results = array();
$groups = $user->getGroupBase();
foreach($groups as $group)
{
array_push($group_results, $group->getName());
}
$token = $request->get('token') ? $request->get('token') : $request->headers->has('Authorization');
$_SESSION['token_lemon_ldap'] = $token;
$data = array(
'roles' => $roles,
'userName' => $user_identifier,
'firstName' => $first_name,
'lastName' => $last_name,
'email' => $email,
'token' => $token,
'groups' => $group_results
);
return new Response(
json_encode($data)
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$oidc = new Oidc($this->ldap_user_repository);
$decoded_token = $oidc->decodeToken($request->get('token'));
try
{
return $this->start($request, $exception);
}catch(UnexpectedValueException $exception)
{
throw new $exception('wrong number of segment');
}
}
}
?>
I've the authorization to access resources from API after successful authentication, but when I'm fetching response from controller, it return onAutenticationSucess() response's data at each request, and can't access data from controllers, do you have an idea what i'm missing? I'm looking at session or kernel.response, but can't make my head around a proper solution.
Thanks
I'm trying to combine Laravel project and Nova admin panel. In the project I need to reset Nova password, But there is a FatalThrowableError error in PasswordBroker.php file.
This is the error
This is my PasswordBroker.php file.
<?php
namespace Illuminate\Auth\Passwords;
use Closure;
use Illuminate\Support\Arr;
use UnexpectedValueException;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\PasswordBroker as PasswordBrokerContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class PasswordBroker implements PasswordBrokerContract
{
protected $tokens;
protected $users;
protected $passwordValidator;
public function __construct(TokenRepositoryInterface $tokens,
UserProvider $users)
{
$this->users = $users;
$this->tokens = $tokens;
}
public function sendResetLink(array $credentials)
{
$user = $this->getUser($credentials);
if (is_null($user)) {
return static::INVALID_USER;
}
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
return static::RESET_LINK_SENT;
}
public function reset(array $credentials, Closure $callback)
{
$user = $this->validateReset($credentials);
if (! $user instanceof CanResetPasswordContract) {
return $user;
}
$password = $credentials['password'];
$callback($user, $password);
$this->tokens->delete($user);
return static::PASSWORD_RESET;
}
protected function validateReset(array $credentials)
{
if (is_null($user = $this->getUser($credentials))) {
return static::INVALID_USER;
}
if (! $this->validateNewPassword($credentials)) {
return static::INVALID_PASSWORD;
}
if (! $this->tokens->exists($user, $credentials['token'])) {
return static::INVALID_TOKEN;
}
return $user;
}
public function validator(Closure $callback)
{
$this->passwordValidator = $callback;
}
public function validateNewPassword(array $credentials)
{
if (isset($this->passwordValidator)) {
list($password, $confirm) = [
$credentials['password'],
$credentials['password_confirmation'],
];
return call_user_func(
$this->passwordValidator, $credentials
) && $password === $confirm;
}
return $this->validatePasswordWithDefaults($credentials);
}
protected function validatePasswordWithDefaults(array $credentials)
{
list($password, $confirm) = [
$credentials['password'],
$credentials['password_confirmation'],
];
return $password === $confirm && mb_strlen($password) >= 6;
}
public function getUser(array $credentials)
{
$credentials = Arr::except($credentials, ['token']);
$user = $this->users->retrieveByCredentials($credentials);
if ($user && ! $user instanceof CanResetPasswordContract) {
throw new UnexpectedValueException('User must implement CanResetPassword interface.');
}
return $user;
}
public function createToken(CanResetPasswordContract $user)
{
return $this->tokens->create($user);
}
public function deleteToken(CanResetPasswordContract $user)
{
$this->tokens->delete($user);
}
public function tokenExists(CanResetPasswordContract $user, $token)
{
return $this->tokens->exists($user, $token);
}
public function getRepository()
{
return $this->tokens;
}
}
Here I want to reset current password using email. Does anyone have a idea to fix this issue ?
So i created a controller for authentication with 2 methods (token() / native)_). Im using fractal transformer to return response. The token method works fine for me, but the loginAndroid() returns
"Call to a member function createData() on null" error.
Any help? Thank you.
class AuthController extends RestController
{
protected $transformer = UserTransformers::Class;
public function __construct()
{
$this->middleware('auth:api', ['except' => ['login', 'loginAndroid']]);
}
public function login(Request $request)
{
$credentials = $request->only(['username', 'password']);
if (!$token = auth()->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
public function loginAndroid(Request $request)
{
$credentials = $request->only(['username', 'password']);
if (Auth::attempt($credentials)) {
//$user = Auth::user()->with(['employees']);
$userdata = User::with(['employees', 'employees.role', 'employees.branch'])->find(Auth::id());
//$success['token'] = $user->createToken('MyApp')->accessToken;
//return response()->json($userdata, 200);
//return $userdata;
$response = $this->generateItem($userdata);
return $this->sendResponse($response, 201);
} else {
return response()->json('gagal', 401);
}
}
}
this is my restcontroller
abstract class RestController extends Controller
{
protected $manager;
protected $transformer;
public function __construct()
{
$this->manager = new Manager();
}
protected function generateItem($model, $transformer = null)
{
if (!is_null($transformer)) {
return new Item($model, new $transformer);
}
return new Item($model, new $this->transformer);
}
protected function generateCollection($model, $transformer = null)
{
if (!is_null($transformer)) {
return new Collection($model, new $transformer);
}
return new Collection($model, new $this->transformer);
}
protected function sendResponse(ResourceInterface $data, $status = 200)
{
return response()->json(
$this->manager->createData($data)->toArray(),
$status
);
}
protected function sendNotFoundResponse($status)
{
return response()->json($status, 404);
}
protected function sendIseResponse($status)
{
return response()->json($status, 500);
}
}
It looks like your sendResponse() method depends on $this->manager. However, $this->manager gets set in RestController::__construct() and you've overridden the __construct() method in your AuthController::__construct(). So, in order to have $this->manager available, you should call the parent constructor from your AuthController, like this:
class AuthController extends RestController
{
protected $transformer = UserTransformers::Class;
public function __construct()
{
parent::__construct(); // call the parent constructor where
// $this->manager gets initialized
$this->middleware('auth:api', ['except' => ['login', 'loginAndroid']]);
}
... etc
Things we want to achieve in our application are:
Non-unique usernames [Done]
Unique username and email combination
FosUserBundle will fetch all users (on user login) with given username and checks if any of the users has the given password (hashed with bcrypt). When a user is found it logs the user in.
Making username non unique was quite simple by just overriding the username field in the user ORM. But we're kinda stuck with how to proceed in achieving the last two points. We've started creating a custom User Provider but it seems Symfony Security can only handle one user(name).
Is there anyone with experience that might be able to help us? If you need more information or code snippets, please ask. Thank you in advance!
So after looking through alot of the documentation for the Symfony Security module we figured it out.
We added an extra field (displayname) to the User model because Symfony is completely build around the fact that usernames are Unique. It always fetches the first user with the given username, this is not what we wanted.
So we started with writing our own Guard Authentication System, this was pretty straight forward although we had to make some adjustments.
This was all working well, but we ran into a problem with the built-in UsernamePasswordFormAuthenticationListener, this listener was still picking up the displayname from the login form. We actually want the unique username so that Symfony knows which user to use.
We created a custom listener that extended the standard listener and made sure the username was not fetched from the login form but from the user token.
So our flow is now like this: The user fills in his username (actually his displayname) and password, the system fetches all users with that displayname. Then we loop these users and check if someone has that password. If so, authenticate the user.
On user create the admin fills in the displayname and the system will autoincrement this as a username. (admin_1, admin_2, ...).
We have to monitor if what #kero said is true, but with Bcrypt it seems that even with simple passwords like "123", it results in a different hash for each user.
The only thing that is left is to have a UniqueConstraint on the unique combination of the displayname and email. If anyone knows how this can be achieved in our orm.xml and form, thank you.
http://symfony.com/doc/current/security/guard_authentication.html
Custom Guard Authenticator
class Authenticator extends AbstractGuardAuthenticator
{
private $encoderFactory;
private $userRepository;
private $tokenStorage;
private $router;
public function __construct(EncoderFactoryInterface $encoderFactory, UserRepositoryInterface $userRepository, TokenStorageInterface $tokenStorage, Router $router)
{
$this->encoderFactory = $encoderFactory;
$this->userRepository = $userRepository;
$this->tokenStorage = $tokenStorage;
$this->router = $router;
}
/**
* Called on every request. Return whatever credentials you want,
* or null to stop authentication.
*/
public function getCredentials(Request $request)
{
$encoder = $this->encoderFactory->getEncoder(new User());
$displayname = $request->request->get('_username');
$password = $request->request->get('_password');
$users = $this->userRepository->findByDisplayname($displayname);
if ($users !== []) {
foreach ($users as $user) {
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
return ['username' => $user->getUsername(), 'password' => $user->getPassword()];
}
}
} else {
if ($this->tokenStorage->getToken() !== null) {
$user = $this->tokenStorage->getToken()->getUser();
return ['username' => $user->getUsername(), 'password' => $user->getPassword()];
}
}
return null;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
if ($credentials !== null) {
return $userProvider->loadUserByUsername($credentials["username"]);
}
return null;
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($user !== null) {
return true;
} else {
return false;
}
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$exclusions = ['/login'];
if (!in_array($request->getPathInfo(), $exclusions)) {
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
throw $exception;
}
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
// you might translate this message
'message' => 'Authentication Required'
);
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
Custom listener
class CustomAuthListener extends UsernamePasswordFormAuthenticationListener
{
private $csrfTokenManager;
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfTokenManagerInterface $csrfTokenManager = null)
{
parent::__construct($tokenStorage, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'csrf_token_id' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfTokenManager = $csrfTokenManager;
$this->tokenStorage = $tokenStorage;
}
/**
* {#inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if ($user = $this->tokenStorage->getToken() !== null) {
$user = $this->tokenStorage->getToken()->getUser();
$username = $user->getUsername();
if ($this->options['post_only']) {
$password = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']);
} else {
$password = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']);
}
if (strlen($username) > Security::MAX_USERNAME_LENGTH) {
throw new BadCredentialsException('Invalid username.');
}
$request->getSession()->set(Security::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
} else {
return null;
}
}
}
Listener service
<service id="security.authentication.listener.form" class="Your\Path\To\CustomAuthListener" parent="security.authentication.listener.abstract" abstract="true" />
I'm a new user of Slim framework, I've a simple Slim 3 application, with sign in and sign up validation. But I'm not really sure if this is the right/best way to set errors and check if user is logged in -In order to redirect it to his account if session user.id exists.
I used a middleware: AuthMiddleware which includes:
class AuthMiddleware
{
protected $container;
public function __construct($container)
{
$this->container = $container;
}
public function __invoke($request, $response, $next)
{
if (isset($_SESSION['user.id']) && !empty($_SESSION['user.id'])) {
return $response->withRedirect($this->container->router->pathFor('user.index'));
}
$twig = $this->container->view->getEnvironment();
if (isset($_SESSION['validation'])) {
$twig->addGlobal('errors', $_SESSION['validation']['errors']);
$twig->addGlobal('values', $_SESSION['validation']['values']);
unset($_SESSION['validation']);
}
if (isset($_SESSION['auth.signup.success'])) {
$twig->addGlobal('auth_signup_success', $_SESSION['auth.signup.success']);
unset($_SESSION['auth.signup.success']);
}
if (isset($_SESSION['auth.signin.failed'])) {
$twig->addGlobal('auth_signin_failed', $_SESSION['auth.signin.failed']);
unset($_SESSION['auth.signin.failed']);
}
$response = $next($request, $response);
return $response;
}
}
And I used Twig for my views.
Session validation assigned in the validator.php which includes:
class Validator
{
protected $errors = [];
protected $values = [];
public function validate($request, $rules)
{
foreach ($rules as $field => $rule) {
$this->values[$field] = $request->getParam($field);
try {
$rule->setName(ucfirst($field))->assert($request->getParam($field));
} catch (NestedValidationException $e) {
$this->errors[$field] = $e->getMessages()[0];
}
}
if ($this->failed()) {
$_SESSION['validation'] = [
'errors' => $this->errors,
'values' => $this->values,
];
}
return $this;
}
public function failed()
{
return !empty($this->errors);
}
}
Using Respect\Validation. Also, is this the right use of Middlewares?
Thanks in advance.
try creating a separate file for the methods, and calling it from the middleware:
<?php
class AuthMiddleware extends Middleware {
public function __invoke($request, $response, $next) {
if (!$this->container->auth->check()) {
$this->container->flash->addMessage('danger', 'Please sign in to continue.');
return $response->withRedirect($this->container->router->pathFor('auth.signin'));
}
$response = $next($request, $response);
return $response;
}
}
while the Auth class would have those methods to check:
<?php
public function check () {
return isset($_SESSION['user']);
}
public function user() {
if (isset($_SESSION['user'])) {
return User::find($_SESSION['user'])->first();
} else {
return false;
}
}
Don't forget to include the Auth Class within your $app:
<?php
$container['auth'] = function ($container) {
return new \App\Auth\Auth();
};