18

As the title suggests, is it possible to authenticate without DB (instead, using User Provider)?

I've seen posts about how to authenticate without password, but would it be ever possible to authenticate without DB?

Here is what I've been trying to achieve...

  1. Ask the user to submit a PIN
  2. Compare the PIN with a value within .env
  3. If the PIN is correct, authenticate the user

It seems to be possible to do authenticate without a traditional RDBMS by introducing a user provider.

However, the doc doesn't seem to describe how the user provider should look like.

Here are snippets of my code (well, I simply mimicked the doc)...

class AuthServiceProvider extends ServiceProvider {

    public function boot()
    {
        $this->registerPolicies();

        Auth::provider('myUser', function ($app, array $config) {
            // Return an instance of Illuminate\Contracts\Auth\UserProvider...

            return new MyUserProvider($app->make('myUser'));
        });
    }
}

In auth.php...

'providers' => [
    'users' => [
        'driver' => 'myUser',
    ],
],

Now, I have no clue as to how to proceed.

So, I'd like to know...

  1. How the user provider should look like

  2. if it's possible to authenticate users with env() in the first place

Any advice will be appreciated.

Hiroki
  • 3,893
  • 13
  • 51
  • 90
  • I haven't looked into it too much, so can't really answer the question very well, but I think you'll want to look at authentication guards in Laravel. https://laravel.com/docs/5.6/authentication#adding-custom-guards – Phil Cross May 30 '18 at 10:26
  • @PhilCross Thanks for your comment. Yeah I noticed it too, but doc doesn't explain how to achieve it really well :( – Hiroki May 30 '18 at 10:28
  • 1
    https://code.tutsplus.com/tutorials/how-to-create-a-custom-authentication-guard-in-laravel--cms-29667 https://mattstauffer.com/blog/multiple-authentication-guard-drivers-including-api-in-laravel-5-2/ https://medium.com/@sirajul.anik/laravel-api-authenticate-user-with-custom-driver-different-table-using-auth-middleware-fa2cabec2d61 Hopefully these might help a little. I haven't read them, but they look alright. – Phil Cross May 30 '18 at 10:30
  • I think you have to follow this path http://laravel-recipes.com/recipes/115/using-your-own-authentication-driver Not sure exactly. I also do not have any experience in this kind of a situation. Since this is an interesting question I'm also trying to implement this as a learning project. – Ravisha Hesh May 30 '18 at 10:42

3 Answers3

27

Create your own Guard ignoring UserProvider interface. I adopted it in my project and it's working well.

PinGuard.php

<?php

declare(strict_types=1);

namespace App\Auth\Guards;

use App\User;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;

/**
 * Class PinGuard
 */
class PinGuard implements Guard
{
    /**
     * @var null|Authenticatable|User
     */
    protected $user;

    /**
     * @var Request
     */
    protected $request;

    /**
     * OpenAPIGuard constructor.
     *
     * @param Request $request
     */
    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    /**
     * Check whether user is logged in.
     *
     * @return bool
     */
    public function check(): bool
    {
        return (bool)$this->user();
    }

    /**
     * Check whether user is not logged in.
     *
     * @return bool
     */
    public function guest(): bool
    {
        return !$this->check();
    }

    /**
     * Return user id or null.
     *
     * @return null|int
     */
    public function id(): ?int
    {
        $user = $this->user();
        return $user->id ?? null;
    }

    /**
     * Manually set user as logged in.
     * 
     * @param  null|\App\User|\Illuminate\Contracts\Auth\Authenticatable $user
     * @return $this
     */
    public function setUser(?Authenticatable $user): self
    {
        $this->user = $user;
        return $this;
    }

    /**
     * @param  array $credentials
     * @return bool
     */
    public function validate(array $credentials = []): bool
    {
        throw new \BadMethodCallException('Unexpected method call');
    }

    /**
     * Return user or throw AuthenticationException.
     *
     * @throws AuthenticationException
     * @return \App\User
     */
    public function authenticate(): User
    {
        $user = $this->user();
        if ($user instanceof User) {
            return $user;
        }
        throw new AuthenticationException();
    }

    /**
     * Return cached user or newly authenticate user.
     *
     * @return null|\App\User|\Illuminate\Contracts\Auth\Authenticatable
     */
    public function user(): ?User
    {
        return $this->user ?: $this->signInWithPin();
    }

    /**
     * Sign in using requested PIN.
     *
     * @return null|User
     */
    protected function signInWithPin(): ?User
    {
        // Implement your logic here
        // Return User on success, or return null on failure
    }

    /**
     * Logout user.
     */
    public function logout(): void
    {
        if ($this->user) {
            $this->setUser(null);
        }
    }
}

NoRememberTokenAuthenticatable.php

User should mixin this trait.

<?php

declare(strict_types=1);

namespace App\Auth;

use Illuminate\Database\Eloquent\Model;

/**
 * Trait NoRememberTokenAuthenticatable
 *
 * @mixin Model
 */
trait NoRememberTokenAuthenticatable
{
    /**
     * Get the name of the unique identifier for the user.
     *
     * @return string
     */
    public function getAuthIdentifierName()
    {
        return 'id';
    }

    /**
     * Get the unique identifier for the user.
     *
     * @return mixed
     */
    public function getAuthIdentifier()
    {
        return $this->id;
    }

    /**
     * Get the password for the user.
     *
     * @return string
     * @codeCoverageIgnore
     */
    public function getAuthPassword()
    {
        throw new \BadMethodCallException('Unexpected method call');
    }

