Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Adds attempt with callbacks #37030

Closed
wants to merge 5 commits into from
Closed

[8.x] Adds attempt with callbacks #37030

wants to merge 5 commits into from

Conversation

DarkGhostHunter
Copy link
Contributor

@DarkGhostHunter DarkGhostHunter commented Apr 18, 2021

This PR expands on the discussion here about adding callbacks the attempt() authentication mechanisms.

Problem

There is no way to add additional checks at runtime when using attempt(). Any addition forces the developer to create a new Guard, or use nonchalantly validate() and getLastAttempted(), and fire Attempting manually.

if (Auth::validate($credentials) && $user = Auth::getLastAttempted()) {
    if ($user->isAwesomeAndCool()) {
        Auth::login($user, $request->filled('remember'));
        Event::fire(Attempting('session', $credentials, $request->filled('remember')));
        return $this->redirect();
    }
}


return $this->sendAttemptFailed();

Solution

This PR allows the SessionGuard::attempt() method to the receive a third argument that receives one or multiple callbacks. Each of these receive the validated User, and if one returns false, the whole attempt fails.

$result = Auth::attempt($credentials, false, function (Authenticatable $user) {
    return $user->isAwesomeAndCool();
});

return $result 
    ? $this->redirect() 
    : $this->sendAttemptFailed();

Since there are callables, the developer can add its own checks, or use checks made by external packages, all while controlling manually the flow and order of these.

How it works

The change adds an internal function shouldLogin() that executes each callback and returns false if one of them "fails". This is called after the user is properly validated:

if ($this->hasValidCredentials($user, $credentials) && $this->shouldLogin($callbacks, $user)) {
    $this->login($user, $remember);

     return true;
}

Breaking changes: extending the Session Guard


The above change is simple and non-breaking, since it doesn't changes the interface signature, but as a precaution I'm pushing it into the next version... unless it gets approved for 8.x. And this also includes an additional test.

@DarkGhostHunter DarkGhostHunter changed the title Adds attempt with callbacks [8.x/9.x] Adds attempt with callbacks Apr 18, 2021
@DarkGhostHunter DarkGhostHunter changed the title [8.x/9.x] Adds attempt with callbacks [8.x|9.x] Adds attempt with callbacks Apr 18, 2021
@GrahamCampbell GrahamCampbell changed the title [8.x|9.x] Adds attempt with callbacks [9.x] Adds attempt with callbacks Apr 18, 2021
@DarkGhostHunter
Copy link
Contributor Author

Yeah. If someone is extending the class it will be terrible. 9.x it is.

Copy link
Contributor Author

@DarkGhostHunter DarkGhostHunter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems ok to me.

@DarkGhostHunter
Copy link
Contributor Author

Since it's for 9.x, why just change the contract? This would mean interoperability between all guards (included and custom ones).

@taylorotwell
Copy link
Member

It is in theory possible to solve this on 8.x by passing a reserved key in the $credentials array... something like __callbacks. An outside user can't submit a Closure through a form of course so we would simply spin the array of callbacks and invoke them if they are an instance of Closure. It would need to introduce a new method to make this easier like:

Auth::attemptSomeMethodName(array $arrayOfCallbacks, array $credentials, etc...)
{
    $credentials['__callbacks'] = $arrayOfCallbacks;
    return $this->attempt($credentials, etc...);
}

@DarkGhostHunter
Copy link
Contributor Author

DarkGhostHunter commented Apr 21, 2021

I really would prefer using callables because I can just reference an static method from somewhere.

I think that reserving a key it's kind of hassle because there is no guarantee the developer is not using it somewhere (I've seen some sh*t).

It could be that just adding a new method that accepts an array of callbacks could work for 8.x.

Auth::attemptAnd($credentials, fn($user) => $user->isCool());

This could work only for the Session Guard, but on 9.x, a contract could force adding the new method to allow interoperability, but I wouldn't mind if everyone voted against.

@taylorotwell
Copy link
Member

If you add a new method, I'm not sure you have a way to get those callbacks called at the correct time during the authentication process without a breaking change? Unless I'm missing something?

@DarkGhostHunter
Copy link
Contributor Author

If you add a new method, I'm not sure you have a way to get those callbacks called at the correct time during the authentication process without a breaking change? Unless I'm missing something?

I'm also missing something. Why it wouldn't be called at the correct time? The idea is to work on the validated user itself. Wrapping it up:

  1. The dev calls attempt() (or whatever name is) with the callbacks.
  2. The guard gets the user for these credentials.
  3. The guard checks if the credentials are valid.
  4. This is where the callbacks run for further checks, because it has now the Authenticatable instance to work with.

This is intended to work like an "AfterValidated" hook because, before validation, its pretty much like a black box.

@taylorotwell
Copy link
Member

Well - if you think you can add a new method in such a way that is not breaking feel free to adjust this PR to do that and I'll take a look.

@DarkGhostHunter
Copy link
Contributor Author

DarkGhostHunter commented Apr 22, 2021 via email

@DarkGhostHunter DarkGhostHunter changed the title [9.x] Adds attempt with callbacks [8.x] Adds attempt with callbacks Apr 22, 2021
@DarkGhostHunter
Copy link
Contributor Author

DarkGhostHunter commented Apr 22, 2021

I think it has more "semantic sense" to use attemptWith(). I used that and added the guard instance into the callback just for show.

You can now do this:

Auth::attemptWith($credentials, $remember, function ($user, $guard) {
  return $user->isCool() && $guard instanceof SessionGuard::class;
});

No BC, 8.x compatible.

@taylorotwell
Copy link
Member

Can you resubmit this to 8.x?

@DarkGhostHunter
Copy link
Contributor Author

Can you resubmit this to 8.x?

Lol, of course.

@DarkGhostHunter DarkGhostHunter deleted the feat/attempt-callback branch April 22, 2021 22:25
@DarkGhostHunter
Copy link
Contributor Author

#37090

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants