Laravel Passport - Authenticating web app against api with Custom UserProvider - laravel

I'm really new building laravel apps, I have a restful laravel API and a web app, I want the client web app to authenticate against the API and store the user in the session, I've registered a new UserProvider and set it on the config`s auth like bellow
ServiceProvider
public function boot()
{
$this->registerPolicies();
Auth::provider('apiAuthServiceProvider', function ($app, $config) {
return new UserProvider(new ApiUserService());
});
}
Config/Auth
'providers' => [
'users' => [
'driver' => 'apiAuthServiceProvider',
],
],
UserProvider Class
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Auth\UserProvider as IlluminateUserProvider;
class UserProvider implements IlluminateUserProvider
{
private $userService;
public function __construct($userService)
{
$this->userService = $userService;
}
/**
* #param mixed $identifier
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
// Get and return a user by their unique identifier
}
/**
* #param mixed $identifier
* #param string $token
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
// Get and return a user by their unique identifier and "remember me" token
}
/**
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $token
* #return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
// Save the given "remember me" token for the given user
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// Get and return a user by looking up the given credentials
}
/**
* 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)
{
// Check that given credentials belong to the given user
}
}
The Custom UserProvider injects a UserService class, with is responsible for making requests to the API and return the user...
I`m so lost, what UserProvider methods should i override from "UserProvider" Interface? "retrieveById", "retrieveByToken", "updateRememberToken", "retrieveByCredentials" and "validateCredentials" ? Or should I override all of them? Considering the the client web app will have a login form, and the user will authenticate sending the email and password (grant_type = password), I'm also confusing about the token, how should I store the token and refresh token in the session? Is that possible to set session timeout as the same as the token expiration time? Where would I call the retrieveByCredentials's UserProvider to pass the authentication params? Thanks in advance....

You should override only the functions you need. Most of the standard functionality should be already defined in the User Provider you are inheriting from. I've only inherited my custom user providers from Illuminate\Auth\EloquentUserProvider (Laravel 5.4 here), so please double check how the class you are inheriting from works. If, for example, you need to retrieve your user by an ID different than your default id field, you should override retrieveById.

Related

How do I make the reset password url dynamic?

