I seem to be running into an issue with my login test with conditions. My login handler checks that the email and password are valid as well as the user being confirmed. The following test cases pass:
public function test_redirect_to_login_if_login_fails()
{
// enable filters to test login
Route::enableFilters();
// some fake credentials to use
$credentials = [
'email'=>'user#domain.com',
'password'=>'badpassword',
'confirmed'=>1
];
// mock the Auth class
Auth::shouldReceive('attempt')
->once()
->with($credentials)
->andReturn(false);
// call the login post with the credentials
$this->call('POST','login',$credentials);
// assert that we are redirected back to the entry page
$this->assertRedirectedToRoute('entry');
}
public function test_redirect_if_login_success()
{
// enable filtere to test login
Route::enableFilters();
// some fake credentials to use
$credentials = [
'email'=>'user#domain.com',
'password'=>'Pa55w0rd',
'confirmed'=>1
];
// mock the Auth class
Auth::shouldReceive('attempt')
->once()
->with($credentials)
->andReturn(true);
// call the login post with the proper credentials
$response = $this->call('POST','login',$credentials);
// assert the response is good
$this->assertTrue($response->isRedirection());
}
This one however, gives me an error - though it's very similar to the first test case above:
public function test_redirect_if_user_is_not_confirmed()
{
// enable filters to test login
Route::enableFilters();
// some fake credentials to use
$credentials = [
'email'=>'user#domain.com',
'password'=>'badpassword',
'confirmed'=>0
];
// mock the Auth class
Auth::shouldReceive('attempt')
->once()
->with($credentials)
->andReturn(false);
// call the login post with the credentials
$this->call('POST','login',$credentials);
// assert that we are redirected back to the entry page
$this->assertRedirectedToRoute('entry');
}
The error:
Mockery\Exception\NoMatchingExpecationException: No matching handler
found for
Mockery_0_Illumnate_Auth_AuthManager::attempt(array('email'=>'user#domain.com','password'=>'badpassword','confirmed'=>1,)).
Either the method was unexpected or its arguments matched no expected
argument list for this method.
My controller method:
public function store()
{
if (Auth::attempt(array("email"=>Input::get('email'),"password"=>Input::get('password'),'confirmed'=>1)))
{
return Redirect::intended('home');
}
return Redirect::route("entry")->with("danger",Lang::get('orientation.invalid'))->withInput();
}
Your test is failing correctly - because your supplying the wrong data to the test.
You have the mock of Auth to receive:
Auth::shouldReceive('attempt')
->once()
->with($credentials)
->andReturn(false);
Specifically that it should receive ->with($credentials). But you have defined $credentials as having confirmed => 0 when your controller is always going to be sending confirmed => 1 because that is what is hard coded.
Instead - you should still be expecting to receive confirmed => 1 in your test - but mocking a false return because no matching record is found.
This should work:
// some fake credentials to use
$credentials = [
'email'=>'user#domain.com',
'password'=>'badpassword',
'confirmed'=>1
];
Related
Any idea on how i can test my authentication routes in authorization code grant:
- GET: '/oauth/authorize/?' . $query
- POST: 'oauth/token'
The problem is that according to the docs you need to provide a redirect_uri field in your query and i don't know how you suppose to have one in tests and then get the response from your laravel app.
i don't want to test this api with my frontend app.(if possible)
i haven't showed any code bc i just need a general idea of the testing process of such APIs that are working with clients and redirect_uris
on google i found tests around password grant authentication which doesn't need a redirect_uri field
this is what i tryed and it failed.
test:
$user = User::orderBy('id', 'asc')->first();
$token = $user->createToken('personal_access');
Passport::actingAs($user, [], 'api');
(new AuthController)->logout();
if (($user = Auth::user()->toArray()) !== null) {
dd(1, $user);
} else {
dd(0);
}
Auth::user() returns the $user
AuthController:
public function logout(): Response
{
$tokenId = $this->getTokenId();
$tokenRepository = app(TokenRepository::class);
$tokenRepository->revokeAccessToken($tokenId);
$refreshTokenRepository = app(RefreshTokenRepository::class);
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);
Artisan::call('passport:purge');
return response('Successfully loged you out.', 200);
}
private function getTokenId(): int
{
return (new CheckAuthentication)->getAuthenticated()->token()->id;
}
$tokenId is always zero.
So, I have created a test for my app, the model is Subscription which is the index and show endpoint should be publicly accessible.
I created a Resource Controller to handle I/O from the client and a Policy to handle the authorization but here, I found something that seems kinda odd.
Inside the controller, I registered the policy on the constructor, like so:
public function __construct()
{
$this->middleware('auth:api')->except(['index', 'show']);
$this->authorizeResource(Subscription::class, 'subscription');
}
And then in the policy class, I modified the default generated methods like so:
/**
* Determine whether the user can view any models.
*
* #param \App\Models\User $user
* #return mixed
*/
public function viewAny(?User $user) // <-- notice here I make it optional, the original was required (without "?" mark).
{
return true; // publicly visible
}
When I run the test, it passed.
public function testSubscriptionIndexArePubliclyAccessible()
{
$subscriptions = Subscription::factory(10)->create()->toArray();
$response = $this->get(route('subscriptions.index'));
$response->assertOk();
$response->assertExactJson($subscriptions);
}
However, if I completely remove the User $user param from the method, the test would fail.
public function viewAny() <-- if I do this, the test fail. Saying that "this action is unauthorized".
{
return true; // publicly visible
}
So.. Why is this happen?
There are checks happening before a policy method or gate ability are called. One check is if the policy method can be called with a user, canBeCalledWithUser. This will check if there is an auth user and return true, if not it does other checks. The next check is if the method allows guest users, methodAllowsGuests, which will use reflection to get the parameter for that method and see if it has a type and is nullable, but there are no parameters so it returns false. So you end up with it not calling the method and treating it more like it doesn't exist, which is always false in terms of authorization checks.
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/Access/Gate.php#L371 #raw -> callAuthCallback -> resolveAuthCallback
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/Access/Gate.php#L530 #resolveAuthCallback
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/Access/Gate.php#L390 #canBeCalledWithUser
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/Access/Gate.php#L416 #methodAllowsGuests
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/Access/Gate.php#L456 #parameterAllowsGuests - it does not make it to this method call
I have built my own UserProvider that is authenticating against another database. Everything is working great; I'm able to log in/out etc. I'm down to returning error messages and running into a snag.
Here is what an example of my code looks like:
MyUserProvider.php
...
$auth = json_decode($response, true); // response from Guzzle to 3rd party auth
if ($auth) {
if ($auth['errors']) {
return redirect('login')
->withErrors(['auth' => 'Invalid username or password']);
} else {
$myUser = $auth['data']['user']; // auth object from 3rd party.
$user = User::where('id', $myUser['id'])->first(); // find user in local db.
...
}
}
}
The part I am struggling with is how to handle the redirect. The error I am getting is:
validateCredentials() must be an instance of Illuminate\Contracts\Auth\Authenticatable, instance of Illuminate\Http\RedirectResponse given
Which make sense, I'm returning a redirect, not an authenticatable object (a user). But, I don't have a user -- my username or password was wrong.
How can I redirect the user back and display a flash message telling them what happened?
Thank you for any suggestions!
I would like to add HTTP basic authentication to my Yii2 application in which the username/password is stored in the Yii configuration file - for exactly one user, no DB authentication.
I have enabled authentication for my controller by adding behaviors function:
public function behaviors()
{
return [
'basicAuth' => [
'class' => \yii\filters\auth\HttpBasicAuth::className(),
'auth' => function ($username, $password) {
if ($username=='api') {
return new SimpleApiUser();
} else {
return null;
}
},
],
];
}
And I was required to create class, that implements IdentityInterface, that is why I have class:
class SimpleApiUser implements IdentityInterface {
public static function findIdentity($id)
{
return null;
}
public static function findIdentityByAccessToken($token, $type = null)
{
return null;
}
public function getId()
{
return 1;
}
public function getAuthKey()
{
return 1;
}
public function validateAuthKey($authKey)
{
return true;
}
}
That is fine, the application asks for username/password in the case of the first request, but then it managed to store the authentication somehow in some internal session and it does not required repeated authentication for each new request be it made from the browser (which my add sessions) or be it from Postman (which certainly does not keep sessions). Is it possibly to modify user class to require to provide username and password with each new request?
I've tried to play around a bit with your code.
First, your code is enough to require the basic auth to be part of every request. If the Authorization header is not present in request the yii will return 401 Unauthorized. So your implementation is already doing what you need.
There are reasons why user is not required to enter username/password each time.
For web browsers:
The credentials are saved for session and the browser will send them automatically in each subsequent request without prompting them again from user.
For postman: The authorization is stored for request as long as you don't manually remove the authorization settings it will be sent as part of each request.
If you want to force users to manually enter username/password with each request you can extend the HttpBasicAuth component to pretend that the authorization was not part of request for every other request like this
use Yii;
use yii\filters\auth\HttpBasicAuth;
class MyHttpBasicAuth extends HttpBasicAuth
{
const SESSION_KEY = 'authForced';
private $session;
public function __construct($config = [])
{
parent::__construct($config);
$this->session = Yii::$app->session;
}
public function authenticate($user, $request, $response)
{
if ($this->session->has(self::SESSION_KEY)) {
$this->session->remove(self::SESSION_KEY);
return parent::authenticate($user, $request, $response);
}
$this->session->set(self::SESSION_KEY, true);
return null;
}
}
But this will require sessions to work so it can only be used with browsers. It won't work well for API. For postman this implementation would make every other request fail with 401 Unauthorized. It would fail same way for api that will work with cookies and it would fail each request for api that wouldn't work with cookies.
Side note: The postman does keep/send cookies so sessions works with postman.
I'm using Socialite in a Laravel application to allow users to connect via Github.
My login controller contains the following two methods:
/**
* GET /login/github
* Redirect the user to the GitHub authentication page.
*/
public function redirectToProvider()
{
return Socialite::driver('github')->redirect();
}
/**
* GET /login/github/callback
* Obtain the user information from GitHub.
*/
public function handleProviderCallback(Request $request)
{
$githubUser = Socialite::driver('github')->user();
// Actual login procedures go here; redacted for brevity
return redirect('/');
}
When I manually test these methods in the browser, they work as expected. I visit /login/github where I'm redirected to Github to authenticate, then I'm sent back to /login/github/callback?state=somelongrandomkey which then redirects me home (/).
I'm also attempting to test these methods via Laravel Dusk, mocking Socialite.
My Dusk test method looks like this:
public function testReceivesGithubRequestAndCreatesNewUser()
{
$this->browse(function (Browser $browser) {
$user = factory('App\Models\User')->create([
'github_token' => 'foobar',
'github_username' => 'foobar'
]);
# Mock 1 - A Socialite user
$abstractUser = Mockery::mock('Laravel\Socialite\Two\User');
# Mock 2 - Socialite's Github provider
$provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
$provider->shouldReceive('user')
->andReturn($abstractUser);
# Mock 3 - Socialite
Socialite::shouldReceive('driver')
->with('github')
->andReturn($provider);
$browser->visit('/login/github/callback')->assertPathIs('/');
});
When I run this test, the visit to /login/github/callback fails with an InvalidStateException.
From the log:
dusk.local.ERROR: {"exception":"[object] (Laravel\\Socialite\\Two\\InvalidStateException(code: 0): at /redacted/vendor/laravel/socialite/src/Two/AbstractProvider.php:210)
[stacktrace]
#0 /redacted/app/Http/Controllers/Auth/LoginController.php(84): Laravel\\Socialite\\Two\\AbstractProvider->user()
[...etc...]
When I trace where the error is coming from in AbstractProvider
I see it's attempting to compare state from the session with state from the query string:
protected function hasInvalidState()
{
if ($this->isStateless()) {
return false;
}
$state = $this->request->session()->pull('state');
return ! (strlen($state) > 0 && $this->request->input('state') === $state);
}
In my Dusk test, when /login/github/callback is visited, there is no state on the query string, so it's logical that it's failing.
I feel I'm missing some key component in setting up the mocks that provides that state, but I'm not sure what.
My test is built using these two examples for reference:
How to Test Laravel Socialite
How I write integration tests for Laravel Socialite powered apps
There's a fundamental difference between Dusk tests and the examples you're mentioning: Dusk opens the website in an actual browser and so the test and your application run in separate processes. That's why mocking doesn't work in Dusk tests.
The idea behind such an integration test is that you simulate a real user, without mocking or any other "shortcuts". In your case, that would mean logging in with a real GitHub account.