I am a bit confused on the laravel scopes part.
I have a user model and table.
How can I assign a user the role of user, customer and/or admin.
I have a SPA with vue and laravel api backend. I use https://laravel.com/docs/5.3/passport#consuming-your-api-with-javascript
Passport::tokensCan([
'user' => 'User',
'customer' => 'Customer',
'admin' => 'Admin',
]);
How can i assign which user model has which scope(s)?
Or are scopes not the same as roles?
How would you implement this?
Thanks in advance!
Or are scopes not the same as roles?
The biggest difference between the two is the context they apply to. Role-based Access Control (RBAC) governs the access control of a user when using the web application directly, while Oauth-2 scope governs the access to the API resources for an external client on behalf of a user.
How can i assign which user model has which scope(s)?
In general Oauth flow, a user (as a resource owner) is requested to authorize a client on things that it can and cannot do on his/her behalf, these are what you called scope. On successful authorization the scope being requested by the client will be assigned to the generated token not to the user per se.
Depending on which Oauth grant flow that you choose, the client should include the scope on its request. In Authorization code grant flow the scope should be included on HTTP GET query parameter when redirecting the user to authorization page, while on Password grant flow the scope must be included in HTTP POST body parameter to request a token.
How would you implement this?
This is an example with Password grant flow, with assumption that you completed the laravel/passport setup beforehand
Define scopes for both admin and user role. Be specific as you can, for example: admin can manage-order and user only read it.
// in AuthServiceProvider boot
Passport::tokensCan([
'manage-order' => 'Manage order scope'
'read-only-order' => 'Read only order scope'
]);
Prepare the REST controller
// in controller
namespace App\Http\Controllers;
class OrderController extends Controller
{
public function index(Request $request)
{
// allow listing all order only for token with manage order scope
}
public function store(Request $request)
{
// allow storing a newly created order in storage for token with manage order scope
}
public function show($id)
{
// allow displaying the order for token with both manage and read only scope
}
}
Assign the route with api guard and scope
// in api.php
Route::get('/api/orders', 'OrderController#index')
->middleware(['auth:api', 'scopes:manage-order']);
Route::post('/api/orders', 'OrderController#store')
->middleware(['auth:api', 'scopes:manage-order']);
Route::get('/api/orders/{id}', 'OrderController#show')
->middleware(['auth:api', 'scopes:manage-order, read-only-order']);
And when issuing a token check the user role first and grant the scope based on that role. To achieve this, we need an extra controller that use AuthenticatesUsers trait to provide login endpoint.
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
class ApiLoginController extends Controller
{
use AuthenticatesUsers;
protected function authenticated(Request $request, $user)
{
// implement your user role retrieval logic, for example retrieve from `roles` database table
$role = $user->checkRole();
// grant scopes based on the role that we get previously
if ($role == 'admin') {
$request->request->add([
'scope' => 'manage-order' // grant manage order scope for user with admin role
]);
} else {
$request->request->add([
'scope' => 'read-only-order' // read-only order scope for other user role
]);
}
// forward the request to the oauth token request endpoint
$tokenRequest = Request::create(
'/oauth/token',
'post'
);
return Route::dispatch($tokenRequest);
}
}
Add route for api login endpoint
//in api.php
Route::group('namespace' => 'Auth', function () {
Route::post('login', 'ApiLoginController#login');
});
Instead of doing POST to /oauth/token route, POST to the api login endpoint that we provided before
// from client application
$http = new GuzzleHttp\Client;
$response = $http->post('http://your-app.com/api/login', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'username' => 'user#email.com',
'password' => 'my-password',
],
]);
return json_decode((string) $response->getBody(), true);
Upon successful authorization, an access_token and a refresh_token based on scope that we define before will be issued for the client application. Keep that somewhere and include the token to the HTTP header whenever making a request to the API.
// from client application
$response = $client->request('GET', '/api/my/index', [
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer '.$accessToken,
],
]);
The API now should return
{"error":"unauthenticated"}
whenever a token with under privilege is used to consumed restricted endpoint.
Implement the Raymond Lagonda response and it works very well, just to be careful with the following.
You need to override some methods from AuthenticatesUsers traits in ApiLoginController:
/**
* Send the response after the user was authenticated.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
protected function sendLoginResponse(Request $request)
{
// $request->session()->regenerate(); // coment this becose api routes with passport failed here.
$this->clearLoginAttempts($request);
return $this->authenticated($request, $this->guard()->user())
?: response()->json(["status"=>"error", "message"=>"Some error for failes authenticated method"]);
}
/**
* Get the failed login response instance.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\RedirectResponse
*/
protected function sendFailedLoginResponse(Request $request)
{
return response()->json([
"status"=>"error",
"message"=>"Autentication Error",
"data"=>[
"errors"=>[
$this->username() => Lang::get('auth.failed'),
]
]
]);
}
If you changed the login: username field to a custom username field eg: e_mail. You must refine the username method as in your LoginController.
Also you have to redefine and edit the methods: validateLogin, attemptLogin, credentials since once the login is validated, the request is forwarded to passport and must be called username.
I know this is a little late, but if you're consuming a backend API in an SPA using the CreateFreshApiToken in web middleware, then you can simply add an 'admin' middleware to your app:
php artisan make:middleware Admin
Then in \App\Http\Middleware\Admin do the following:
public function handle($request, Closure $next)
{
if (Auth::user()->role() !== 'admin') {
return response(json_encode(['error' => 'Unauthorised']), 401)
->header('Content-Type', 'text/json');
}
return $next($request);
}
Make sure you have added the role method to \App\User to retrieve the users role.
Now all you need to do is register your middleware in app\Http\Kernel.php $routeMiddleware, like so:
protected $routeMiddleware = [
// Other Middleware
'admin' => \App\Http\Middleware\Admin::class,
];
And add that to your route in routes/api.php
Route::middleware(['auth:api','admin'])->get('/customers','Api\CustomersController#index');
Now if you try to access the api without permission you will receive a "401 Unauthorized" error, which you can check for and handle in your app.
I've managed to get this into working, with #RaymondLagonda solution, for Laravel 5.5 with Sentinel, but it should, also work, without Sentinel.
The solution needs some class methods overriding (so please keep that in mind, for future updates), and adds some protection to your api routes (not exposing client_secret for example).
First step, is to modify your ApiLoginController in order to add construct function:
public function __construct(Request $request){
$oauth_client_id = env('PASSPORT_CLIENT_ID');
$oauth_client = OauthClients::findOrFail($oauth_client_id);
$request->request->add([
'email' => $request->username,
'client_id' => $oauth_client_id,
'client_secret' => $oauth_client->secret]);
}
In this example, you need to define var ('PASSPORT_CLIENT_ID') in your .env and create OauthClients Model, but you can safely skip this by putting your proper test values here.
One thing to notice, is that we are setting $request->email value to username, just to stick to Oauth2 convention.
Second step is, to override, sendLoginResponse method which is causing errors like Session storage not set, we don't need sessions here, cause it is api.
protected function sendLoginResponse(Request $request)
{
// $request->session()->regenerate();
$this->clearLoginAttempts($request);
return $this->authenticated($request, $this->guard()->user())
?: redirect()->intended($this->redirectPath());
}
Third step is, to modify your authenticated methods as suggested by #RaymondLagonda. You need to write your own logic here, and especially configure your scopes.
And final step (in case you are using Sentinel) is to modify AuthServiceProvider. Add
$this->app->rebinding('request', function ($app, $request) {
$request->setUserResolver(function () use ($app) {
return \Auth::user();
// return $app['sentinel']->getUser();
});
});
just after $this->registerPolicies(); in boot method.
After these steps you should be able, to get your api working, by providing username ('this will always be email, in this implementation'), password and grant_type='password'
At this point, you can add to middlewares scopes scopes:... or scope:... to protect your routes.
I hope, it is going to really help...
With #RaymondLagonda solution. If you are getting a class scopes not found error, add the following middleware to the $routeMiddleware property of your app/Http/Kernel.php file:
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,`
Also, if you are getting the error Type error: Too few arguments to function, you should be able to get the $user from the request like below.
(I am using laratrust for managing roles)
public function login(Request $request)
{
$email = $request->input('username');
$user = User::where('email','=',$email)->first();
if($user && $user->hasRole('admin')){
$request->request->add([
'scope' => 'manage-everything'
]);
}else{
return response()->json(['message' => 'Unauthorized'],403);
}
$tokenRequest = Request::create(
'/oauth/token',
'post'
);
return Route::dispatch($tokenRequest);
}
Thanks for this, this question was riddling my mind for a while! I took Raymond Lagonda's solution customised it a little for Laravel 5.6, using the built-in rate limiting, using a single thirdparty client (or be more custom if needed), while still giving each user a list of permissions (scopes).
Uses Laravel Passport password grant and follows Oauth flow
Gives you ability to set roles (scopes) for different users
don't expose/release client ID or client secret, only the user's username (email) and password, pretty much a password grant, minus the client/grant stuff
Examples at bottom
routes/api.php
Route::group(['namespace' => 'ThirdParty', 'prefix' => 'thirdparty'], function () {
Route::post('login', 'ApiLoginController#login');
});
ThirdParty/ApiLoginController.php
<?php
namespace App\Http\Controllers\ThirdParty;
use Hash;
use App\User;
use App\ThirdParty;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class ApiLoginController extends Controller
{
use AuthenticatesUsers;
/**
* Thirdparty login method to handle different
* clients logging in for different reasons,
* we assign each third party user scopes
* to assign to their token, so they
* can perform different API tasks
* with the same token.
*
* #param Request $request
* #return Illuminate\Http\Response
*/
protected function login(Request $request)
{
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
$user = $this->validateUserLogin($request);
$client = ThirdParty::where(['id' => config('thirdparties.client_id')])->first();
$request->request->add([
'scope' => $user->scopes,
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret
]);
return Route::dispatch(
Request::create('/oauth/token', 'post')
);
}
/**
* Validate the users login, checking
* their username/password
*
* #param Request $request
* #return User
*/
public function validateUserLogin($request)
{
$this->incrementLoginAttempts($request);
$username = $request->username;
$password = $request->password;
$user = User::where(['email' => $username])->first();
abort_unless($user, 401, 'Incorrect email/password.');
$user->setVisible(['password']);
abort_unless(Hash::check($password, $user->password), 401, 'Incorrect email/password.');
return $user;
}
}
config/thirdparties.php
<?php
return [
'client_id' => env('THIRDPARTY_CLIENT_ID', null),
];
ThirdParty.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ThirdParty extends Model
{
protected $table = 'oauth_clients';
}
.env
## THIRDPARTIES
THIRDPARTY_CLIENT_ID=3
php artisan make:migration add_scope_to_users_table --table=users
// up
Schema::table('users', function (Blueprint $table) {
$table->text('scopes')->nullable()->after('api_access');
});
// down
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('scopes');
});
(note: api_access is a flag which decides whether a user can login to the website/frontend portion of the app, to view dashboards/records etc.),
routes/api.php
Route::group(['middleware' => ['auth.client:YOUR_SCOPE_HERE', 'throttle:60,1']], function () {
...routes...
});
MySQL - Users scopes
INSERT INTO `users` (`id`, `created_at`, `updated_at`, `name`, `email`, `password`, `remember_token`, `api_access`, `scopes`)
VALUES
(5, '2019-03-19 19:27:08', '2019-03-19 19:27:08', '', 'hello#email.tld', 'YOUR_HASHED_PASSWORD', NULL, 1, 'YOUR_SCOPE_HERE ANOTHER_SCOPE_HERE');
MySQL - ThirdParty Oauth Client
INSERT INTO `oauth_clients` (`id`, `user_id`, `name`, `secret`, `redirect`, `personal_access_client`, `password_client`, `revoked`, `created_at`, `updated_at`)
VALUES
(3, NULL, 'Thirdparty Password Grant Client', 'YOUR_SECRET', 'http://localhost', 0, 1, 0, '2019-03-19 19:12:37', '2019-03-19 19:12:37');
cURL - Logging in/requesting a token
curl -X POST \
http://site.localhost/api/v1/thirdparty/login \
-H 'Accept: application/json' \
-H 'Accept-Charset: application/json' \
-F username=hello#email.tld \
-F password=YOUR_UNHASHED_PASSWORD
{
"token_type": "Bearer",
"expires_in": 604800,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciO...",
"refresh_token": "def502008a75cd2cdd0dad086..."
}
Use longlived access_token/refresh_token as normal!
Accessing forbidden scope
{
"data": {
"errors": "Invalid scope(s) provided."
},
"meta": {
"code": 403,
"status": "FORBIDDEN"
}
}
Related
I am trying to create a custom verification flow, where as soon as a user clicks the verification link, it logs him in and also verifies him, instead of first making him log in and only then the verification link works.
I built a custom notification URL in my CustomVerificationNotification, including the registered user_id, to login him later:
protected function verificationUrl($notifiable)
{
if (static::$createUrlCallback) {
return call_user_func(static::$createUrlCallback, $notifiable);
}
return URL::temporarySignedRoute(
'verification.custom-verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
'user_id' => $this->user->id
]
);
}
Then in my web.php I added this route:
Route::get('/email/verify/{id}/{hash}/{user_id}','Auth\CustomVerifyController#login_and_verify')->name('verification.custom-verify');
Then in my CustomVerifyController:
public function login_and_verify(EmailVerificationRequest $request)
{
//..
}
But I get Call to a member function getKey() on null. And I can't edit EmailVerificationRequest, so what can I do? Is it possible to somehow call Auth::login($user); before calling the EmailVerificationRequest? (Because I have the user_id from the route)
I tried to follow the best answer from this post as well: How to Verify Email Without Asking the User to Login to Laravel
But I'm not sure then how to trigger the verify() method from the web.php and send the $request when I'm first calling the verify_and_login method
First you need verify that the URL is signed by adding the middleware signed
You don't want that anoyone having the url /email/verify/{id}/{hash}/{user_id} able to access this ressource without the signature.
web.php
Route::get('/email/verify/{id}/{hash}/{user_id}','Auth\CustomVerifyController#login_and_verify')
->middleware('signed')
->name('verification.custom-verify');
Then you need to verify that the hash correspond the user_id and for that you can use a Request or a Middleware. I think the Request fits better since Laravel already uses a Request for this.
CustomEmailVerificationRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Http\FormRequest;
class EmailVerificationRequest extends FormRequest
{
public function authorize()
{
$user = User::findOrFail($this->route('id'));
if (! hash_equals((string) $this->route('hash'), sha1($user->getEmailForVerification()))) {
return false;
}
return true;
}
}
Finally you need to login with the user and set is email as verified
CustomVerifyController.php
public function login_and_verify(CustomEmailVerificationRequest $request)
{
$user = User::findOrFail($this->route('id'));
Auth::login($user);
$user->markEmailAsVerified();
event(new Verified($user));
...
}
[Edit to add addition feature from comments]
In order to have a middleware that verify the signed URL and resend automatically the verification email, you need to build a custom middleware.
ValidateSignatureAndResendEmailVerification.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
use URL;
class ValidateSignatureAndResendEmailVerification
{
public function handle($request, Closure $next, $relative = null)
{
if(! URL::hasCorrectSignature($request, $relative !== 'relative')( {
throw new InvalidSignatureException;
}
if (URL::signatureHasNotExpired()) {
return $next($request);
}
return redirect()->route('resend-email-confirmation');
}
}
Then you need to add the middleware to Kernel.php
Kernel.php
protected $routeMiddleware = [
...
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'signed.email' => \App\Http\Middleware\ValidateSignatureAndResendEmailVerification::class,
...
];
Then, don't forget to update your route with the new middleware
web.php
Route::get('/email/verify/{id}/{hash}/{user_id}','Auth\CustomVerifyController#login_and_verify')
->middleware('signed.email')
->name('verification.custom-verify');
I need to solve the following problem: I created two applications using Laravel.
Application A can be reached via https://test.example.org and provides a passport-instance and all user-data (Model: User).
Application B can be reached via https://test.org and uses the API Application A provides using an "api-user" that exists in Application A to fetch "public" data
Now I would like to provide a login method for users of Application B. All users that are using Application B are existing in Application A. So a user logging in on Application B is using the credentials that are saved in Application A. All changes that are made to the user in Application A should be visible on Application B.
Now I wonder how I could do that. Any idea how to solve this? I can imagine using API-calls with
$client = $this->client;
try {
return $client->request('POST',
'https://'.config('server').'/oauth/token', [
'headers' => [
'cache-control' => 'no-cache',
'Content-Type' => 'application/x-www-form-urlencoded',
],
'form_params' => [
'client_id' => ...,
'client_secret' => ...,
'grant_type' => 'password',
'username' => $users-username,
'password' => $users-password,
],
]);
} catch (BadResponseException $e) {
return $e->getResponse();
}
}
but I don't get, whether I need to create a client_id and client_secret for each user on Application A. If this is the case, I'd need to store these information somewhere so I'd end up creating a User model on Application B itself.
My questions are:
How can I log in users that are existing on Application A to Application B the most elegant way?
Is it an option to use Laravel/Socialite from the client side (Application B)?
The most elegant way would be to use Laravel Socialite in your Application B to act as an OAuth client for Application A which acts as an OAuth server (using Laravel Passport).
On Application A, you need to create a client on Passport for your Application B:
php artisan passport:client --password
First, you will be asked Which user ID should the client be assigned to?. Press enter as you don't want to restrict the client to a specific user.
Then, you will be asked to provide a name for your client, your should enter something like Application B.
At last, you will be asked for the redirect URL of you client. You should enter https://test.org/oauth/callback, which is the URL that Socialite will use to redirect the users back after they are authenticated.
If you need to test this on your machine, enter your local domain, e.g. https://application-b.test/oauth/callback.
Once the client is created, keep the Client ID and Client secret as you will need them later.
You will also need an API endpoint to provide the details for the authenticated user, .e.g. https://test.example.org/user.
On Application B, after installing Laravel Socialite, you need to set the credentials for the Application A in your services.php configuration file. For example:
'passport' => [
'client_id' => env('PASSPORT_CLIENT_ID'),
'client_secret' => env('PASSPORT_CLIENT_SECRET'),
'redirect' => 'oauth/callback',
],
In your .env file, set the client ID and secret for the Passport client you created in Application A:
PASSPORT_CLIENT_ID=...
PASSPORT_CLIENT_SECRET=...
Now you need to extend Socialite with a custom provider for Application A.
Create a PassportProvider class in the app/Socialite directory (or anywhere inside app if you prefer).
<?php
namespace App\Socialite;
use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\ProviderInterface;
use Laravel\Socialite\Two\User;
use Illuminate\Support\Arr;
class PassportProvider extends AbstractProvider implements ProviderInterface
{
/**
* {#inheritdoc}
*/
protected function getAuthUrl($state)
{
return $this->buildAuthUrlFromBase('https://test.example.org/oauth/authorize', $state);
}
/**
* {#inheritdoc}
*/
protected function getTokenUrl()
{
return 'https://test.example.org/oauth/token';
}
/**
* {#inheritdoc}
*/
protected function getUserByToken($token)
{
$response = $this->getHttpClient()->get(
'https://test.example.org/user',
$this->getRequestOptions($token)
);
return json_decode($response->getBody(), true);
}
/**
* {#inheritdoc}
*/
protected function mapUserToObject(array $user)
{
return (new User)->setRaw($user)->map([
'id' => $user['id'],
'name' => Arr::get($user, 'name'),
'email' => Arr::get($user, 'email'),
]);
}
/**
* Get the default options for an HTTP request.
*
* #param string $token
* #return array
*/
protected function getRequestOptions($token)
{
return [
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'token '.$token,
],
];
}
}
Next, in your ApplicationServiceProvider (or in a separate service provider), add the following:
use App\Socialite\PassportProvider;
use Illuminate\Support\Facades\URL;
use Laravel\Socialite\Facades\Socialite;
public function boot()
{
Socialite::extend('passport', function (function ($app) {
$config = $app['config']['services.passport'];
return new PassportProvider(
$app['request'],
$config['client_id'],
$config['client_secret'],
URL::to($config['redirect'])
);
});
}
Now you are ready to use your custom Socialite provider in your login controller on your client application:
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Laravel\Socialite\Facades\Socialite;
class LoginController extends Controller
{
/**
* Redirect the user to the GitHub authentication page.
*
* #return \Illuminate\Http\RedirectResponse
*/
public function redirectToProvider()
{
return Socialite::driver('passport')->redirect();
}
/**
* Obtain the user information from GitHub.
*
* #return \Illuminate\Http\Response
*/
public function handleProviderCallback()
{
$user = Socialite::driver('passport')->user();
}
}
Don't forget to register the login routes in your routes/web.php file:
Route::get('oauth/redirect', 'Auth\LoginController#redirectToProvider');
Route::get('oauth/callback', 'Auth\LoginController#handleProviderCallback');
A. You would only create one client_id and client_secret for Application B
B. Use Passport as you wrote above
C. You can, but you need to set it up manually which I'm not sure is a good idea, a better one would be to redirect it to Application A UI (In case of web) or directly contact Application A (in case of mobile) and let it handle it by itself.
there is two solutions
1. If you just need Application A database details than create multiple database connection in your Application B
https://fideloper.com/laravel-multiple-database-connections
2. If you want to access Application A endpoint than keep same files storage/oauth-public.key, storage/oauth-private.key for your both Application so Application A can easily Authenticate Application B authorisation token.
I am using LumenPassport (https://github.com/dusterio/lumen-passport) and I followed a few tutorials listed here.
I used a combination of these tutorials as well as a heck of google and stackoverflow searches to achieve what I have thus far:
http://esbenp.github.io/2017/03/19/modern-rest-api-laravel-part-4/
http://esbenp.github.io/2015/05/26/lumen-web-api-oauth-2-authentication/
https://blog.pusher.com/make-an-oauth2-server-using-laravel-passport/
What I achieved so far
1. Using password grant to get an access & refresh token
2. Storing these tokens in a secure http only cookie
3. Retrieving these tokens in Lumen's AuthServiceProvider
What I am unable to do
1. Getting the authenticated user with the AccessToken
I am trying to access either of these endpoints:
$router->group(['middleware' => 'auth:api'], function () use ($router) {
$router->get('/', function () use ($router) {return $router->app->version();});
$router->post('/logout', '\App\Auth\LoginController#logout');
});
I will immediately get an unauthorized error.. After some deep diving, the error comes from Authenticate.php which I know is called after AuthServiceProvider. I took a look at AuthServiceProvider and according to Lumen's documentation, this is how the boot method should looks like. Of course it is using the "api" driver and I had to switch it to "passport" for it to work.
AuthServiceProvider.php
public function boot()
{
$this->app['auth']->viaRequest('passport', function ($request) {
// dd("test") // this works
// dd(Auth::user());
// dd($request->user());
// dd(Auth::guard('api')->user());
});
}
Authenticate.php
public function handle($request, Closure $next, $guard = null)
{
if ($this->auth->guard($guard)->guest()) {
$status = Response::HTTP_UNAUTHORIZED;
return response()->json(['success' => false, 'status' => $status, 'message' => 'HTTP_UNAUTHORIZED'], $status);
}
return $next($request);
}
From here, I am still unable to get any of the authenticated user's information. I have made sure to access these endpoints with Postman with the appropriate Authorization headers.
The reason why I need to retrieve the user is because I hope that in my logout method, I will be able to then retrieve the accessToken of that authenticated user and revoke the token and clear the cookies.
LoginController.php
public function logout()
{
// Get the accessToken from Auth
// Need to fix AuthServiceProvider first
$accessToken = $this->auth->user()->token();
$refreshToken = $this->db
->table('oauth_refresh_tokens')
->where('access_token_id', $accessToken->id)
->update([
'revoked' => true,
]);
$accessToken->revoke();
$this->cookie->queue($this->cookie->forget(self::REFRESH_TOKEN));
}
At that point you cannot use Auth::user() since that function is the functionality for resolving that. So what you need to do is extract the bearer token with $request->bearerToken() and use that to retrieve your user.
Update
I took a look at your code and I would recommend the following:
An API is recommended to be 'stateless' meaning that it should not persist any state (i.e. cookies). It is far better to pass the access token with each request and let the application that accesses your API handle the tokens. Therefore I would recommend to remove the log-out functionality. Then you can do the following in your AuthServiceProvider:
if ($token_exists) {
$user = User::find($token->user_id);
return $user;
}
I'm currently using 2 projects. 1 front end (with laravel backend to communicate with API) and another laravel project (the API).
Now I use Laravel Passport to authenticate users and to make sure every API call is an authorized call.
Now when I want to log out my user, I send a post request to my API (with Bearer token) and try to log him out of the API (and clear session, cookies,...)
Then on the client I also refresh my session so the token is no longer known. Now when I go back to the login page, it automatically logs in my user. (Or my user is just still logged in).
Can someone explain me how to properly log out a user with Laravel passport?
Make sure that in User model, you have this imported
use Laravel\Passport\HasApiTokens;
and you're using the trait HasApiTokens in the User model class using
use HasApiTokens
inside the user class.
Now you create the log out route and in the controller,
do this
$user = Auth::user()->token();
$user->revoke();
return 'logged out'; // modify as per your need
This will log the user out from the current device where he requested to log out. If you want to log out from all the devices where he's logged in. Then do this instead
$tokens = $user->tokens->pluck('id');
Token::whereIn('id', $tokens)
->update(['revoked'=> true]);
RefreshToken::whereIn('access_token_id', $tokens)->update(['revoked' => true]);
Make sure to import these two at the top
use Laravel\Passport\RefreshToken;
use Laravel\Passport\Token;
This will revoke all the access and refresh tokens issued to that user. This will log the user out from everywhere. This really comes into help when the user changes his password using reset password or forget password option and you have to log the user out from everywhere.
You need to delete the token from the database table oauth_access_tokens
you can do that by creating a new model like OauthAccessToken
Run the command php artisan make:model OauthAccessToken to create the model.
Then create a relation between the User model and the new created OauthAccessToken Model , in User.php add :
public function AauthAcessToken(){
return $this->hasMany('\App\OauthAccessToken');
}
in UserController.php , create a new function for logout:
public function logoutApi()
{
if (Auth::check()) {
Auth::user()->AauthAcessToken()->delete();
}
}
In api.php router , create new route :
Route::post('logout','UserController#logoutApi');
Now you can logout by calling posting to URL /api/logout
This is sample code i'm used for log out
public function logout(Request $request)
{
$request->user()->token()->revoke();
return response()->json([
'message' => 'Successfully logged out'
]);
}
Create a route for logout:
$router->group(['middleware' => 'auth:api'], function () use ($router) {
Route::get('me/logout', 'UserController#logout');
});
Create a logout function in userController ( or as mentioned in your route)
public function logout() {
$accessToken = Auth::user()->token();
DB::table('oauth_refresh_tokens')
->where('access_token_id', $accessToken->id)
->update([
'revoked' => true
]);
$accessToken->revoke();
return response()->json(null, 204);
}
I am using Laravel 6.12.0, below function is working for me.
public function logout(Request $request){
$accessToken = Auth::user()->token();
$token= $request->user()->tokens->find($accessToken);
$token->revoke();
$response=array();
$response['status']=1;
$response['statuscode']=200;
$response['msg']="Successfully logout";
return response()->json($response)->header('Content-Type', 'application/json');
}
This is my first post.. and i find a clean solution (Laravel last Version)
/**
* Logout api
*
* #return \Illuminate\Http\Response
*/
public function logout(Request $request)
{
if (Auth::check()) {
$token = Auth::user()->token();
$token->revoke();
return $this->sendResponse(null, 'User is logout');
}
else{
return $this->sendError('Unauthorised.', ['error'=>'Unauthorised'] , Response::HTTP_UNAUTHORIZED);
}
}
Below is the simplest way I found to do it.
1. USE database SESSION INSTEAD OF file SESSION
Official documention
php artisan session:table
php artisan migrate
Replace SESSION_DRIVER=file by SESSION_DRIVER=database in your .env file.
2. DELETE USER SESSION RIGHT AFTER LOGIN
After a user is redirected to your frontend and logs in to finally get a token, you probably call a route in api/routes.php to get the user information, that's where I'm closing the user backend session before sending back user information to the frontend:
Route::middleware('auth:api')->get('/user', function (Request $request) {
// Close user session here
Illuminate\Support\Facades\DB::table('sessions')
->whereUserId($request->user()->id)
->delete();
return $request->user();
});
3. REVOKE TOKENS AT LOGOUT
Then, to "log out" (actually, revoke tokens) the user from the frontend, you just need to call another route to revoke the token and refresh_token:
Route::middleware('auth:api')->post('/logout', function (Request $request) {
// Revoke access token
// => Set oauth_access_tokens.revoked to TRUE (t)
$request->user()->token()->revoke();
// Revoke all of the token's refresh tokens
// => Set oauth_refresh_tokens.revoked to TRUE (t)
$refreshTokenRepository = app('Laravel\Passport\RefreshTokenRepository');
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($request->user()->token()->id);
return;
});
You may prefer to put these two closures in the UserController.
Hope help someone:
if (Auth::check()) {
$request->user()->tokens->each(function ($token, $key) {
$token->delete();
});
}
Good Luck.
I use this in my project to logout from multiple device.
public function logout(Request $request, $devices = FALSE)
{
$this->logoutMultiple(\Auth::user(), $devices);
return response()->json([], 204);
}
private function logoutMultiple(\App\Models\User $user, $devices = FALSE)
{
$accessTokens = $user->tokens();
if ($devices == 'all') {
} else if ($devices == 'other') {
$accessTokens->where('id', '!=', $user->token()->id);
} else {
$accessTokens->where('id', '=', $user->token()->id);
}
$accessTokens = $accessTokens->get();
foreach ($accessTokens as $accessToken) {
$refreshToken = \DB::table('oauth_refresh_tokens')
->where('access_token_id', $accessToken->id)
->update(['revoked' => TRUE]);
$accessToken->revoke();
}
}
Try this code to help you to logout from passport authentication.
Route::post('/logout', function(){
if (Auth::check()) {
Auth::user()->AauthAcessToken()->delete();
}
return response()->json([
'status' => 1,
'message' => 'User Logout',
], 200);
});
check whether your model contains OauthAccessToken which needs to connect with the database oauth_access_tokens. The access token is stored in the database table oauth_access_tokens. and makes a relation from users to oauth_access_tokens.
public function AauthAcessToken(){
return $this->hasMany(OauthAccessToken::class);
}
You can use following code to remove to token for logged in user.
$request->user()->token()->revoke();
If you want to learn about this in-depth then watch this tutorial:
https://www.youtube.com/watch?v=UKSQdg1uPbQ
public function logout(Request $request)
{
$request->user()->token()->revoke();
if ($request->everywhere) {
foreach ($request->user()->tokens()->whereRevoked(0)->get() as $token) {
$token->revoke();
}
}
return response()->json(['message' => 'success']);
}
How can you authenticate multiple types of users in Laravel 5.1 e.g. Jobseeker, Recruiter, Admin etc.
Some of you have suggested using a single users table to store only the password and email, creating profile tables to store user specific information (jobseeker_profile, recruiter_profile) and using roles to differentiate between the different types of users (i.e having a roles and role_user) table.
This is all very well but then what if the different types of users have different registration and login forms. How do you customize the default auth controller out of the box to display the correct view?
So if I have the following routes:
// Jobseeker Authentication routes...
Route::get('auth/login', 'Auth\AuthController#getLogin');
Route::post('auth/login', 'Auth\AuthController#postLogin');
Route::get('auth/logout', 'Auth\AuthController#getLogout');
// Jobseeker Registration routes...
Route::get('auth/register', 'Auth\AuthController#getRegister');
Route::post('auth/register', 'Auth\AuthController#postRegister');
// Recruiter Authentication routes...
Route::get('recruiter/auth/login', 'Auth\AuthController#getLogin');
Route::post('recruiter/auth/login', 'Auth\AuthController#postLogin');
Route::get('recruiter/auth/logout', 'Auth\AuthController#getLogout');
// Recruiter Registration routes...
Route::get('recruiter/auth/register', 'Auth\AuthController#getRegister');
Route::post('recruiter/auth/register', 'Auth\AuthController#postRegister');
This is the default auth controller out of the box:
class AuthController extends Controller
{
use AuthenticatesAndRegistersUsers;
public function __construct()
{
$this->middleware('guest', ['except' => 'getLogout']);
}
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|confirmed|min:6',
]);
}
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
}
traits used by the default out of the box auth controller:
trait AuthenticatesUsers
{
use RedirectsUsers;
public function getLogin()
{
return view('auth.login');
}
public function postLogin(Request $request)
{
$this->validate($request, [
'email' => 'required|email', 'password' => 'required',
]);
$credentials = $this->getCredentials($request);
if (Auth::attempt($credentials, $request->has('remember'))) {
return redirect()->intended($this->redirectPath());
}
return redirect($this->loginPath())
->withInput($request->only('email', 'remember'))
->withErrors([
'email' => $this->getFailedLoginMessage(),
]);
}
public function loginPath()
{
return property_exists($this, 'loginPath') ? $this->loginPath : '/auth/login';
}
}
trait RegistersUsers
{
use RedirectsUsers;
public function getRegister()
{
return view('auth.register');
}
public function postRegister(Request $request)
{
$validator = $this->validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}
Auth::login($this->create($request->all()));
return redirect($this->redirectPath());
}
}
I'm sure this is a very common requirement for many web applications but I can't find any helpful tutorials for Laravel specific implementations. All the tutorial simply focus on the out of the box implementation for some odd reason.
Any help on the above would be much appreciated.
This is not a solution to your question directly, but alternative way to solve your question problem with.
In stead of creating different username and password for different groups, make a central authentication that has roles. It called user and roles.
You can define groups with different roles, and each roles has specific access to respective area.
Regarding registration process you can make two differnet views but using the same controller, and for each view you can create a hidden field to indicate if it is jobseekers group or recruiter group.
Both will receive two different confirmation emails where they should fill the rest of the profile information, like recruiter should put company name and jobseeker should put his name etc. they might have two different tables for profile information, but still using the same login system.
By adding condition to middleware and correct route, if jobseeker tries to access recruiter area even if jobseeker is logged in the system, the jobseeker won't be able to access that area or the opposite way.
Since Laravel 5.1 has build in user login system, so you have few choices, build your own roles or use 3rd party.
I suggest you to build your own so you have control over your code and can further develop it as you wish with time. It might take you half day to get it run and understand how it works, but it is worth spending that time with the right approach in stead of the way you go in your Question OR using 3rd party is fine too, there is a lot of packages around you can search for. I have personally used Entrust (https://github.com/Zizaco/entrust) it is easy and nice way to provide roles and permissions to your project.
Here is also a link to video developed by Jeffrey Way at Laracast, it builds user and roles system from scratch for Laravel 4. but since you have user part, just follow roles part and with small modifications you will have a roles system to your Laravel 5.1, I have tried it and it works.
Regarding your question in the comments, when you follow the video you will understand the concept.
Link to the video: https://laracasts.com/lessons/users-and-roles
You might need to create account to see the video, most of videos are free.
Good practice
It is always also a good practice to illustrate what you want to achieve that makes things easier, I have just made an example for your project, but that is only example for learning:
I encourage you to read some of the topics regarding roles, here you will also find some inspiration to 3rd party acl systems to Laravel, there might be more articles but here is some:
Reading:
https://laracasts.com/discuss/channels/laravel/which-package-is-best-for-roles-permissions/?page=2
https://laracasts.com/discuss/channels/general-discussion/laravel-5-user-groups-management
https://laracasts.com/discuss/channels/general-discussion/roles-and-permissions-in-laravel-5
EDIT
Important Note
Laravel 5.1 has introduced Authorization, I have not found much documentation online yet but it is worth to spend some time learning it:
http://laravel.com/docs/5.1/authorization#policies
NEW UPDATE
There are some great videos solution for what you asking, follow ACL parts here
https://laracasts.com/series/whats-new-in-laravel-5-1
This might be very interesting too:
https://laracasts.com/lessons/email-verification-in-laravel
This will give you a complete own developed solution.
You can achieve multiple authentication easily by pulling up the sarav/laravel-multiauth package
composer require sarav/laravel-multiauth
I assume you have separate tables for Jobseeker, Recruiter, Admin.
Step 1 : Open app.php and replace
Illuminate\Auth\AuthServiceProvider::class
with
Sarav\Multiauth\MultiauthServiceProvider::class
Then, open up auth.php file and remove
<?php
return [
'driver' => 'eloquent',
'model' => 'App\User::class',
'table' => 'users',
'password' => [
'email' => 'emails.password',
'table' => 'password_resets',
'expire' => 60,
],
];
and add the following code
return [
'multi' => [
'jobseeker' => [
'driver' => 'eloquent',
'model' => App\Jobseeker::class, // Model Class
'table' => 'jobseeker' // jobseeker table
],
'recruiter' => [
'driver' => 'eloquent',
'model' => App\Recruiter::class, // Model Class
'table' => 'recruiter' //recruiter table
],
'admin' => [
'driver' => 'eloquent',
'model' => App\Admin::class, // Model Class
'table' => 'admin' //admin table
],
],
'password' => [
'email' => 'emails.password',
'table' => 'password_resets',
'expire' => 60,
]
];
Thats it!
Now you can try login attempt by calling
\Auth::attempt('jobseeker', ['email'=> 'johndoe#example.com', 'password' => 'secret']);
\Auth::attempt('recruiter', ['email'=> 'johndoe#example.com', 'password' => 'secret']);
\Auth::attempt('admin', ['email'=> 'johndoe#example.com', 'password' => 'secret']);
Always remember first paramter should be your user parameter. Here I have given jobseeker for jobseeker login attempt, recruiter for recruiter attempt and admin for admin login attempt. Without the proper first parameter system will throw exception.
For more detailed information checkout this article
http://sarav.co/blog/multiple-authentication-in-laravel-continued/
Short Answer: Add user types to your users table with specific number.
TL;DR answer.
Long Answer:
If you have migrated your table, just run php artisan migrate:rollback.
Add following line to your migration table for users:
$table->integer("user_type")->default(0);
Here I am considering that user type zero is just a simple JobSeeker.
And in your form, you can add option with value zero and one such that people will be selecting what they want to be like recruiter. There is no need of other
As another solution, i can suggest you to use a polymorphic relation between User and Account, like
class User extends Eloquent {
...
public function account() {
return $this->morphTo();
}
}
class Account extends Eloquent {
...
public function user() {
return $this->morphOne(App\User::class, 'account');
}
}
class JobSeeker extends Account { ... }
class Recruiter extends Account { ... }
For different types of Account, you can use route prefixes and different auth controllers, specially for registration who differs for each account instances :
// Recruiter Authentication routes...
Route::group(['prefix' => 'recruiter'], function() {
Route::controller('auth', 'Auth\RecruiterAuthController');
});
At last, you can access the authenticated account directly from auth()->user()->account. it will return any instance of Account (Recruiter, Admin, ....)
hope it helps you ;)
I will try to explain how authentication is managed in Laravel 5.1
On application start AuthServiceProvider is called, which calls registerAuthenticator() function in which new AuthManager is created.
AuthServiceProvider -> registerAuthenticator() -> new AuthManager()
On manager create createNameDriver function will be called in which new nameProvider will be created, where name is your auth driver selected in auth config file. Then in that function new Guard will be created and nameProivder will be passed to its contractor. All auth functions in that Guard will use functions from that provider to manage auth. Provider implements UserProvider which has
retrieveById($identifier),
retrieveByToken($identifier, $token),
updateRememberToken(Authenticatable $user, $token),
retrieveByCredentials(array $credentials),
validateCredentials(Authenticatable $user, array $credentials)
functions.
Main idea of managing multi auth in Laravel 5.1 is to create new AutServiceProvider and on its boot pass app auth new AuthModelProvider which functions then will be used in same Guard. In AuthModelProvider you can manage all retrieve functions the way you need.
Here is all changed I've made to manage multi auth. My project name is APC, that's why I use it everywhere.
Add this function to your models
public function getAuthIdentifier()
{
return [self::MODULE_NAME => $this->getKey()];
}
Create AuthServiceProvider in Provider/YourProjectName directory. In boot function we extend auth from our new provider AuthModelProvider.
<?php
namespace App\Providers\Apc;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Hashing\BcryptHasher;
class AuthServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
self::getAuthModels();
$this->app['auth']->extend('apc', function() {
return new AuthModelProvider(self::getAuthModels(), new BcryptHasher());
});
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
}
public static function getAuthModels()
{
$configModels = config('auth.models');
$authModels = [];
foreach ($configModels as $key => $class) {
$authModel = new $class();
$authModels [$key]= $authModel;
}
return $authModels;
}
}
Create AuthModelProvider in same directory. Diff in my models is existence of login field in company table. But you can be more specific if you want. In retrieveByCridentials function I just look for existence of login and choose my model accordingly.
<?php
namespace App\Providers\Apc;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Support\Str;
class AuthModelProvider implements UserProvider
{
protected $users;
protected $hasher;
public function __construct($usersModels, HasherContract $hasher)
{
$this->users = $usersModels;
$this->hasher = $hasher;
}
/**
* Retrieve a user by their unique identifier.
*
* #param mixed $identifier
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifiers)
{
foreach ($identifiers as $key => $id) {
if (isset($this->users[$key])) {
return $this->users[$key]->where('id', $id)->active()->base()->first();
}
}
}
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* #param mixed $identifier
* #param string $token
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifiers, $token)
{
return null;
$user = $this->getUserByIdentifier($identifiers);
if ($user) {
return $user->where($user->getRememberTokenName(), $token)->active()->first();
}
}
/**
* Update the "remember me" token for the given user in storage.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $token
* #return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
$user->setRememberToken($token);
$user->save();
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials)) {
return null;
}
if (isset($credentials['login'])) {
$userModel = $this->users['company'];
} else {
$userModel = $this->users['user'];
}
$query = $userModel->newQuery();
foreach ($credentials as $key => $value) {
if (! Str::contains($key, 'password')) {
$query->where($key, $value);
}
}
return $query->first();
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
$plain = $credentials['password'];
return $this->hasher->check($plain, $user->getAuthPassword());
}
private function getUserByIdentifier($identifiers)
{
if (!$identifiers) {
}
foreach ($identifiers as $namespace => $id) {
if (isset($this->users[$namespace])) {
return $this->users[$namespace];
}
}
return null;
}
}
Add AuthServiceProvider to app conf file.
\App\Providers\Apc\AuthServiceProvider::class,
Make this changes to auth conf file.
'driver' => 'apc',
'models' => [
\App\Apc\User\User::MODULE_NAME => \App\Apc\User\User::class,
\App\Apc\Company\Company::MODULE_NAME => \App\Apc\Company\Company::class
],
That's all. Hope it was helpful.