How to authenticate/authorize anonymous user for a limited time? - php

Let's say I have an invoice entity. Invoice belongs to some user (invoices.user_id).
If the user enters myapp.com/invoices/1 he needs to sign in to gain access to his invoice. That's pretty normal.
Sometimes invoices.user_id is null (invoice owner doesn't have an account in our system), but we have an invoices.phone_number column.
The goal is to create an authentication system based on SMS code verification for users that don't have the account in our system. If the user confirms that he indeed owns phone number related to the invoice (code verification) I want to grant him temporary access (15 min) to this invoice details page (and only this page).
My first idea was to use a JWT token stored in the session.
My second idea was to use a custom firewall.
Is there any better approach?

Create a kernel.request listener. This way you can act, before anything is executed, and whole application is oblivious to the fact that the user can be logged out any minute.
Call a "service" which will validate the token. If the token is not valid, clear authentication status and override the request. For instance, redirect the user to a "you need to pay again" page.
This way you don't need to modify any code, execute any voters and so on, your whole application can be protected.
As for the authentication itself, go for a custom guard, where you can fully control how the authentication process will work.

You can authenticate a dummy user for 15 minutes using the following action:
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
public function indexAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
/**
* confirm that the user indeed owns
* phone number related to the invoice (code verification)
*/
//create a user for this task only and fetch it
$user = $em->getRepository(User::class)->find(1);
//firewall name used for authentication in security.yml
$firewall = "main_secured_area";
$token = new UsernamePasswordToken($user, null, $firewall, $user->getRoles());
$this->get('security.token_storage')->setToken($token);
$this->get('session')->set("_security_$firewall", serialize($token));
//$lifetime takes number of seconds to define session timeout 15min = 900sec
$this->container->get('session')->migrate($destroy = false, $lifetime = 900);
//fire the login event manually
$event = new InteractiveLoginEvent($request, $token);
$this->get("event_dispatcher")->dispatch("security.interactive_login", $event);
return $this->render('default/index.html.twig');
}

Related

Symfony 6.1 - Programatically login user

I am trying to automatically login in a programatical way. (context; I got a seperate account system for admins, if a person is an admin (s)he should be able to login as a user, so I got the userIdentifier)
In symfony 3.3 the way this was done was by;
$securityContext = $this->get('security.token_storage');
$token = new UsernamePasswordToken($user, $user->getPassword(), 'main'], $user->getRoles());
$securityContext->setToken($token);
In symfony 5.* I did read something about UserAuthenticatorInterface::authenticateUser(UserInterface $user)
However, in Symfony 6 it requires more arguments, one of them being a AuthenticatorInterface and a Request (public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response;). I get that it is used when a user requests authentication with a request that these could be used in this function.
I already tried it by making a UsernamePasswordToken and adding that token to the storage (TokenStorageInterface $tokenStorage);
$token = new UsernamePasswordToken($user, 'main', $user->getRoles());
$tokenStorage->setToken($token);
But then I got;
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.
What is the best way to login an user programatically?

Symfony authentication event that fires only once?

I want to log logins.
But both InteractiveLogin and AuthenticationSuccess events fire on each request (because we use JWT to authenticate, which is passed as a header with every request), and the frontend does the redirection after a successful /api/login call, there is no way for me to know, whether the user just logged in.
How would you approach this?
If you are using a stateless firewall, as is the case when using JWT, the classic InteractiveLogin and AuthenticationSuccess are useless for this.
What you want to log is when the token is actually generated.
If you are using lexik/LexikJWTAuthenticationBundle, you could listen for a JWTCreatedEvent event to register that a user logged in.
class JwtCreatedSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'lexik_jwt_authentication.on_jwt_created' => 'onJwtCreated'
];
}
public function onJwtCreated(JWTCreatedEvent $event): void
{
$user = $event->getUser();
// record your "last logged-in" according depending on your application set-up
}
}
If you are not using this bundle, well, it depends how you are generating your tokens in the first place. But the basic idea would be the same: check for token generation and log that time.
A caveat to take into account: if you are using any kind of "token refresh" to maintain sessions without needing to log-in again, each time you refresh the session you would generate a new token... and register a new log-in time.

Account security by Sending code in email instead of SMS: Laravel 5.2

