├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json └── src ├── AuthManager.php ├── Contracts ├── MagicBroker.php └── MagicBrokerFactory.php ├── Controllers └── LoginController.php ├── Facades └── Magic.php ├── Guard.php ├── Magic.php ├── MagicBroker.php ├── MagicBrokerManager.php ├── MagicServiceProvider.php ├── MagicUserProvider.php ├── Notifications └── SendLoginLink.php ├── Traits ├── AuthenticatesUsers.php └── Magical.php ├── config └── auth.php ├── database └── migrations │ └── 2019_12_16_100000_create_auth_requests_table.php └── routes └── web.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2019 [Karl Monson](https://github.com/karlmonson) 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Magic 2 | 3 | Laravel Magic is a passwordless authentication driver. Users receive a login link via email. 4 | 5 | ## Installation 6 | 7 | Install via Composer: 8 | 9 | ```bash 10 | composer require karlmonson/laravel-magic 11 | ``` 12 | 13 | The package service provider and facade will be automatically registered. 14 | 15 | ## Setup 16 | 17 | After installation, run migrations: 18 | 19 | ```bash 20 | php artisan migrate 21 | ``` 22 | 23 | This will create a new table ```magic_auth_requests``` in your database. 24 | 25 | Next, replace the default ```AuthenticatesUsers``` trait on your ```LoginController``` with the following: 26 | 27 | ```php 28 | use KarlMonson\Magic\Traits\AuthenticatesUsers; 29 | 30 | class LoginController extends Controller 31 | { 32 | use AuthenticatesUsers; 33 | 34 | ... 35 | } 36 | ``` 37 | 38 | You'll also need to add the ```Magical``` trait to your user model: 39 | 40 | ```php 41 | use KarlMonson\Magic\Traits\Magical; 42 | 43 | class User extends Authenticatable 44 | { 45 | use Magical, Notifiable; 46 | 47 | ... 48 | } 49 | ``` 50 | 51 | We suggest dropping the ```password``` column from your user table, or at least making it ```nullable```. 52 | 53 | ## Configuration 54 | 55 | Next, in your ```auth``` config file, replace the 'users' driver with 'magic': 56 | 57 | ```php 58 | 'providers' => [ 59 | 'users' => [ 60 | 'driver' => 'magic', 61 | 'model' => App\User::class, 62 | ], 63 | ], 64 | ``` 65 | 66 | You may also specify an 'expire' option for Magic to use, this is how long login tokens will stay alive. The default is 10 if this is not specified. 67 | 68 | ```php 69 | 'magic' => [ 70 | 'expire' => 10, 71 | ] 72 | ``` 73 | 74 | ## Credits 75 | 76 | - [Karl Monson](https://github.com/karlmonson) - Author 77 | - [Fast](https://fast.co) - Inspiration 78 | - [Slack](https://slack.com) - Inspiration 79 | 80 | ## License 81 | 82 | The MIT License (MIT). Please see [License File](https://github.com/karlmonson/laravel-magic/blob/master/LICENSE.md) for more information. 83 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "karlmonson/laravel-magic", 3 | "description": "Laravel Magic is a passwordless authentication driver", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Karl Monson", 8 | "email": "karl@karlmonson.com", 9 | "homepage": "https://github.com/karlmonson", 10 | "role": "Developer" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "KarlMonson\\Magic\\": "src/" 16 | } 17 | }, 18 | "require": { 19 | "illuminate/auth": "^6.0", 20 | "illuminate/database": "^6.0", 21 | "illuminate/notifications": "^6.0", 22 | "illuminate/support": "^6.0" 23 | }, 24 | "extra": { 25 | "laravel": { 26 | "providers": [ 27 | "KarlMonson\\Magic\\MagicServiceProvider" 28 | ], 29 | "aliases": { 30 | "Magic": "KarlMonson\\Magic\\Facades\\Magic" 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/AuthManager.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | $this->name = $name; 17 | } 18 | 19 | protected function createDriver($driver) { 20 | $guard = parent::createDriver($driver); 21 | 22 | $guard->setCookieJar($this->app['cookie']); 23 | $guard->setDispatcher($this->app['events']); 24 | return $guard->setRequest($this->app->refresh('request', $guard, 'setRequest')); 25 | } 26 | 27 | public function createMagicDriver() { 28 | $provider = $this->createMagicProvider(); 29 | return new Guard($provider, $this->app['session.store'], $this->name); 30 | } 31 | 32 | protected function createMagicProvider() { 33 | $model = $this->config['model']; 34 | return new MagicUserProvider($model); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Contracts/MagicBroker.php: -------------------------------------------------------------------------------- 1 | middleware('guest')->except('logout'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Facades/Magic.php: -------------------------------------------------------------------------------- 1 | name = $name; 19 | } 20 | 21 | public function getName() { 22 | return 'login_' . $this->name . '_' . md5(get_class($this)); 23 | } 24 | 25 | public function getRecallerName() { 26 | return 'remember_' . $this->name . '_' . md5(get_class($this)); 27 | } 28 | 29 | public function get() { 30 | return $this->user(); 31 | } 32 | 33 | public function impersonate($type, $id, $remember = false) { 34 | if($this->check()) { 35 | return Auth::$type()->loginUsingId($id, $remember); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/Magic.php: -------------------------------------------------------------------------------- 1 | app = $app; 20 | $this->config = $this->app['config']['auth.providers']; 21 | 22 | foreach($this->config as $key => $config) { 23 | if($config['driver'] == 'magic') { 24 | $this->providers[$key] = new AuthManager($this->app, $key, $config); 25 | } 26 | } 27 | } 28 | 29 | public function __call($name, $arguments = array()) { 30 | if(array_key_exists($name, $this->providers)) { 31 | return $this->providers[$name]; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/MagicBroker.php: -------------------------------------------------------------------------------- 1 | users = $users; 36 | $this->tokens = $tokens; 37 | } 38 | 39 | /** 40 | * Send a magic login link to a user. 41 | * 42 | * @param array $credentials 43 | * @return string 44 | */ 45 | public function sendLoginLink(array $credentials) 46 | { 47 | // First we will check to see if we found a user at the given credentials and 48 | // if we did not we will redirect back to this current URI with a piece of 49 | // "flash" data in the session to indicate to the developers the errors. 50 | $user = $this->getUser($credentials); 51 | 52 | if (is_null($user)) { 53 | return static::INVALID_USER; 54 | } 55 | 56 | if (method_exists($this->tokens, 'recentlyCreatedToken') && 57 | $this->tokens->recentlyCreatedToken($user)) { 58 | return static::LOGIN_THROTTLED; 59 | } 60 | 61 | // Once we have the reset token, we are ready to send the message out to this 62 | // user with a link to reset their password. We will then redirect back to 63 | // the current URI having nothing set in the session to indicate errors. 64 | $user->sendMagicLoginNotification( 65 | $this->tokens->create($user) 66 | ); 67 | 68 | return static::LOGIN_LINK_SENT; 69 | } 70 | 71 | /** 72 | * Login the user for the given token. 73 | * 74 | * @param array $credentials 75 | * @param \Closure $callback 76 | * @return mixed 77 | */ 78 | public function login(array $credentials) 79 | { 80 | $user = $this->validateLogin($credentials); 81 | 82 | $this->tokens->delete($user); 83 | 84 | return static::MAGIC_LOGIN; 85 | } 86 | 87 | /** 88 | * Validate a password reset for the given credentials. 89 | * 90 | * @param array $credentials 91 | * @return \Illuminate\Contracts\Auth\CanResetPassword|string 92 | */ 93 | protected function validateLogin(array $credentials) 94 | { 95 | if (is_null($user = $this->getUser($credentials))) { 96 | return static::INVALID_USER; 97 | } 98 | 99 | if (! $this->tokens->exists($user, $credentials['token'])) { 100 | return static::INVALID_TOKEN; 101 | } 102 | 103 | return $user; 104 | } 105 | 106 | /** 107 | * Get the user for the given credentials. 108 | * 109 | * @param array $credentials 110 | * @return \Illuminate\Contracts\Auth\CanResetPassword|null 111 | * 112 | * @throws \UnexpectedValueException 113 | */ 114 | public function getUser(array $credentials) 115 | { 116 | $credentials = Arr::except($credentials, ['token']); 117 | 118 | $user = $this->users->retrieveByCredentials($credentials); 119 | 120 | if(! $user) { 121 | $user = $this->users->createModel(); 122 | $user->email = $credentials['email']; 123 | $user->save(); 124 | } 125 | 126 | return $user; 127 | } 128 | 129 | /** 130 | * Create a new password reset token for the given user. 131 | * 132 | * @param \Illuminate\Contracts\Auth\CanResetPassword $user 133 | * @return string 134 | */ 135 | public function createToken($user) 136 | { 137 | return $this->tokens->create($user); 138 | } 139 | 140 | /** 141 | * Delete password reset tokens of the given user. 142 | * 143 | * @param \Illuminate\Contracts\Auth\CanResetPassword $user 144 | * @return void 145 | */ 146 | public function deleteToken($user) 147 | { 148 | $this->tokens->delete($user); 149 | } 150 | 151 | /** 152 | * Validate the given password reset token. 153 | * 154 | * @param \Illuminate\Contracts\Auth\CanResetPassword $user 155 | * @param string $token 156 | * @return bool 157 | */ 158 | public function tokenExists($user, $token) 159 | { 160 | return $this->tokens->exists($user, $token); 161 | } 162 | 163 | /** 164 | * Get the password reset token repository implementation. 165 | * 166 | * @return \Illuminate\Auth\Passwords\TokenRepositoryInterface 167 | */ 168 | public function getRepository() 169 | { 170 | return $this->tokens; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/MagicBrokerManager.php: -------------------------------------------------------------------------------- 1 | app = $app; 39 | } 40 | 41 | /** 42 | * Attempt to get the broker from the local cache. 43 | * 44 | * @param string|null $name 45 | * @return \Illuminate\Contracts\Auth\PasswordBroker 46 | */ 47 | public function broker($name = null) 48 | { 49 | $name = $name ?: $this->getDefaultDriver(); 50 | 51 | return $this->brokers[$name] ?? ($this->brokers[$name] = $this->resolve($name)); 52 | } 53 | 54 | /** 55 | * Resolve the given broker. 56 | * 57 | * @param string $name 58 | * @return \Illuminate\Contracts\Auth\PasswordBroker 59 | * 60 | * @throws \InvalidArgumentException 61 | */ 62 | protected function resolve($name) 63 | { 64 | $config = $this->getConfig($name); 65 | 66 | if (is_null($config)) { 67 | throw new InvalidArgumentException("Magic authenticator [{$name}] is not defined."); 68 | } 69 | 70 | // The password broker uses a token repository to validate tokens and send user 71 | // password e-mails, as well as validating that password reset process as an 72 | // aggregate service of sorts providing a convenient interface for resets. 73 | return new MagicBroker( 74 | $this->createTokenRepository($config), 75 | $this->createUserProvider($config['provider'] ?? null) 76 | ); 77 | } 78 | 79 | /** 80 | * Create a token repository instance based on the given configuration. 81 | * 82 | * @param array $config 83 | * @return \Illuminate\Auth\Passwords\TokenRepositoryInterface 84 | */ 85 | protected function createTokenRepository(array $config) 86 | { 87 | $key = $this->app['config']['app.key']; 88 | 89 | if (Str::startsWith($key, 'base64:')) { 90 | $key = base64_decode(substr($key, 7)); 91 | } 92 | 93 | $connection = $config['connection'] ?? null; 94 | 95 | return new DatabaseTokenRepository( 96 | $this->app['db']->connection($connection), 97 | $this->app['hash'], 98 | 'magic_auth_requests', 99 | $key, 100 | $config['expire'], 101 | $config['throttle'] ?? 0 102 | ); 103 | } 104 | 105 | /** 106 | * Create an instance of the database user provider. 107 | * 108 | * @param array $config 109 | * @return \Illuminate\Auth\DatabaseUserProvider 110 | */ 111 | protected function createUserProvider($config) 112 | { 113 | $model = $this->app['config']['auth.providers.users.model'] ?? null; 114 | 115 | return new MagicUserProvider($model); 116 | } 117 | 118 | /** 119 | * Get the password broker configuration. 120 | * 121 | * @param string $name 122 | * @return array 123 | */ 124 | protected function getConfig($name) 125 | { 126 | return $this->app['config']["auth.{$name}"]; 127 | } 128 | 129 | /** 130 | * Get the default password broker name. 131 | * 132 | * @return string 133 | */ 134 | public function getDefaultDriver() 135 | { 136 | return 'magic'; 137 | } 138 | 139 | /** 140 | * Set the default password broker name. 141 | * 142 | * @param string $name 143 | * @return void 144 | */ 145 | public function setDefaultDriver($name) 146 | { 147 | $this->app['config']['auth.defaults.magic'] = $name; 148 | } 149 | 150 | /** 151 | * Dynamically call the default driver instance. 152 | * 153 | * @param string $method 154 | * @param array $parameters 155 | * @return mixed 156 | */ 157 | public function __call($method, $parameters) 158 | { 159 | return $this->broker()->{$method}(...$parameters); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/MagicServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/database/migrations'); 25 | 26 | $this->loadRoutesFrom(__DIR__.'/routes/web.php'); 27 | 28 | Auth::provider('magic', function ($app, array $config) { 29 | return new MagicUserProvider($config['model']); 30 | }); 31 | } 32 | 33 | /** 34 | * Register the service provider. 35 | * 36 | * @return void 37 | */ 38 | public function register() 39 | { 40 | $this->mergeConfigFrom( 41 | __DIR__.'/config/auth.php', 'auth' 42 | ); 43 | 44 | $this->app->singleton('auth.magic', function ($app) { 45 | return new MagicBrokerManager($app); 46 | }); 47 | 48 | $this->app->bind('auth.magic.broker', function ($app) { 49 | return $app->make('auth.magic')->broker(); 50 | }); 51 | } 52 | 53 | /** 54 | * Get the services provided by the provider. 55 | * 56 | * @return array 57 | */ 58 | public function provides() 59 | { 60 | return ['auth.magic', 'auth.magic.broker']; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/MagicUserProvider.php: -------------------------------------------------------------------------------- 1 | model = $model; 29 | } 30 | 31 | /** 32 | * Retrieve a user by their unique identifier. 33 | * 34 | * @param mixed $identifier 35 | * @return \Illuminate\Contracts\Auth\Authenticatable|null 36 | */ 37 | public function retrieveById($identifier) 38 | { 39 | $model = $this->createModel(); 40 | 41 | return $this->newModelQuery($model) 42 | ->where($model->getAuthIdentifierName(), $identifier) 43 | ->first(); 44 | } 45 | 46 | /** 47 | * Retrieve a user by their unique identifier and "remember me" token. 48 | * 49 | * @param mixed $identifier 50 | * @param string $token 51 | * @return \Illuminate\Contracts\Auth\Authenticatable|null 52 | */ 53 | public function retrieveByToken($identifier, $token) 54 | { 55 | $model = $this->createModel(); 56 | 57 | $retrievedModel = $this->newModelQuery($model)->where( 58 | $model->getAuthIdentifierName(), $identifier 59 | )->first(); 60 | 61 | if (! $retrievedModel) { 62 | return; 63 | } 64 | 65 | $rememberToken = $retrievedModel->getRememberToken(); 66 | 67 | return $rememberToken && hash_equals($rememberToken, $token) 68 | ? $retrievedModel : null; 69 | } 70 | 71 | /** 72 | * Update the "remember me" token for the given user in storage. 73 | * 74 | * @param \Illuminate\Contracts\Auth\Authenticatable|\Illuminate\Database\Eloquent\Model $user 75 | * @param string $token 76 | * @return void 77 | */ 78 | public function updateRememberToken(UserContract $user, $token) 79 | { 80 | $user->setRememberToken($token); 81 | 82 | $timestamps = $user->timestamps; 83 | 84 | $user->timestamps = false; 85 | 86 | $user->save(); 87 | 88 | $user->timestamps = $timestamps; 89 | } 90 | 91 | /** 92 | * Retrieve a user by the given credentials. 93 | * 94 | * @param array $credentials 95 | * @return \Illuminate\Contracts\Auth\Authenticatable|null 96 | */ 97 | public function retrieveByCredentials(array $credentials) 98 | { 99 | if (empty($credentials) || 100 | (count($credentials) === 1 && 101 | array_key_exists('token', $credentials))) { 102 | return; 103 | } 104 | 105 | // First we will add each credential element to the query as a where clause. 106 | // Then we can execute the query and, if we found a user, return it in a 107 | // Eloquent User "model" that will be utilized by the Guard instances. 108 | $query = $this->newModelQuery(); 109 | 110 | foreach ($credentials as $key => $value) { 111 | if (Str::contains($key, 'token')) { 112 | continue; 113 | } 114 | 115 | if (is_array($value) || $value instanceof Arrayable) { 116 | $query->whereIn($key, $value); 117 | } else { 118 | $query->where($key, $value); 119 | } 120 | } 121 | 122 | return $query->first(); 123 | } 124 | 125 | /** 126 | * Validate a user against the given credentials. 127 | * 128 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 129 | * @param array $credentials 130 | * @return bool 131 | */ 132 | public function validateCredentials(UserContract $user, array $credentials) 133 | { 134 | return isset($credentials['token']); 135 | } 136 | 137 | /** 138 | * Get a new query builder for the model instance. 139 | * 140 | * @param \Illuminate\Database\Eloquent\Model|null $model 141 | * @return \Illuminate\Database\Eloquent\Builder 142 | */ 143 | protected function newModelQuery($model = null) 144 | { 145 | return is_null($model) 146 | ? $this->createModel()->newQuery() 147 | : $model->newQuery(); 148 | } 149 | 150 | /** 151 | * Create a new instance of the model. 152 | * 153 | * @return \Illuminate\Database\Eloquent\Model 154 | */ 155 | public function createModel() 156 | { 157 | $class = '\\'.ltrim($this->model, '\\'); 158 | 159 | return new $class; 160 | } 161 | 162 | /** 163 | * Gets the name of the Eloquent user model. 164 | * 165 | * @return string 166 | */ 167 | public function getModel() 168 | { 169 | return $this->model; 170 | } 171 | 172 | /** 173 | * Sets the name of the Eloquent user model. 174 | * 175 | * @param string $model 176 | * @return $this 177 | */ 178 | public function setModel($model) 179 | { 180 | $this->model = $model; 181 | 182 | return $this; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Notifications/SendLoginLink.php: -------------------------------------------------------------------------------- 1 | token = $token; 34 | } 35 | 36 | /** 37 | * Get the notification's channels. 38 | * 39 | * @param mixed $notifiable 40 | * @return array|string 41 | */ 42 | public function via($notifiable) 43 | { 44 | return ['mail']; 45 | } 46 | 47 | /** 48 | * Build the mail representation of the notification. 49 | * 50 | * @param mixed $notifiable 51 | * @return \Illuminate\Notifications\Messages\MailMessage 52 | */ 53 | public function toMail($notifiable) 54 | { 55 | if (static::$toMailCallback) { 56 | return call_user_func(static::$toMailCallback, $notifiable, $this->token); 57 | } 58 | 59 | return (new MailMessage) 60 | ->subject(Lang::get('Login Link Notification')) 61 | ->line(Lang::get('You are receiving this email because we received a login request for your account.')) 62 | ->action(Lang::get('Login'), url(config('app.url').route('magic', ['token' => $this->token, 'email' => $notifiable->email], false))) 63 | ->line(Lang::get('This login link will expire in :count minutes.', ['count' => config('auth.magic.expire')])) 64 | ->line(Lang::get('If you did not request a login link, no further action is required.')); 65 | } 66 | 67 | /** 68 | * Set a callback that should be used when building the notification mail message. 69 | * 70 | * @param \Closure $callback 71 | * @return void 72 | */ 73 | public static function toMailUsing($callback) 74 | { 75 | static::$toMailCallback = $callback; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Traits/AuthenticatesUsers.php: -------------------------------------------------------------------------------- 1 | validateEmail($request); 37 | 38 | $response = $this->broker()->sendLoginLink( 39 | $this->loginCredentials($request) 40 | ); 41 | 42 | $request->session()->put('remember', $request->filled('remember')); 43 | 44 | return redirect()->back()->with('status', trans($response)); 45 | } 46 | 47 | /** 48 | * Handle a login request to the application. 49 | * 50 | * @param \Illuminate\Http\Request $request 51 | * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse 52 | * 53 | * @throws \Illuminate\Validation\ValidationException 54 | */ 55 | public function magic(Request $request) 56 | { 57 | $this->validateLogin($request); 58 | 59 | // If the class is using the ThrottlesLogins trait, we can automatically throttle 60 | // the login attempts for this application. We'll key this by the username and 61 | // the IP address of the client making these requests into this application. 62 | if (method_exists($this, 'hasTooManyLoginAttempts') && 63 | $this->hasTooManyLoginAttempts($request)) { 64 | $this->fireLockoutEvent($request); 65 | 66 | return $this->sendLockoutResponse($request); 67 | } 68 | 69 | if ($this->attemptLogin($request)) { 70 | $this->broker()->login($this->credentials($request)); 71 | return $this->sendLoginResponse($request); 72 | } 73 | 74 | // If the login attempt was unsuccessful we will increment the number of attempts 75 | // to login and redirect the user back to the login form. Of course, when this 76 | // user surpasses their maximum number of attempts they will get locked out. 77 | $this->incrementLoginAttempts($request); 78 | 79 | return $this->sendFailedLoginResponse($request); 80 | } 81 | 82 | /** 83 | * Validate the email for the given request. 84 | * 85 | * @param \Illuminate\Http\Request $request 86 | * @return void 87 | */ 88 | protected function validateEmail(Request $request) 89 | { 90 | $request->validate(['email' => 'required|email']); 91 | } 92 | 93 | /** 94 | * Validate the user login request. 95 | * 96 | * @param \Illuminate\Http\Request $request 97 | * @return void 98 | * 99 | * @throws \Illuminate\Validation\ValidationException 100 | */ 101 | protected function validateLogin(Request $request) 102 | { 103 | $request->validate([ 104 | $this->username() => 'required|string', 105 | 'token' => 'required|string' 106 | ]); 107 | } 108 | 109 | /** 110 | * Attempt to log the user into the application. 111 | * 112 | * @param \Illuminate\Http\Request $request 113 | * @return bool 114 | */ 115 | protected function attemptLogin(Request $request) 116 | { 117 | return $this->guard()->attempt( 118 | $this->credentials($request), $request->session()->get('remember') 119 | ); 120 | } 121 | 122 | /** 123 | * Get the needed authorization credentials from the request. 124 | * 125 | * @param \Illuminate\Http\Request $request 126 | * @return array 127 | */ 128 | protected function credentials(Request $request) 129 | { 130 | return $request->only($this->username(), 'token'); 131 | } 132 | 133 | /** 134 | * Get the needed authorization credentials from the request. 135 | * 136 | * @param \Illuminate\Http\Request $request 137 | * @return array 138 | */ 139 | protected function loginCredentials(Request $request) 140 | { 141 | return $request->only($this->username()); 142 | } 143 | 144 | /** 145 | * Send the response after the user was authenticated. 146 | * 147 | * @param \Illuminate\Http\Request $request 148 | * @return \Illuminate\Http\Response 149 | */ 150 | protected function sendLoginResponse(Request $request) 151 | { 152 | $request->session()->regenerate(); 153 | 154 | $this->clearLoginAttempts($request); 155 | 156 | return $this->authenticated($request, $this->guard()->user()) 157 | ?: redirect()->intended($this->redirectPath()); 158 | } 159 | 160 | /** 161 | * The user has been authenticated. 162 | * 163 | * @param \Illuminate\Http\Request $request 164 | * @param mixed $user 165 | * @return mixed 166 | */ 167 | protected function authenticated(Request $request, $user) 168 | { 169 | // 170 | } 171 | 172 | /** 173 | * Get the failed login response instance. 174 | * 175 | * @param \Illuminate\Http\Request $request 176 | * @return \Symfony\Component\HttpFoundation\Response 177 | * 178 | * @throws \Illuminate\Validation\ValidationException 179 | */ 180 | protected function sendFailedLoginResponse(Request $request) 181 | { 182 | throw ValidationException::withMessages([ 183 | $this->username() => [trans('auth.failed')], 184 | ]); 185 | } 186 | 187 | /** 188 | * Get the login username to be used by the controller. 189 | * 190 | * @return string 191 | */ 192 | public function username() 193 | { 194 | return 'email'; 195 | } 196 | 197 | /** 198 | * Log the user out of the application. 199 | * 200 | * @param \Illuminate\Http\Request $request 201 | * @return \Illuminate\Http\Response 202 | */ 203 | public function logout(Request $request) 204 | { 205 | $this->guard()->logout(); 206 | 207 | $request->session()->invalidate(); 208 | 209 | return $this->loggedOut($request) ?: redirect('/'); 210 | } 211 | 212 | /** 213 | * The user has logged out of the application. 214 | * 215 | * @param \Illuminate\Http\Request $request 216 | * @return mixed 217 | */ 218 | protected function loggedOut(Request $request) 219 | { 220 | // 221 | } 222 | 223 | /** 224 | * Get the guard to be used during authentication. 225 | * 226 | * @return \Illuminate\Contracts\Auth\StatefulGuard 227 | */ 228 | protected function guard() 229 | { 230 | return Auth::guard(); 231 | } 232 | 233 | /** 234 | * Get the broker to be used during authentication. 235 | * 236 | * @return \CartHero\Magic\MagicBroker 237 | */ 238 | protected function broker() 239 | { 240 | return Magic::broker(); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/Traits/Magical.php: -------------------------------------------------------------------------------- 1 | notify(new SendLoginLink($token)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/config/auth.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'expire' => 10, 6 | ] 7 | ]; 8 | -------------------------------------------------------------------------------- /src/database/migrations/2019_12_16_100000_create_auth_requests_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token'); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('magic_auth_requests'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/routes/web.php: -------------------------------------------------------------------------------- 1 | middleware('web')->name('magic'); 4 | --------------------------------------------------------------------------------