User doesn't stay logged in when I use setIdentity.
$user = $this->Users->get(1);
$this->Authentication->setIdentity($user);
$userInfo = $this->Authentication->getIdentity(); // Returns good.
$this->redirect('Somewhere');
Somewhere:
$userInfo = $this->Authentication->getIdentity(); // Returns null.
I am not using form. There is no POST happening only dynamically setting user based on some algo...
Application.php
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
$service = new AuthenticationService([
'unauthenticatedRedirect' => '/login',
'queryParam' => 'redirect',
]);
/* $fields = [
'username' => 'username',
'password' => 'password'
];
$service->loadIdentifier('Authentication.Password', compact('fields')); */
// Load the authenticators, you want session first
$service->loadAuthenticator('Authentication.Session');
return $service;
}
You are setting the principal information on the Authentication but you loose it on the next request because it's not persisted (I'm sparing you the "http is stateless" song...)
Part of your setIdentity should also be persisting the identity. This can be achieved in different ways:
in the session, when using sessions
in a JWT token, when using tokens
Here is how AuthenticationService does it in persistIdentity. I suggest you also have a look at the JWT auth configuration.
Related
In a laravel 5.8 API project, I want users to login via their social accounts. So far I have been able to use Socialite to retrieve user info from the provider and use it to create a new user record. But when I try to have the user log in again, it throws up the following error
Call to undefined method Laravel\Socialite\Two\User::createToken()
Here's the code I am working with
<?php
namespace App\Http\Controllers;
use App\User;
use Socialite;
use App\SocialAccount;
use App\Http\Resources\UserResource;
class SocialAuthController extends Controller
{
...
public function handleProviderCallback($provider)
{
$socialUser = Socialite::driver($provider)->stateless()->user();
$userSocialAccount = SocialAccount::where('provider_id', $socialUser->id)->where('provider_name', $provider)->first();
/*
if account exist, return the social account user
else create the user account, then return the new user
*/
if ($userSocialAccount) {
// generate access token for use
$token = $socialUser->createToken('********')->accessToken;
// return access token & user data
return response()->json([
'token' => $token,
'user' => (new UserResource($userSocialAccount))
]);
} else {
$user = User::create([
'firstname' => $socialUser->name,
'lastname' => $socialUser->name,
'username' => $socialUser->email,
'email_verified_at' => now()
]);
if ($user) {
SocialAccount::create([
'provider_id' => $socialUser->id,
'provider_name' => $provider,
'user_id' => $user->id
]);
}
// assign passport token to user
$token = $user->createToken('********')->accessToken;
return response()->json(['token' => $token, 'user' => new UserResource($user)]);
}
}
}
I haven't been able to spot the reason why I am getting the error when the user attempts a second login but there is no error if it's the first time the user logs in with a social account.
Why does it complain about Laravel\Socialite\Two\User::createToken() method? If I try adding this line use Laravel\Socialite\Two\User vscode intelephsense flags it as a duplicate of App\User so what is really going on in my code?
I think your last sentence hits the problem: the Laravel\Socialite\Two\User and App\User are two fully separate entities.
The Socialite::driver($provider)->stateless()->user() provides you with a Socialite User whereas User::create creates an App\User.
The second $token = $user->createToken('********')->accessToken; works because App\User has the createToken function and the other does not.
First of all the problem I was having with having a token generated by passport for users authentication after the first social login was because I was calling the createToken method on the user returned by Socialite. As explained by #JorisJ1 Socialite does not have the createToken function so my initial code threw an error.
Here's how I fixed it
public function handleProviderCallback($provider)
{
// retrieve social user info
$socialUser = Socialite::driver($provider)->stateless()->user();
// check if social user provider record is stored
$userSocialAccount = SocialAccount::where('provider_id', $socialUser->id)->where('provider_name', $provider)->first();
if ($userSocialAccount) {
// retrieve the user from users store
$user = User::find($userSocialAccount->user_id);
// assign access token to user
$token = $user->createToken('Pramopro')->accessToken;
// return access token & user data
return response()->json([
'token' => $token,
'user' => (new UserResource($user))
]);
} else {
...
}
}
Comments are welcomed if there is a better way for adding social authentication to API.
I am using laravel 5.6 and passport is installed and working. I wanted to login the user by sending the post request from ionic 3 application. I wanted to know how to make the login route.
I already made create account route which is working and after user creates account it returns the access_token and refresh_token. But how to implement this for the login route?
I found we can do by using following code by sending post request to the http://127.0.0.1:8000/oauth/token route but... is it safe to save client_id and client_secret in the ionic app itself?
What are the best practices to login the user using laravel passport by sending post request from ionic 3 app (or any other front end framework)?
{
"grant_type" : "password",
"client_id" : "2",
"client_secret" : "HqLqRCRzNN2dwLWM1JhvCoNrbndwNSTGz515hSrswT",
"username" : "test#test.com",
"password" : "123456"
}
2020's Update
There's now an alternative to Passport to authenticate SPAs and mobile apps (also maintained by the Laravel): Laravel Sanctum.
Note: This library requires Laravel v6.9+.
Original answer
Exposing client-credentials is always risky because you don't have full control of the client apps.
If you don't want to store that kind of data in the client-side you could make a little proxy to receive username/password and then complete the call adding the passport client details.
Citing this answer made by #adiachenko:
routes/api.php
Route::post('auth/token', 'Api\Auth\DefaultController#authenticate');
Route::post('auth/refresh', 'Api\Auth\DefaultController#refreshToken');
app/Http/Controllers/Api/Auth/DefaultController.php
<?php
namespace App\Http\Controllers\Api\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;
class DefaultController extends Controller
{
/**
* #var object
*/
private $client;
/**
* DefaultController constructor.
*/
public function __construct()
{
$this->client = DB::table('oauth_clients')->where('id', 2)->first();
}
/**
* #param Request $request
* #return mixed
*/
protected function authenticate(Request $request)
{
$request->request->add([
'username' => $request->username,
'password' => $request->password,
'grant_type' => 'password',
'client_id' => $this->client->id,
'client_secret' => $this->client->secret,
'scope' => '*'
]);
$proxy = Request::create(
'oauth/token',
'POST'
);
return Route::dispatch($proxy);
}
/**
* #param Request $request
* #return mixed
*/
protected function refreshToken(Request $request)
{
$request->request->add([
'grant_type' => 'refresh_token',
'refresh_token' => $request->refresh_token,
'client_id' => $this->client->id,
'client_secret' => $this->client->secret,
]);
$proxy = Request::create(
'/oauth/token',
'POST'
);
return Route::dispatch($proxy);
}
}
As you can see, you'll need to make a POST call to /auth/token sending user credentials:
{
"username" : "some#email.com",
"password" : "some-awesome-password"
}
then the authenticate method will complete the passport-client details to continue with the flow.
I know how the basic auth works for sign up/login on Laravel. However, I want to learn setting up how to do user sign up (for Password Grant). I set up Passport (2.0) - Passport Grant, and I can get token; however I couldn't find anything for user signup. Then I found a topic here explaining that I should call the oauth/token internally, but also couldn't figure out how to achieve it exactly.
So what I thought is creating a signup() method in a controller and handle user registration on my own, but then how would I pass necessary data to oauth/token route? Because what Laravel uses for $request is Request $request but what Passport uses is ServerRequestInterface $request and when I vardump the $request on oauth/token's issueToken method, it's totally different Request $request.
// Setup new method
public function signup(Request $request) {
// do validations
// create user
User::create([
'email' => $request->username,
'password' => bcrypt($request->password),
]);
$client = \Laravel\Passport\Client::where('password_client', 1)->first();
$oauthData = [
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret,
'username' => $request->email,
'password' => $request->password,
'scope' => null
]
// Here, I got confused how to pass this `oauthData` to `issueToken()` route
// and it takes it as `ServerRequestInterface $request`
}
Someone did it likes this here, but no idea how I should exactly implement this approach. Because my /oauth/token route's post method issueToken() is like this:
public function issueToken(ServerRequestInterface $request)
{
return $this->withErrorHandling(function () use ($request) {
return $this->server->respondToAccessTokenRequest($request, new Psr7Response);
});
}
I am very confused and couldn't figure out how to overcome this. What is the right way of handling such scenario where I need to signup users through api?
I have a question regarding Authentication in Laravel 5.x. I’ve been specifically looking at tymondesigns/jwt-auth and irazasyed/jwt-auth-guard packages to do the JSON web token authentication token handling in my Laravel application.
I am not using a local database whatsoever, nor do I want to. I have environment variables set up in .env for my API’s URL, USERNAME & PASSWORD. The Guzzle PHP HTTP client is doing the trick just fine, connecting and returning data between the API and my application as needed.
However, I need to set up Authentication within my Laravel instance. I run into problems here, and the auth is wanting a DB connection.
$token = JWTAuth::attempt($credentials)
Here's the exception:
PDOException in Connector.php line 55:
SQLSTATE[HY000] [14] unable to open database file
How can I make use of JWT without using a database?
How can I COMPLETELY shut-off database connections within Laravel?
Thanks.
UPDATE:
Using tymon/jwt-auth, I've set things up within the routes, Kernel, Middleware, etc.
I created a "claim" successfully, but I need to create the token by encoding the "payload."
$this->username = $request->username;
$sub = $this->username;
$iat = time();
$jti = md5($sub . $iat);
$aud = env('APP_URL');
$this->claims = [
'sub' => $sub,
'iat' => $iat,
'exp' => time() + (2 * 7 * 24 * 60 * 60),
'nbf' => $iat,
'iss' => 'khill',
'jti' => $jti,
'aud' => $aud,
];
$payload = JWTFactory::make($this->claims);
How do I get the custom token?
You should define a custom Authentication Provider and set it in config/jwt.php.
Example of provider
Put this class anywhere you like.
namespace MyNamespace;
use Tymon\JWTAuth\Providers\Auth\AuthInterface;
class MyCustomAuthenticationProvider implements AuthInterface
{
public function byCredentials(array $credentials = [])
{
return $credentials['username'] == env('USERNAME') && $credentials['password'] == env('PASSWORD');
}
public function byId($id)
{
// maybe throw an expection?
}
public function user()
{
// you will have to implement this maybe.
}
}
Example of configuration
In the providers array in config/jwt.php, change this:
'auth' => 'Tymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter',
to this:
'auth' => 'MyNamespace\MyCustomAuthenticationProvider',
Other considerations
Using the env() function anywhere is not good practice. It's better to use it in your config files, and then use the config() function anywhere else.
You may need to reimplement also the User Provider.
JWTAuth::attempt() won't help you with this, because it hits the database for you behind the scenes. You need some other way to check the environment credentials.
Add a custom method to a class somewhere which will do that for you or pass the credentials against the API you are hitting with Guzzle.
Code example:
public function authenticate($username, $password)
{
if(!$username === env('USERNAME') or !$password === env('PASSWORD')) {
// return a message that the user could not be authenticated or false.
}
// Generate the JWT token here and store it somewhere.
}
As a quick fix I decided to implement the following custom code...
1) Created custom middleware to handle the logic.
class CustomMiddleware
{
protected $loginPath = 'login';
public function handle($request, Closure $next) {
$logged_in = $request->session()->get('logged_in');
if (!$logged_in) {
return redirect()->guest('login')->with('flag','1');
}
return $next($request);
}
}
2) Added a reference to the middleware class.
class Kernel extends HttpKernel
{
protected $routeMiddleware = [
'custom' => \App\Http\Middleware\CustomMiddleware::class,
];
}
3) Added it to routes.php.
Route::group(['middleware' => ['custom']], function () {
// Add routes here
}
yes.
you can create jwt token without database using tymondesigns/jwt-auth package...
for that you have to use jwt::encode method...
let me explain ...
first you have to put your credential in .env file...
then i am recomending you to use custom claims ...
after that you can create jwt token using below code ...
$customClaims = ['foo' => 'bar', 'baz' => 'bob'];
$factory = JWTFactory::customClaims($customClaims);
$token = JWTAuth::encode($payload);
for further details you can refer below link
wiki
Today I noticed something disturbing while inspecting session files in storage/framework/sessions folder created by Laravel 5.
Here is what happened:
I logged in as user A
I navigated to a page which stores variable X in the Session
I logged out, but did not close the browser.
Session file in storage/framework/sessions was still there, and browser
cookie was alive.
I logged in as user B.
The old session file in storage/framework/sessions got deleted and a new session file was there.
I looked into the new session file - surprise! variable X has survived log-out and is still there, accessible for user B!
It leads to security concerns because now user B has access to the data of user A.
While debugging through Laravel source code, I found out that Session Store is never being cleared up during logout/login process. Only login credentials are being removed in Illuminate\Auth\Guard::clearUserDataFromStorage() method, but all the session Store attributes are still there, and then later when $kernel->terminate($request, $response); is called, which in turn leads to Illuminate\Session\Middleware\StartSession::terminate() calling Store::save(), which blindly saves $this->attributes to the new session, ignoring the fact that it now belongs to another user.
From one hand, it seems logical - Laravel has no assumptions about my data and whether I want it to expire together with authentication or not. But it would be great to have it documented somewhere with a solution to attach some sensitive data to authentication object and expire together with it.
This means that I as a programmer am responsible for completely clearing away all the sensitive data from current session when a new (or the same) user is logging in.
Clearing on logging out would not be reliable, because user might never click Logout link but wait for the session to "expire", which for Laravel still does not clear up the session.
One more thing to keep in mind: I should not clear the session too early - there is AntiForgery token which must be present, or else login form will always fail.
I have found a forum topic which also tries to solve somewhat similar problem:
http://laravel.io/forum/04-27-2014-how-to-expire-session-data
I got confused by this:
I had another go at it today and realised what the problem was: Session::flush() does not delete session data that the app creates, such as the shopping cart details
If this is true, then the only way to get completely rid of session would be to use PHP native session_unset() and session_destroy() but I wouldn't want to go that way - I would prefer to find a cleaner, Laravel-ish solution, if possible.
How do I tell Laravel that I want my old session data to be removed together with user authentication data when authentication expires or user logs out?
In the laravel docs it says you can:
Removing An Item From The Session
Session::forget('key');
Removing All Items From The Session
Session::flush();
You could navigate to the AuthenticatesAndRegistersUsers.php trait and rewrite
/**
* Log the user out of the application.
*
* #return \Illuminate\Http\Response
*/
public function getLogout()
{
$this->auth->logout();
return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/');
}
to
/**
* Log the user out of the application.
*
* #return \Illuminate\Http\Response
*/
public function getLogout()
{
Session::flush();
$this->auth->logout();
return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/');
}
I have no idea if this actually work, but give it a try :)
Update
According to this answer here on Stack Overflow, you can set the session to expire on browser close, or after XXX minutes. Used together with the above solution, it should fix the problem?
In config/session.php
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to immediately expire on the browser closing, set that option.
|
*/
'lifetime' => 120,
'expire_on_close' => false
I believe this is the correct answer to this question/problem:
When making multiple requests in one test, the state of your laravel application is not reset between the requests. The Auth manager is a singleton in the laravel container, and it keeps a local cache of the resolved auth guards. The resolved auth guards keep a local cache of the authed user.
So, your first request to your api/logout endpoint resolves the auth manager, which resolves the api guard, which stores a references to the authed user whose token you will be revoking.
Now, when you make your second request to /api/user, the already resolved auth manager is pulled from the container, the already resolved api guard is pulled from it's local cache, and the same already resolved user is pulled from the guard's local cache. This is why the second request passes authentication instead of failing it.
When testing auth related stuff with multiple requests in the same test, you need to reset the resolved instances between tests. Also, you can't just unset the resolved auth manager instance, because when it is resolved again, it won't have the extended passport driver defined.
So, the easiest way I've found is to use reflection to unset the protected guards property on the resolved auth manager. You also need to call the logout method on the resolved session guards.
Source: Method Illuminate\Auth\RequestGuard::logout does not exist Laravel Passport
To use that, add this to:
TestCase.php
protected function resetAuth(array $guards = null) : void
{
$guards = $guards ?: array_keys(config('auth.guards'));
foreach ($guards as $guard) {
$guard = $this->app['auth']->guard($guard);
if ($guard instanceof SessionGuard) {
$guard->logout();
}
}
$protectedProperty = new \ReflectionProperty($this->app['auth'], 'guards');
$protectedProperty->setAccessible(true);
$protectedProperty->setValue($this->app['auth'], []);
}
Then, use it like this:
LoginTest.php
class LoginTest extends TestCase
{
use DatabaseTransactions, ThrottlesLogins;
protected $auth_guard = 'web';
/** #test */
public function it_can_login()
{
$user = $this->user();
$this->postJson(route('login'), ['email' => $user->email, 'password' => TestCase::AUTH_PASSWORD])
->assertStatus(200)
->assertJsonStructure([
'user' => [
'id' ,
'status',
'name',
'email',
'email_verified_at',
'created_at',
'updated_at',
'photo_url',
'roles_list',
'roles',
],
]);
$this->assertEquals(Auth::check(), true);
$this->assertEquals(Auth::user()->email, $user->email);
$this->assertAuthenticated($this->auth_guard);
$this->assertAuthenticatedAs($user, $this->auth_guard);
$this->resetAuth();
}
/** #test */
public function it_can_logout()
{
$this->actingAs($this->user())
->postJson(route('logout'))
->assertStatus(204);
$this->assertGuest($this->auth_guard);
$this->resetAuth();
}
/** #test */
public function it_should_get_two_cookies_upon_login_without_remember_me()
{
$user = $this->user();
$response = $this->postJson(route('login'), [
'email' => $user->email,
'password' => TestCase::AUTH_PASSWORD,
]);
$response->assertCookieNotExpired(Str::slug(config('app.name'), '_').'_session');
$response->assertCookieNotExpired('XSRF-TOKEN');
$this->assertEquals(config('session.http_only'), true);
$this->resetAuth();
}
/** #test */
public function it_should_get_three_cookies_upon_login_with_remember_me()
{
$user = $this->user();
$response = $this->postJson(route('login'), [
'email' => $user->email,
'password' => TestCase::AUTH_PASSWORD,
'remember' => true,
]);
$response->assertCookieNotExpired(Str::slug(config('app.name'), '_').'_session');
$response->assertCookieNotExpired('XSRF-TOKEN');
$response->assertCookieNotExpired(Auth::getRecallerName());
$this->resetAuth();
}
/** #test */
public function it_should_throw_error_422_on_login_attempt_without_email()
{
$this->postJson(route('login'), ['email' => '', 'password' => TestCase::AUTH_PASSWORD])
->assertStatus(422)
->assertJsonStructure(['message', 'errors' => ['email']]);
$this->assertGuest($this->auth_guard);
$this->resetAuth();
}
/** #test */
public function it_should_throw_error_422_on_login_attempt_without_password()
{
$this->postJson(route('login'), ['email' => $this->adminUser()->email, 'password' => ''])
->assertStatus(422)
->assertJsonStructure(['message', 'errors' => ['password']]);
$this->assertGuest($this->auth_guard);
$this->resetAuth();
}
/** #test */
public function it_should_throw_error_422_on_login_attempt_with_empty_form()
{
$this->postJson(route('login'), ['email' => '', 'password' => ''])
->assertStatus(422)
->assertJsonStructure(['message', 'errors' => ['email', 'password']]);
$this->assertGuest($this->auth_guard);
$this->resetAuth();
}
/** #test */
public function it_should_throw_error_401_as_guest_on_protected_routes()
{
$this->assertGuest($this->auth_guard);
$this->getJson(route('me'))
->assertStatus(401)
->assertJson(['message' => 'Unauthenticated.']);
}
/** #test */
public function it_should_throw_error_429_when_login_attempt_is_throttled()
{
$this->resetAuth();
$throttledUser = factory(User::class, 1)->create()->first();
foreach (range(0, 9) as $attempt) {
$this->postJson(route('login'), ['email' => $throttledUser->email, 'password' => "{TestCase::AUTH_PASSWORD}_{$attempt}"]);
}
$this->postJson(route('login'), ['email' => $throttledUser->email, 'password' => TestCase::AUTH_PASSWORD . 'k'])
->assertStatus(429)
->assertJson(['message' => 'Too Many Attempts.']);
$this->resetAuth();
}
}
A note about the throttle one. It took me several days to figure out how to ensure that 429 behaviour. Earlier unit tests will increase the number of 'attempts' leading up to the throttling, so you need to resetAuth before the throttle test or the throttle will be triggered at the wrong time and screw the test.
Given the above unit test code, I am using this:
Route::group(['middleware' => ['guest', 'throttle:10,5']], function () { /**/ });
You can observe it working by changing any of those numbers, like 10,5 to 9,5 or 11,5 and watch how it affects the throttle unit test. You can also uncomment the resetAuth method and watch how it also screws the test.
For unit testing anything related to auth, the resetAuth utility method is extremely useful, must-have. Also the knowledge of auth caching in AuthManager is a must-know to make sense of observed behaviour.