When we login into our gmail account for the first time or after removing the cache and cookie, we get the window to type a code which is sent to our Mobile.
I am trying to implement this but through email instead of SMS. Below is my approach to implement this.
I am following this link : https://laravel.com/docs/5.2/session
and create a Session table in database. I can also see my browser details in Session Table record. I am not sure if this is the correct approach.
Gmail has provision keep track of multiple browsers. This means if I last time logged in from Firefox and this time from Chrome then I will be asked for code again. Going forward, I will not be asked to fill code for Chrome and Firefox if cache/cookie is not removed.
Can somebody give me any link that explains how to make provision for multiple browsers when it is cache/cookie saved ? so that I can send email for security code
You can achieve this by issuing a extra cookie (lets say browser_cookie) to remember the already authenticated browser.
Implementation:
Create a following table (browser_management) :
token (pk)| user_id (fk) | series_identifier
where:
token : hashed form of token issued to the user (using bcrypt or similar algorithm ) (token issued to user, itself, is unguessable randomly generated key from a suitably large space)
series_identifier : unguessable randomly generated key from a suitably large space
Whenever, user logs in check for the browser_cookie.
Case 1: User is logging for the first time.
Considering user is logging in for first time browser_cookie won't be present. So, you would send an email with the authentication code.
Once authenticated, generate two random numbers each for token and series_identifier. Store the hashed token and series_identifier in the browser_management table, for the user identified by user_id.
Also, issue the browser_cookie to the user with token and series_identifier.
Case 2: User is re-logging next time.
Now, when same user logs in next time,take the token and find the entry in the browser_management table with the hashed token.
If found, check if the user_id and series_identifier matches.
Case 2.1: Entries matched:
Allow the user to enter the system without the need to re-authenticate the email code.
Generate another token and replace the token in the cookie as well as table with the new one. (This will lower the risk of session hijacking).
Case 2.2: Entries does not match:
Follow the steps for email authentication and notify the user regarding the possible theft.(Like gmail notifying new browser logins).
References:
Improved Persistent Login Cookie Best Practice
What is the best way to implement “remember me” for a website?
Update:
Sample code:
Migration:
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class browser_management extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('browser_management', function (Blueprint $table) {
$table->string('token');
$table->string('user_id');
$table->string('series_identifier');
$table->timestamps();
$table->primary('token');
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::drop('users');
}
}
Middleware: Create a new middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Cookies;
class Email_verification
{
public function handle($request, Closure $next, $guard = null)
{
//retrieve $token from the user's cookie
$token = $request->cookie('browser_cookie');
//check id token is present
if($token == null){
//if token is not present allow the request to the email_verification
return $next($request);
}
else{
//Retrieve the series_identifier issued to the user
$series_identifier = Auth::user()
->series_identifier(Hash::make($token))
->first()
->series_identifier;
//Check if series_identifier matches
if($series_identifier != $request->cookie('series_identifier')){
//if series_identifier does not match allow the request to the email_verification
return $next($request);
}
}
return redirect('/dashboard'); //replace this with your route for home page
}
}
Make the middleware's entry in the kernel.php
protected $routeMiddleware = [
'email_verification' => \App\Http\Middleware\Email_verification::class,
//your middlewares
];
User Model: Add the following method's to your user model
// method to retrieve series_identifier related to token
public function series_identifier($token){
return $this->hasMany(Browser_management::class)->where('token',$token);
}
//method to retriev the tokens related to user
public function tokens (){
return $this->hasMany(Browser_management::class);
}
Browser_management Model: Create a model to represent browser_managements table
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Browser_management extends Model
{
protected $primaryKey = 'token';
protected $fillable = array('token','series_identifier');
public function User(){
return $this->hasOne('App\Models\User');
}
}
Email Verification Methods: Add the following methods to your AuthController for handling the email verification
public function getVerification(Request $request){
//Create a random string to represent the token to be sent to user via email.
//You can use any string as we are going to hash it in our DB
$token = str_random(16);
//Generate random string to represent series_identifier
$series_identifier = str_random(64);
//Issue cookie to user with the generated series_identifier
Cookie::queue('series_identifier', $series_identifier,43200,null,null,true,true);
//Store the hashed token and series_identifier ini DB
Auth::user()->tokens()->create(['token'=>Hash::make($token)]);
//Your code to send an email for authentication
//return the view with form asking for token
return view('auth.email_verification');
}
public function postVerification(Request $request){
//Retrieve the series_identifier issued to the user in above method
$series_identifier = $request->cookie('series_identifier');
//Retrieve the token associated with the series_identifier
$token = Auth::user()
->tokens()
->where('series_identifier',$series_identifier)
->first()
->value('token');
//Check if the user's token's hash matches our token entry
if(Hash::check($request->token,$token)){
// If token matched, issue the cookie with token id in it. Which we can use in future to authenticate the user
Cookie::queue('token', $token,43200,null,null,true,true);
return redirect('dashboard');
}
//If token did not match, redirect user bak to the form with error
return redirect()->back()
->with('msg','Tokens did not match');
}
Routes: Add these routes for handling email verification requests. We will also add the email_verification middleware to it.
Route::get('/auth/email_verification',`AuthController#getVerification')->middleware('email_verification');
Route::post('/auth/email_verification',`AuthController#postVerification')->middleware('email_verification');<br/>
Update 2:
Regarding the flow of gmail..
I followed following steps:
1)Log into gmail followed by 2-step verification.
2)Log out
3)Clear cache link
4)Log in again
When I loged in again, after clearing the cache, it did not ask me for 2-step verification.
Though, If you clear the cookies, it will ask for 2-step verification.
Reason:
All the user data which identifies the user (here token) is stored in the cookies. And if you clear the cookies, server will have no mechanism to identify the user.
Update 3:
Gmail asking for 2-step verification:
First of all Gmail or any other website is not notified about the clearing of cache
As given here:
The cache is nothing more than a place on your hard disk where the
browser keeps things that it downloaded once in case they’re needed
again.
Now, cookies are the small text files issued by server to store user related information. As given here
The main purpose of a cookie is to identify users and possibly prepare
customized Web pages or to save site login information for you.
So, basically when you clear the cookies in your browser, webserver will not get any user data. So, the user will be considered as guest and will be treated accordingly.
Create an additional table ( besides the session one )
Something like
UserId | UserAgent | IP
And when they go to login check that against their current values in the $_SERVER array. If it's in there all is good, if not interrupt the login, and send them a link to confirm the new data. You'll probably want to do some kind of ajax on the original login to check when they are logged in, then once that happens do the redirect to where they were going.
Make sense.
As I said in the comments for maintainability I would handle it myself and not use any third party APIs, the data is easy enough to verify. That part is relatively trivial, continuing the login process not so much.
OP, if I understand you clearly, you simply want to understand how to implement the laravel session table so you can have multiple login from same user in the same browser:
Schema::create('sessions', function ($table) {
$table->string('id')->unique();
$table->integer('user_id')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->text('payload');
$table->integer('last_activity');
});
While this question has been answered before here I will add that you can easily achieve this feature in your actual login method without modifying your core files.
To do this, you can follow the following logic
Before login, check manually if request user agent header is same as session user agent in session, i.e.:
public function authenticate()
{
$data = Input::all();
$user = User::where('email', '=', $data['email'])->first();
if($user != null)
{
//check if user agent different from session
if($request->header('User-Agent') != session('user_agent'))
{
//do some extra login/password validation check
}
if(Auth::attempt($data))
{
//here you now may manually update the session ID on db
}
}
}
You will have to do substantially more work than this but I hope you get the concept.

