I am creating an AngularJS application with a restful API written in PHP as backend. This is the first time I'm using AngularJS and PHP "together".
Angular is keeping track of the authentication of users using the ngCookies module. Some operations, like deleting stuff, should only be available for users with specific privileges. How can I make sure that "normal" users or users that have not logged in cannot access the deletion operations of the API?
Any ideas are appreciated.
Here is how I do it.
In users DB table I add column named token VARCHAR(36)
Whenever user logs in:
I update lastlogin column
I update that token with MD5($ip.$email.$logindate)
Now I return user object to Angular and angular knows token.
In Angular $http service I add interceptors and before any request is made Authentication header is set. I use basic authentication. I create string $user_id.'::'.$token.
app.factory('authInterceptor', function($rootScope, $q, appConfig, $injector, $cacheFactory) {
function request(config) {
if(angular.isDefined($rootScope.currentUser.id)) {
config.headers.Authorization = 'Basic ' +
window.btoa($rootScope.currentUser.id + ':' +
$rootScope.currentUser.token);
}
return config;
}
function response(response) {
if(angular.isDefined(response.data.code) && parseInt(response.data.code) == 401) {
var UserApi = $injector.get('UserApi');
UserApi.logout();
$cacheFactory.get('$http').removeAll();
UserApi.login(response.data.message)
.catch(function() {
var $state = $injector.get('$state');
$state.go('app.home');
});
}
return response || $q.when(response);
}
return {
request: request,
response: response
};
})
This is my authInterceptor factory that I insert into app
app.config(function($httpProvider) {
$httpProvider.interceptors.push('authInterceptor');
})
What is happening there I set standard Authentication header for every request if user is authorised.
Then in PHP I get this header. I get user ID and Token separately. Then I use user ID to get user data from DB where I have token and last login date.
Now I can compare token and see if this user is the one who logged in.
But this is not absolutely secure. If anyone get this token, then he can login. That is why IP is used. not only I check the token against one in DB I also check it against IP. I create MD5($ip.$email.$logindate) because I know all that data and check against token that I get from angular. If it was sent from different IP it will not pass through.
You can also see function response in authInterceptor. Whenever I have authentication problem I send back HTTP code 401. Now in response I know that Authentication failed. I logout user and redirect him to homepage of the site.
Now it is very simple to code. You just return what have to be returned, and do not care about none authenticated user.
But there is more. If you need some kind of ACL, then you can design this as you wish. In your class that return particular RESTFull API method you can define $acl property and set name of the group. In the same place where you check for authentication, you can check for ACL too.
Please see my code here it is PHP backend and Angular frontend
https://github.com/Coach-Hub
This is the basic Idea, you can of course build around that.
I am not a php developer. You can not ensure this thing in front-end-side. Below is the sample code we use in NodeJs app, to validate user has valid permission of deletion or not.
router.patch('/:id', auth.hasRole(userEnums.roles.admin), controller.update);
router.post('/create', auth.hasRole(userEnums.roles.admin), controller.create);
//Checks if the user role meets the minimum requirements of the route
function hasRole(roleRequired) {
if (!roleRequired) throw new Error('Required role needs to be set');
return compose()
.use(isAuthenticated())
.use(function meetsRequirements(req, res, next) {
if (req.user.role.indexOf(roleRequired) !== -1) {
next();
}
else {
res.send(403);
}
});
}
Here auth.hasRole is simple method/middleware, which is a curry design pattern. It checks use req and checks that user has valid permission to delete. In case of user don't have admin permission it return back with unautherize error message.
This is for admin and user relation. We also use another strategy at the end to validate user. Suppose we have expose our delete API and anyone can delete it. In that case we have to ensure that active user has to be the owner of the document. In that case first we get the owner id of the document and match it with requested user id. If matches than we delete the document.
BlogPostApiService.destroy(id, _.curry(hasPermission)(req.user))
//hasPermission implementation
function hasPermission(user, blogPost) {
return user && (user.hasRole(UserEnums.roles.admin) || (user._id.toString() == blogPost.author.id.toString()));
}
Related
I am new to laravel and angular. I m using Laravel Framework 7.10.2 and Angular CLI: 8.3.26
I configured laravel passport according to documentation. Now when user logged in my api sends access token to my angular app.like this(copied from developer console)
token:"eyJ0eXAiOixxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxx"
this is my laravel login method.
class UserController extends Controller
{
public function login(Request $request){
$login=$request->validate([
'email'=>'required',
'password'=>'required'
]);
if (!Auth::attempt($login)){
return response(['message'=>'Invalid login details.']);
}
$user=Auth::user();
$token=$user->createToken('authToken')->accessToken;
return response(['user'=>Auth::user(),'token'=>$token]);
}
Now I want to save this user id in local storage. But I know it is not secure. So Is it possible to send this user object from this token?
1)If is it possible how can I do this?
2)How can I read this(means user object) in the angular application?
This is my angular function to read the token.
login() {
this.authService.login(this.email, this.password).subscribe(res => {
//reading token comes from api
console.log(res);
if (res.token) {
this.token = res.token;
localStorage.setItem('auth', this.token);
this.router.navigate(['tasks'],);
} else {
this.errormessage = res.message;
}
this.authService.changeProgress(true);
}, err => {
this.errormessage='Connection failed! Try again later!';
this.authService.changeProgress(true);
});
I found some similar questions related to this. But they didn't work for me.
Thanks in advance.
I don't think you need to store all of this, the passport it self identify whit a user.
In my case I need the username to display in a navbar for that I use a service to get the data, maybe I could store it in the browser but that can be done if that info in the local storage is only for display, not if is an ID or something you need to make a request, that is highly insecure.
So, my advice, is to make a request to get the user info that you need.
I am starting on jwt passport but for what I have read you need the secret key to decode the token, that said I believe you can't decode it on the front-end.
Note: if someone has more info comment please
I am building a new Laravel application (v5.4) that will run alongside (installed in the same environment) an existing PHP application that has it's own authentication system. I want the users who have successfully logged in to the existing system to be automatically authenticated in the Laravel app so they can navigate seamlessly between the applications.
My Laravel app has read-only access (through a second db connection) to the existing system's database so it can safely check that the PHP Session matches the session cookie recorded in the database and I can even pull out the user object and hand it to Auth's login() method.
I want to know the best way to put Auth into an authorised state (not guest) and where is the best place to put such code?
Options I've thunked of so far:
Middleware: Check session and call the login() method on Auth from some application-wide middleware?
Extend Illuminate/Auth/SessionGuard.php and override the attempt() method? If so, how do I tell the other parts to use my extended SessionGuard? (I suspect this was not designed to be easily overridden)
Super hacky disgusting way of dynamically setting the user's password to a random hash and then calling Auth/LoginController#login() in the background with a faked request containing that hash as the password field. (I seriously hope this doesn't end up being the most popular answer)
Some other option (?)...
Thanks in advance for your help SO community!
The solution I ran with in the end was creating a middleware that contains this:
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (isSet($_SESSION['intranet_user_id']) && $_SESSION['intranet_user_id']) {
// Log them in manually
$intranet_user_id = $_SESSION['intranet_user_id'];
if (!Auth::guest() && Auth::user()->getId() !== $intranet_user_id ) {
Auth::logout();
}
if (Auth::guest()) {
Auth::login( User::find($intranet_user_id), true);
}
} else {
Auth::logout();
}
I trying to use lumen for the first time to create a restful backend service.
I'm used to working with laravel but with lumen I'm already stuck at the autentication. I can't find any tutorials on this.
I'm not even sure if my logic is secure for this. Bassically I receive a post request which contains an email and a password, then I want to check if the details are correct etc and authenticate the user.
I feel like I'm missing something, is this something that lumes comes with standard or will I need to rewrite the Auth service
It seems to be in the documentation you linked.
$this->app['auth']->viaRequest('api', function ($request) {
// Return User or null...
});
The Request class is passed in to this function. You would need to grab the email and password out of it $request->get('email') and request->get('password'), check to make sure they are valid.
I'm not sure of the best way to do this with Lumen or how much is available so to make it easy, you could just do something like the following...
$this->app['auth']->viaRequest('api', function ($request) {
$email = $request->get('email');
$password = $request->get('password');
$user = \DB::table('users')->where('email', $email)->first();
// Invalid Email
if ($user === null) {
return null;
}
// Check if password matches
if ( \Hash::check($user->password, $password) ) {
return $user;
}
// Invalid password
return null;
});
Keep in mind Lumen does not support session state you would need to pass in the email and password for every request. However, once it's setup, all you need to do in Lumen is use Auth::user() to grab the user.
You could also use jwt-auth which uses JSON Web Tokens which also makes it fairly easy and allows you to not pass emails and password around.
https://github.com/tymondesigns/jwt-auth
For anyone who encounters this problem. This is how i solved it:
In the auth serviceProvider (boot method) you check if there is a authorization header present. If there is one, it should include a apiToken, witch you can validate and continue with the normal flow.
If there is no Authorization header present, you can check the request variable for a email and password. Validate the login, and on success you save a new apiToken. I returned this token to the frontend, and made a feature that handles all ajax request, to include this token in the header. I also implemented a function that handles every response in my frontend application witch checks for a 401, when its there redirect to the login page.
With this aproach, you can use both auth methods, and Auth::user() is available through your application. Just make sure the login page is not handled with the Auth middleware!
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.
I am quite inexperienced when it comes to the topic of server-side user authentication.
I want to use as few PHP code as possible to achieve the following:
A user can log in to my app. If he does so, i will store all of that users information, including the status of being authenticated to an Angular service.
As a user navigates through my app, i need to check whether or not he is logged in. If he ain't, i need to redirect him immediately.
The question
Would it be enough to set up two session variables when the user has been logged in successfully and then doing something like this on every route change, updating my service and handle the result client-side?
public function getLogStatus(){
return
$_SESSION["isLoggedIn"] == "true" &&
$_SESSION['useradr'] == $_SERVER['REMOTE_ADDR'] ?
true : false;
}
Yes it IS enough.
But I suggest this :
public function checkAuth(){
if(!$_SESSION["isLoggedIn"] || $_SESSION['useradr'] !=$_SERVER['REMOTE_ADDR'])
header('location:"thePage.php"');
}
and call it in the first line of every method that you dont want to non-authed visitors can gain .
public function method(){
$this->checkAuth();
...
}