    /**
     * Get the token value for the "remember me" session.
     *
     * @return string
     * @codeCoverageIgnore
     */
    public function getRememberToken()
    {
        throw new \BadMethodCallException('Unexpected method call');
    }

    /**
     * Set the token value for the "remember me" session.
     *
     * @param string $value
     * @codeCoverageIgnore
     */
    public function setRememberToken($value)
    {
        throw new \BadMethodCallException('Unexpected method call');
    }

    /**
     * Get the column name for the "remember me" token.
     *
     * @return string
     * @codeCoverageIgnore
     */
    public function getRememberTokenName()
    {
        throw new \BadMethodCallException('Unexpected method call');
    }
}

AuthServiceProvider.php

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Auth\Guards\PinGuard;
use Illuminate\Container\Container;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [];

    /**
     * Register any authentication / authorization services.
     */
    public function boot()
    {
        Auth::extend('pin', function (Container $app) {
            return new PinGuard($app['request']);
        });
        $this->registerPolicies();
    }
}

config/auth.php

You should comment out most of them.

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option controls the default authentication "guard" and password
    | reset options for your application. You may change these defaults
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => 'web',
        // 'passwords' => 'users',
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | here which uses session storage and the Eloquent user provider.
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | Supported: "session", "token"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'pin',
        ],
        // 'api' => [
        //     'driver' => 'session',
        //     'provider' => 'users',
        // ],
        // 'web' => [
        //     'driver' => 'session',
        //     'provider' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        // 'users' => [
        //     'driver' => 'eloquent',
        //     'model' => App\User::class,
        // ],
        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | You may specify multiple password reset configurations if you have more
    | than one user table or model in the application and you want to have
    | separate password reset settings based on the specific user types.
    |
    | The expire time is the number of minutes that the reset token should be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    */

    // 'passwords' => [
    //     'users' => [
    //         'provider' => 'users',
    //         'table' => 'password_resets',
    //         'expire' => 60,
    //     ],
    // ],

];
mpyw
  • 5,526
  • 4
  • 30
  • 36
  • 1
    Then you can normally use `Auth` Facade, like `Auth::user()` `Auth::id()` – mpyw May 31 '18 at 06:47
  • 1
    should be the aceptes Answer – sandman Mar 07 '19 at 01:38
  • I've published the part of this as a library: [mpyw/null-auth: Null Guard for Laravel. Designed for Middleware-based authentication and testing.](https://github.com/mpyw/null-auth) – mpyw Dec 17 '19 at 11:57
  • Using this solution, how would a normal login without db look like? Please help: https://stackoverflow.com/questions/60298133/eloquent-queries-without-database-data-is-retrieved-from-external-api – Floris Feb 19 '20 at 12:52
  • 3
    And could you please show a code example of how to use this new guard. I am using this as a login request. But still not passing the auth middleware: $user = new User([ 'id' => $result->success->user->id, 'name' => $result->success->user->name, 'email' => $result->success->user->email, 'password' => $password ]); Auth::setUser($user); $re = Auth::authenticate(); – Floris Feb 19 '20 at 13:05
  • This is fantastic, this is the correct answer, nice work. – Mike Lucid Nov 14 '21 at 06:29
23

Thank you all, but there was a much simpler solution... which is to use session().

In the controller,

public function login(Request $request)
{
    if ($request->input('pin') === env('PIN')) {
        $request->session()->put('authenticated', time());
        return redirect()->intended('success');
    }

    return view('auth.login', [
        'message' => 'Provided PIN is invalid. ',
    ]);
    //Or, you can throw an exception here.
}

The route looks like,

Route::group(['middleware' => ['web', 'custom_auth']], function () {
    Route::get('/success', 'Controller@success')->name('success');
});

The custom_auth will looke like,

public function handle($request, Closure $next)
{
    if (!empty(session('authenticated'))) {
        $request->session()->put('authenticated', time());
        return $next($request);
    }

    return redirect('/login');
}

Hope this will help someone.

Hiroki
  • 3,893
  • 13
  • 51
  • 90
  • what is the location of the `custom_auth` and did you create a simple `
    ` with just one input where users enter the PIN?
    – ST80 Sep 19 '19 at 08:39
  • 2
    Please consider not using `env()` into your controller or model. You should create new config file in `config/` directory. If you're using `env()`, you will get `null` value after caching your config. [Docs](https://laravel.com/docs/8.x/configuration) – ibnɘꟻ Feb 16 '21 at 04:10
  • Every user has the same pin? Or there is only one User? BC there will only be one pin to the ```env``` variable set to PIN. Am I missing something? There's no way that this works.... – Mike Lucid Nov 14 '21 at 06:27
-5

If you know who the user is (IE: have a $user instance already), you can simply login without using any guards as shown in the documentation

Auth::login($user);

I would pass in a User instance instead of $email/$password to your function

$user = User::whereEmail($email)->first();
$class->login($user);

and then in your class

public static function Login(User $user) 
{
// ...
 if(substr ( $result, 0, 3 ) == '+OK')
    Auth::login($user);
    return true; //user will be logged in and then redirect
 else
    return false;
 }
foram kantaria
  • 748
  • 6
  • 11
  • Thank your for your suggestion... but I'm in a situation where DB is not available. So, `User::whereEmail` won't get any instance. Also, I don't think that `Auth::login($user)` can be used, since I don't have any user instance. – Hiroki May 30 '18 at 21:11
  • 1
    Maybe this answer is not the best for the question, but google brought me here and its what I was looking for :) – alvaropgl Nov 27 '19 at 15:47
  • Answers provide bits and pieces of information. Would be great if there was a working sample .. Are there any available? – user2233677 Dec 14 '20 at 03:45