Asked  8 Months ago    Answers:  5   Viewed   39 times

I'm trying to implement a user policy whereby only one user can login at a time. I'm trying to build this on top of Laravel's Auth driver.

I've thought of using the Session driver to store the sessions in the database and make the keys constant for each username. This is probably a terrible implementation because of session fixation.

What would the implementation be like? What methods in the Auth driver should I be editing? Where would the common session key be stored?

 Answers

10

I recently did this.

My solution was to set the session value when a user logs in. Then I had a small class checking if the session ID stored is the same as the current user who is logged in.

If the user logs in from somewhere else the session ID in the DB will update and the "older" user will be logged out.

I didn't alter the Auth driver or anything, just put it on top when the user logs in. Below happens when login is successful:

$user->last_session = session_id();
$user->save();

To check if the session is valid I used below

if(session_id() != Auth::user()->last_session){
   Auth::logout();
   return true;
}

As you can see I added a column in the users table called last_session

Wednesday, March 31, 2021
 
jsuissa
answered 8 Months ago
87

The problem was that I mapped my passport routes in my RouteServiceProvider and not in my AuthServiceProvider

Saturday, May 29, 2021
 
shin
answered 5 Months ago
61

When you ask the Auth class to attempt a login, you pass in the username and pass as it is. But if you look into the method, it will first hash the password to make it secure and then match it with database entry. When you are storing it, from your present implementation, its not hashed.

As suggested above, you should make this change in your seeder:

array(
    'username' => 'admin',
    'password' => Hash::make('password')
),

Although I am not very sure if the way you are using seeder is correct by syntax, but if it works, just hash the password there.

Saturday, May 29, 2021
 
DCD
answered 5 Months ago
DCD
90

First up: you should never, ever, use md5 to hash passwords. Read this article on why, and then use Go's bcrypt package. You should also parameterise your SQL queries else you are open to catastrophic SQL injection attacks.

Anyway: there are a few problems you need to address here:

  • Your sessions aren't "sticking" is that you're setting the Path as /loginSession - so when a user visits any other path (i.e. /), the session isn't valid for that scope.

You should be setting up a session store on program initialisation and setting the options there:

var store = sessions.NewCookieStore([]byte("something-very-secret"))

func init() {

   store.Options = &sessions.Options{
    Domain:   "localhost",
    Path:     "/",
    MaxAge:   3600 * 8, // 8 hours
    HttpOnly: true,
}

The reason you might set a more specific path is if logged in users are always within a sub-route like /accounts. In your case, that's not what's happening.

I should add that Chrome's "Resource" tab in the Web Inspector (Resources > Cookies) is incredibly useful for debugging issues like these as you can see the cookie expiry, path and other settings.

  • You're also checking session.Values["email"] == nil, which doesn't work. An empty string in Go is just "", and because session.Values is a map[string]interface{}, you need to type assert the value to a string:

i.e.

if val, ok := session.Values["email"].(string); ok {
      // if val is a string
      switch val {
             case "":
                 http.Redirect(res, req, "html/login.html", http.StatusFound)
             default:
                 http.Redirect(res, req, "html/home.html", http.StatusFound)
      }
    } else {
        // if val is not a string type
        http.Redirect(res, req, "html/login.html", http.StatusFound)
    }

We deal with the "not a string" case so we're explicit about what the program should do if the session is not how we expected (client modified it, or an older version of our program used a different type).

  • You are not checking errors when saving your sessions.

    sessionNew.Save(req, res)
    

... should be:

    err := sessionNew.Save(req, res)
    if err != nil {
            // handle the error case
    }
  • You should get/validate the session in SessionHandler before serving static files (you are doing it in a very roundabout way, however):

    func SessionHandler(res http.ResponseWriter, req *http.Request) {
        session, err := store.Get(req, "loginSession")
        if err != nil {
            // Handle the error
        }
    
        if session.Values["email"] == nil {
            http.Redirect(res, req, "html/login.html", http.StatusFound)
        } else {
           http.Redirect(res, req, "html/home.html", http.StatusFound)
        }
        // This shouldn't be here - router isn't scoped in this function! You should set this in your main() and wrap it with a function that checks for a valid session.
        router.PathPrefix("/").Handler(http.FileServer(http.Dir("../static/")))
    }
    
Monday, August 9, 2021
 
jsuissa
answered 3 Months ago
54

When I get this issue, the solution that I found to get my project working was to generate a new token with data from older token on each new request.

My solution, that works for me, is bad, ugly, and can generate more issues if you have many async requests and your API(or business core) server is slow.

For now is working, but I will investigate more this issue, cause after 0.5.3 version the issue continues.

E.g:

Request 1 (GET /login):

Some guest data on token

Request 2 (POST /login response):

User data merged with guest data on old token generating a new token

Procedural code example(you can do better =) ), you can run this on routes.php out of routes, I say that is ugly haha:

// ----------------------------------------------------------------
// AUTH TOKEN WORK
// ----------------------------------------------------------------
$authToken = null;
$getAuthToken = function() use ($authToken, $Response) {
    if($authToken === null) {
         $authToken = JWTAuth::parseToken();
    }
    return $authToken;
};

$getLoggedUser = function() use ($getAuthToken) {
    return $getAuthToken()->authenticate();
};

$getAuthPayload = function() use ($getAuthToken) {
    try {
        return $getAuthToken()->getPayload();
    } catch (Exception $e) {
        return [];
    }
};

$mountAuthPayload = function($customPayload) use ($getLoggedUser, $getAuthPayload) {
    $currentPayload = [];
    try {
        $currentAuthPayload = $getAuthPayload();
        if(count($currentAuthPayload)) {
            $currentPayload = $currentAuthPayload->toArray();
        }
        try {
            if($user = $getLoggedUser()) {
                $currentPayload['user'] = $user;
            }
            $currentPayload['isGuest'] = false;
        } catch (Exception $e) {
            // is guest
        }
    } catch(Exception $e) {
        // Impossible to parse token
    }

    foreach ($customPayload as $key => $value) {
        $currentPayload[$key] = $value;
    }

    return $currentPayload;
};

// ----------------------------------------------------------------
// AUTH TOKEN PAYLOAD
// ----------------------------------------------------------------
try {
    $getLoggedUser();
    $payload = ['isGuest' => false];
} catch (Exception $e) {
    $payload = ['isGuest' => true];
}

try {
    $payload = $mountAuthPayload($payload);
} catch (Exception $e) {
    // Make nothing cause token is invalid, expired, etc., or not exists.
    // Like a guest session. Create a token without user data.
}

Some route(simple example to save user mobile device):

Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function () use ($getLoggedUser, $mountAuthPayload) {
    Route::post('/session/device', function () use ($Response, $getLoggedUser, $mountAuthPayload) {
        $Response = new IlluminateHttpResponse();
        $user = $getLoggedUser();

        // code to save on database the user device from current "session"...

        $payload = app('tymon.jwt.payload.factory')->make($mountAuthPayload(['device' => $user->device->last()->toArray()]));
        $token = JWTAuth::encode($payload);
        $Response->header('Authorization', 'Bearer ' . $token);

        $responseContent = ['setted' => 'true'];

        $Response->setContent($responseContent);
        return $Response;
    });
});
Tuesday, September 21, 2021
 
Angshuman Agarwal
answered 1 Month ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share