Admin login as another user with Symfony2 Security

I have a system, that has 2 roles (admins and users). Authentication made using Security Symfony2 component. Admin doesn't know user password. But he should be able to login into the system as user. I have a grid with all users and want to add buttons like "Login as this user". How can I make it?
I have tried, but no prfit:
$userRepo = $this->getDoctrine()->getRepository('FrameFoxBackEndBundle:User');
$this->get('security.context')->getToken()->setUser($userRepo->find(1));
Why not use built-in switch user option?
I use this code :
// use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
$entity = $userRepo->find(1);
// Authentication
// create the authentication token
$token = new UsernamePasswordToken(
$entity,
null,
'user_db',
$entity->getRoles());
// give it to the security context
$this->container->get('security.context')->setToken($token);
I would use Symfony core support for that manner.
Have a look at:
http://symfony.com/doc/current/cookbook/security/impersonating_user.html.
You define a role which is allowed to switch user, and a parameter in the url that allows you to switch the user.

Setting Symfony2 security.context User data after login

Using Symfony2 I want to augment the security.content user after login with information obtained after login.
So in the login success code I do the following in AccountController:
$user = $this->get('security.context')->getToken()->getUser();
// Get everything we'll ever need for this user
$user->fillDetails($this, $this->container);
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
// Give it to the security context
$this->container->get('security.context')->setToken($token);
return $this->redirect($this->generateUrl('AccountBundle_homepage'));
If I immediately retrieve the user again after calling setToken() this information that is set in the User object in fillDetails() is still present.
However in the controller action for AccountBundle_homepage when I get the user using
$user = $this->get('security.context')->getToken()->getUser();
The extra information I set in fillDetails() is no longer there, or 0.
Any help appreciated.
The security context creates a token on each request, that means you can't modify a token, redirect the user and expect getting data set on the previous token. If you don't persist your user, it won't work. The user is reloaded on each request too.
You can find more information about the token here: http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html#the-token

Categories