<?php
namespace Illuminate\Auth\Notifications;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Lang;
class ResetPassword extends Notification
{
/**
* The password reset token.
*
* #var string
*/
public $token;
/**
* The callback that should be used to create the reset password URL.
*
* #var \Closure|null
*/
public static $createUrlCallback;
/**
* The callback that should be used to build the mail message.
*
* #var \Closure|null
*/
public static $toMailCallback;
/**
* Create a notification instance.
*
* #param string $token
* #return void
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the notification's channels.
*
* #param mixed $notifiable
* #return array|string
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
if (static::$toMailCallback) {
return call_user_func(static::$toMailCallback, $notifiable, $this->token);
}
if (static::$createUrlCallback) {
$url = call_user_func(static::$createUrlCallback, $notifiable, $this->token);
} else {
$url = url(route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
}
return (new MailMessage)
->subject(Lang::get('Reset Password Notification'))
->line(Lang::get('You are receiving this email because we received a password reset request for your account.'))
->action(Lang::get('Reset Password'), $url)
->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]))
->line(Lang::get('If you did not request a password reset, no further action is required.'));
}
/**
* Set a callback that should be used when creating the reset password button URL.
*
* #param \Closure $callback
* #return void
*/
public static function createUrlUsing($callback)
{
static::$createUrlCallback = $callback;
}
/**
* Set a callback that should be used when building the notification mail message.
*
* #param \Closure $callback
* #return void
*/
public static function toMailUsing($callback)
{
static::$toMailCallback = $callback;
}
}
Hi, I am using Laravel 7.6.2.
I keep on getting an error. I am trying to make a multiauth login system, and I am testing the password reset routes. The problem is that when I access the admin forgot password page, the email that is sent actually contains a link to the user password reset page, not the admin password reset page.
So route('password.reset' should actually be route('admin.password.reset' for the admin request. But I really have no clue how to make this URL dynamic.... Help please!!
Another option is to add this to the boot method in your AppServiceProvider:
ResetPassword::createUrlUsing(function ($notifiable, $token) {
return "http://www.my-spa.co/password/reset/{$token}";
});
I use Laravel as an API and needed this to generate a link to my single page application url.
The ResetPassword notification provided by the Laravel framework allows custom URLs out of the box. The method createUrlUsing lets you provide a function that will generate the URL in the output email.
Example in the User model class:
// Import the ResetPassword class from the framework
use Illuminate\Auth\Notifications\ResetPassword;
class User extends Authenticatable {
// ... the rest of your implementation
// The customization of the email happens here
/**
* Send the password reset notification.
*
* #param string $token
* #return void
*/
public function sendPasswordResetNotification($token) {
// The trick is first to instantiate the notification itself
$notification = new ResetPassword($token);
// Then use the createUrlUsing method
$notification->createUrlUsing(function ($token) {
return 'http://acustomurl.lol';
});
// Then you pass the notification
$this->notify($notification);
}
}
I don't know if it's completely off topic but that was what I was looking for 😅
I have done as following:
In Admin user class override sendPasswordResetNotification method:
/**
* Send the password reset notification.
*
* #param string $token
* #return void
*/
public function sendPasswordResetNotification($token)
{
$this->notify(new AdminMailResetPasswordToken($token));
}
In AdminMailResetPasswordToken extends default Laravel ResetPassword notification class:
namespace App\Notifications\Admin\Auth;
use Illuminate\Auth\Notifications\ResetPassword;
class AdminMailResetPasswordToken extends ResetPassword
{
public static $createUrlCallback = [self::class, 'createActionUrl'];
public static function createActionUrl($notifiable, $token)
{
return url(route('admins.password.reset', [
'token' => $token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
}
}
ResetPassword::createUrlUsing(function ($notifiable, $token) {
$route = Request::is('admin/password/reset')
? 'admin.password.reset'
: 'password.reset';
return url(route($route, [
'token' => $token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
});`
Here what I did in the User model
use Illuminate\Auth\Notifications\ResetPassword;
/**
* Override the mail body for reset password notification mail.
*/
public function sendPasswordResetNotification($token)
{
ResetPassword::createUrlUsing(function ($user, string $token) {
return 'https://example.com/reset-password?token='.$token;
});
$this->notify(new ResetPassword($token));
}

laravel 6 api resource policy index

The policy does not work on the index apiResource call. Always returns 403..
My ProjectPolicy is this:
class ProjectPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any projects.
*
* #param \App\User $user
* #return mixed
*/
public function viewAny(User $user)
{
return true;
}
...
My ProjectController used this:
class ProjectController extends Controller
{
public function __construct()
{
$this->authorizeResource(ProjectPolicy::class, 'project');
}
...
I registered the policy:
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
Project::class => ProjectPolicy::class,
];
Everything works except the index method call:
/**
* Display a listing of the resource.
*
* #param Request $request
* #return Response
*/
public function index(Request $request)
{
return $request->user()->projects()->get();
}
In the project controller constructor, you need to authorize the resource (model) not the policy itself
public function __construct()
{
$this->authorizeResource(\App\Project::class, 'project');
}
Also make sure that the user making the request is authenticated, can't have guests authorized
But if you want guests to be authorized, you may mark the User model nullable/optional on the Dependency injection on the Policy class
/**
* Determine whether the user can view any projects.
*
* #param \App\User $user
* #return mixed
*/
public function viewAny(?User $user)
{
return true;
}
Hope this helps

Method Illuminate\Auth\RequestGuard::attempt does not exist. when trying custom authentication via web route

I have to authenticate users via an external api (something like ldap) and have been trying to realize authentication via a closure request guard as documented here https://laravel.com/docs/master/authentication#closure-request-guards
It works fine if the user logs in correctly, however on auth failure laravel throws the mentioned error https://laravel.com/docs/master/authentication#closure-request-guards if the failed attempt is returning null from the closure (as it says in the documentation). If it just returns false, laravel doesn't throw an error, however there is no validation feedback.
Auth::viaRequest('ldap', function ($request) {
$credentials = $request->only('login_id', 'password');
if ($user = ExternalLDPAAuth::auth()) {
return $user;
} else {
return null; // laravel throws error
// return false; // <- would not throw error, but no validation
}
}
Is there an easier way to do custom authentication?
I don't really understand the documentation about https://laravel.com/docs/5.7/authentication#authenticating-users
in the end I have to write the guard just like above anyway, right?
You haven't shown the code where you're calling attempt(), but you don't need to use that method when authenticating via the request. You use attempt() when a user attempts to login with credentials and you need to explicitly attempt to authenticate. With request authentication, Laravel will automatically attempt to authenticate as the request is handled, so your code can simply check to see if auth()->check() returns true or not.
In the end I decided to customize the EloquentUserProvider instead of the guard. In the end all i needed was additional logic to validate credentials and retrieve a user by credentials in case the user hadn't logged in yet. I.e. checking the normal eloquent logic first and then checking against the external API if nothing was found (also checking for case of changed password).
class CustomUserProvider extends EloquentUserProvider
{
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
// (...)
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// (...)
}
}
// config/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'custom',
],
// (...)
],
// providers/AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
Auth::provider('custom', function ($app, array $config) {
return new CustomUserProvider($app['hash'], $config['model']);
});
}
}

How to disable laravel 5.2 password bcrypt

I want to disable the laravel password bcrypt when I try to log-in like this
Auth::guard('client')->attempt(
'id' => $request['id'],
'password' => $request['password'])
But it seems to be more dificult than I thought, I know I should not do this but I temporally need to work like this, but laravel forces me to use encrypted passwords. I need to be able to use plain passwords on my database.
I been searching on internet but I cant find a solution.
Try extending SessionGuard and overriding function hasValidCredentials()
Create A file by name 'SessionGuardExtended' in App\CoreExtensions
use Illuminate\Auth\SessionGuard;
use Illuminate\Contracts\Auth\Authenticatable;
class SessionGuardExtended extends SessionGuard
{
/**
* Determine if the user matches the credentials.
*
* #param mixed $user
* #param array $credentials
* #return bool
*/
protected function hasValidCredentials($user, $credentials)
{
return ! is_null($user) && $credentials['password'] == $user->getAuthPassword();
}
}
Edit config/auth.php edit the driver and use sessionExtended
'web' => [
'driver' => 'sessionExtended',
'provider' => 'users',
],
In AppServiceProvider Write Code in boot function
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Auth::extend(
'sessionExtended',
function ($app) {
$provider = new EloquentUserProvider($app['hash'], config('auth.providers.users.model'));
return new SessionGuardExtended('sessionExtended', $provider, app()->make('session.store'), request());
}
);
}
Reference: Extending Laravel 5.2 SessionGuard
You can extend Illuminate\Auth\EloquentUserProvider, ie:
<?php
namespace App\Services\Auth;
use Illuminate\Auth\EloquentUserProvider as BaseUserProvider;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
class UserProvider extends BaseUserProvider {
/**
* Create a new database user provider.
*
* #param string $model
*
* #return void
*/
public function __construct($model)
{
$this->model = $model;
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
*
* #return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['password'];
// $matches = some method of matching $plain with $user->getAuthPassword();
return $matches;
}
}
Then register this in the IoC in a service provider like so:
<?php // ...
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
// ...
$this->app['auth']->extend(
'legacy',
function () {
return new \Illuminate\Auth\Guard(
new \App\Services\Auth\UserProvider(
$this->app['config']['auth.model']
),
$this->app['session.store']
);
}
);
// ...
}
Then set your current driver to legacy in config/auth.php.
PS: You may want to include the classes in the provider,
You can use
Auth::guard('client')->login($user);
In this $user is an instance of Model which is implemented Illuminate\Contracts\Auth\Authenticatable contract.
In user model it is already implemented.
Maybe it will helpful

Laravel 5.3 Passport Custom Grants?

I know I am not the only person who has come up to this point. Does anyone know how to properly implement a custom grant in Laravel(5.3) Passport?
Or
Have a good link/tutorial to reference how to properly do it?
I know there's this package:
https://github.com/mikemclin/passport-custom-request-grant
But I'm asking for a more "Do it yourself" approach.
Thank you in advance.
namespace App\Providers;
use App\Auth\Grants\FacebookGrant;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Bridge\RefreshTokenRepository;
use Laravel\Passport\Passport;
use League\OAuth2\Server\AuthorizationServer;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
app(AuthorizationServer::class)->enableGrantType(
$this->makeFacebookGrant(), Passport::tokensExpireIn()
);
Passport::routes();
//
}
/**
* Create and configure a Facebook grant instance.
*
* #return FacebookGrant
*/
protected function makeFacebookGrant()
{
$grant = new FacebookGrant(
$this->app->make(RefreshTokenRepository::class)
);
$grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn());
return $grant;
}
}
EDIT:
sorry for only posting this code, i do not know how much this code is going to be useful to you.
Well, here i'll leave my implementation of FacebookGrant, hope this helps someone.
<?php
namespace App\Auth\Grants;
use Illuminate\Http\Request;
use Laravel\Passport\Bridge\User;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AbstractGrant;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
class FacebookGrant extends AbstractGrant
{
/**
* #param RefreshTokenRepositoryInterface $refreshTokenRepository
*/
public function __construct(
RefreshTokenRepositoryInterface $refreshTokenRepository
) {
$this->setRefreshTokenRepository($refreshTokenRepository);
$this->refreshTokenTTL = new \DateInterval('P1M');
}
/**
* {#inheritdoc}
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
\DateInterval $accessTokenTTL
) {
// Validate request
$client = $this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request));
$user = $this->validateUser($request, $client);
// Finalize the requested scopes
$scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());
// Issue and persist new tokens
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $scopes);
$refreshToken = $this->issueRefreshToken($accessToken);
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
return $responseType;
}
/**
* #param ServerRequestInterface $request
*
* #return UserEntityInterface
* #throws OAuthServerException
*/
protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
{
$facebookId = $this->getRequestParameter('facebook_id', $request);
if (is_null($facebookId)) {
throw OAuthServerException::invalidRequest('facebook_id');
}
$email = $this->getRequestParameter('email', $request);
if (is_null($email)) {
throw OAuthServerException::invalidRequest('email');
}
$user = $this->getUserEntityByUserFacebookId(
$facebookId,
$email,
$this->getIdentifier(),
$client
);
if ($user instanceof UserEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidCredentials();
}
return $user;
}
/**
* Retrieve a user by the given Facebook Id.
*
* #param string $facebookId
* #param string $email
* #param string $grantType
* #param \League\OAuth2\Server\Entities\ClientEntityInterface $clientEntity
*
* #return \Laravel\Passport\Bridge\User|null
* #throws \League\OAuth2\Server\Exception\OAuthServerException
*/
private function getUserEntityByUserFacebookId($facebookId, $email, $grantType, ClientEntityInterface $clientEntity)
{
$provider = config('auth.guards.api.provider');
if (is_null($model = config('auth.providers.'.$provider.'.model'))) {
throw new RuntimeException('Unable to determine authentication model from configuration.');
}
$user = (new $model)->where('facebook_id', $facebookId)->first();
if (is_null($user)) {
$user = (new $model)->where('email', $email)->first();
if (is_null($user)) {
return;
}
// Now that we retrieved the user with the email, we need to update it with
// the given facebook id. So the user account will be linked correctly.
$user->facebook_id = $facebookId;
$user->save();
}
return new User($user->getAuthIdentifier());
}
/**
* {#inheritdoc}
*/
public function getIdentifier()
{
return 'facebook';
}
}
I am not sure what do you mean by custom grant but you can use passport service for password grant which can be customized however you like.
Reference: https://laravel.com/docs/5.3/passport#password-grant-tokens
To give more insight, you get a client_id and client_secret which all users of your api will use to get access_token and refresh_token using their password and email as well and then you can customize the process by adding features via middleware.
For example you can check some customized header if present or change database based on request to perform further query and so on.
Maybe I got whole wrong idea about your ques, if that's the case then please specify more about your needs.

Resources