Browse Source

Add reverse-proxy guard to support authentication proxy

Bubka 3 years ago
parent
commit
911e18c9c4

+ 1 - 1
app/Api/v1/Resources/UserResource.php

@@ -17,7 +17,7 @@ class UserResource extends JsonResource
     {
         return [
             'name'  => $this->name,
-            'email' => $this->when(Auth::guard('api')->user(), $this->email),
+            'email' => $this->when(Auth::guard()->check(), $this->email),
         ];
     }
 }

+ 67 - 0
app/Extensions/RemoteUserProvider.php

@@ -0,0 +1,67 @@
+<?php
+
+// Part of Firefly III (https://github.com/firefly-iii)
+// see https://github.com/firefly-iii/firefly-iii/blob/main/app/Support/Authentication/RemoteUserProvider.php
+
+namespace App\Extensions;
+
+use App\Models\User;
+use Illuminate\Contracts\Auth\Authenticatable;
+use Illuminate\Contracts\Auth\UserProvider;
+use Illuminate\Support\Str;
+use Exception;
+
+class RemoteUserProvider implements UserProvider
+{
+    /**
+     * @inheritDoc
+     */
+    public function retrieveById($identifier): User
+    {
+        $user = User::where('email', $identifier)->first();
+
+        // if (null === $user) {
+        //     $user = User::create(
+        //         [
+        //             'name' => $identifier,
+        //             'email' => $identifier,
+        //             'password' => bcrypt(Str::random(64)),
+        //         ]
+        //     );
+        // }
+
+        return $user;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function retrieveByToken($identifier, $token)
+    {
+        throw new Exception(sprintf('No implementation for %s', __METHOD__));
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function updateRememberToken(Authenticatable $user, $token)
+    {
+        throw new Exception(sprintf('No implementation for %s', __METHOD__));
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function retrieveByCredentials(array $credentials)
+    {
+        throw new Exception(sprintf('No implementation for %s', __METHOD__));
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function validateCredentials(Authenticatable $user, array $credentials)
+    {
+        throw new Exception(sprintf('No implementation for %s', __METHOD__));
+    }
+}

+ 1 - 0
app/Http/Kernel.php

@@ -39,6 +39,7 @@ class Kernel extends HttpKernel
             \Illuminate\View\Middleware\ShareErrorsFromSession::class,
             \App\Http\Middleware\VerifyCsrfToken::class,
             \Illuminate\Routing\Middleware\SubstituteBindings::class,
+            \App\Http\Middleware\LogUserLastSeen::class,
             \App\Http\Middleware\CustomCreateFreshApiToken::class,
         ],
 

+ 1 - 13
app/Http/Middleware/Authenticate.php

@@ -6,17 +6,5 @@ use Illuminate\Auth\Middleware\Authenticate as Middleware;
 
 class Authenticate extends Middleware
 {
-    /**
-     * Get the path the user should be redirected to when they are not authenticated.
-     *
-     * @param  \Illuminate\Http\Request  $request
-     * @return string
-     * @codeCoverageIgnore
-     */
-    protected function redirectTo($request)
-    {
-        if (! $request->expectsJson()) {
-            return route('login');
-        }
-    }
+
 }

+ 10 - 4
app/Http/Middleware/LogUserLastSeen.php

@@ -13,14 +13,20 @@ class LogUserLastSeen
      *
      * @param  \Illuminate\Http\Request  $request
      * @param  \Closure  $next
+     * @param  string|null $guard
      * @return mixed
      */
-    public function handle($request, Closure $next)
+    public function handle($request, Closure $next, ...$quards)
     {
+        $guards = empty($guards) ? [null] : $guards;
 
-        if( Auth::guard('api')->check() && !$request->bearerToken()) {
-            Auth::guard('api')->user()->last_seen_at = Carbon::now()->format('Y-m-d H:i:s');
-            Auth::guard('api')->user()->save();
+        foreach ($guards as $guard) {
+            // Activity coming from a client authenticated with a personal access token is not logged
+            if( Auth::guard($guard)->check() && !$request->bearerToken()) {
+                Auth::guard($guard)->user()->last_seen_at = Carbon::now()->format('Y-m-d H:i:s');
+                Auth::guard($guard)->user()->save();
+                break;
+            }
         }
 
         return $next($request);

+ 15 - 0
app/Providers/AuthServiceProvider.php

@@ -5,7 +5,9 @@ namespace App\Providers;
 use Illuminate\Support\Facades\Gate;
 use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
 use Illuminate\Support\Facades\Auth;
+use App\Services\Auth\ReverseProxyGuard;
 use App\Extensions\EloquentTwoFAuthProvider;
+use App\Extensions\RemoteUserProvider;
 use DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator;
 use Illuminate\Contracts\Hashing\Hasher;
 
@@ -46,6 +48,19 @@ class AuthServiceProvider extends ServiceProvider
                 );
             });
 
+        // 
+        Auth::provider('remote-user', function ($app, array $config) {
+            // Return an instance of Illuminate\Contracts\Auth\UserProvider...
+    
+            return new RemoteUserProvider;
+        });
+
+        Auth::extend('reverse-proxy', function ($app, string $name, array $config) {  
+            // Return an instance of Illuminate\Contracts\Auth\Guard...
+
+            return new ReverseProxyGuard(Auth::createUserProvider($config['provider']));
+        });
+
 
         // Normally we should set the Passport routes here using Passport::routes().
         // If so the passport routes would be set for both 'web' and 'api' middlewares without

+ 109 - 0
app/Services/Auth/ReverseProxyGuard.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace App\Services\Auth;
+
+use Exception;
+use Illuminate\Contracts\Auth\Authenticatable;
+use Illuminate\Contracts\Auth\Guard;
+use Illuminate\Contracts\Auth\UserProvider;
+use Illuminate\Support\Facades\Log;
+
+class ReverseProxyGuard implements Guard
+{
+    /**
+     * The currently authenticated user.
+     *
+     * @var \Illuminate\Contracts\Auth\Authenticatable
+     */
+    protected $user;
+
+    /**
+     * The user provider implementation.
+     *
+     * @var \Illuminate\Contracts\Auth\UserProvider
+     */
+    protected $provider;
+
+    /**
+     * Create a new authentication guard.
+     *
+     * @param Illuminate\Contracts\Auth\UserProvider $provider
+     * @return void
+     */
+    public function __construct(UserProvider $provider)
+    {
+        $this->provider = $provider;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function check(): bool
+    {
+        return !is_null($this->user());
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function guest(): bool
+    {
+        return !$this->check();
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function user()
+    {
+        // If we've already retrieved the user for the current request we can just
+        // return it back immediately. We do not want to fetch the user data on
+        // every call to this method because that would be tremendously slow.
+        if (!is_null($this->user)) {
+            return $this->user;
+        }
+
+        $user = null;
+
+        // Get the user identifier from $_SERVER or apache filtered headers
+        $header = config('auth.guard_header', 'REMOTE_USER');
+        $userID = request()->server($header) ?? apache_request_headers()[$header] ?? null;
+
+        if (null === $userID) {
+            Log::error(sprintf('No user in header "%s".', $header));
+            return $this->user = null;
+            // throw new Exception('The guard header was unexpectedly empty. See the logs.');
+        }
+
+        $user = $this->provider->retrieveById($userID);
+
+        return $this->user = $user;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function id()
+    {
+        return $this->user;
+    }
+
+    /**
+     * Validate a user's credentials.
+     *
+     * @param  array  $credentials
+     * @return Exception
+     */
+    public function validate(array $credentials = [])
+    {
+        throw new Exception('No implementation for RemoteUserGuard::validate()');
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function setUser(Authenticatable $user)
+    {
+        $this->user = $user;
+    }
+}

+ 12 - 1
config/auth.php

@@ -14,9 +14,11 @@ return [
     */
 
     'defaults' => [
-        'guard' => 'web',
+        'guard' => env('AUTHENTICATION_GUARD', 'web'),
         'passwords' => 'users',
     ],
+    'guard_header' => env('AUTHENTICATION_GUARD_HEADER', 'REMOTE_USER'),
+    // 'guard_email'  => env('AUTHENTICATION_GUARD_EMAIL_HEADER', null),
 
     /*
     |--------------------------------------------------------------------------
@@ -46,6 +48,11 @@ return [
             'provider' => 'users',
             'hash' => false,
         ],
+
+        'reverse-proxy' => [
+            'driver'   => 'reverse-proxy',
+            'provider' => 'remote-user',
+        ],
     ],
 
     /*
@@ -70,6 +77,10 @@ return [
             'driver' => 'eloquent-2fauth',
             'model' => App\Models\User::class,
         ],
+        'remote-user' => [
+            'driver' => 'remote-user',
+            'model'  => App\Models\User::class,
+        ],
     ],
 
     /*

+ 1 - 1
resources/js/routes.js

@@ -17,7 +17,7 @@ import Register         from './views/auth/Register'
 import PasswordRequest  from './views/auth/password/Request'
 import PasswordReset    from './views/auth/password/Reset'
 import WebauthnLost     from './views/auth/webauthn/Lost'
-import WebauthnRecover     from './views/auth/webauthn/Recover'
+import WebauthnRecover  from './views/auth/webauthn/Recover'
 import SettingsOptions  from './views/settings/Options'
 import SettingsAccount  from './views/settings/Account'
 import SettingsOAuth    from './views/settings/OAuth'

+ 3 - 0
resources/js/views/auth/Login.vue

@@ -129,6 +129,9 @@
                 const { data } = await vm.axios.get('api/v1/user/name')
 
                 if( data.name ) {
+                    if( data.email ) {
+                        return next({ name: 'accounts' });
+                    }
                     vm.username = data.name
                 }
                 else {

+ 2 - 4
routes/api/v1.php

@@ -13,11 +13,9 @@ use Illuminate\Http\Request;
 |
 */
 
-Route::group(['middleware' => 'guest:api'], function () {
-    Route::get('user/name', 'UserController@show')->name('user.show.name');
-});
+Route::get('user/name', 'UserController@show')->name('user.show.name');
 
-Route::group(['middleware' => 'auth:api'], function() {
+Route::group(['middleware' => 'auth:reverse-proxy,api'], function() {
 
     Route::get('oauth/personal-access-tokens', '\Laravel\Passport\Http\Controllers\PersonalAccessTokenController@forUser')->name('passport.personal.tokens.index');
     Route::post('oauth/personal-access-tokens', '\Laravel\Passport\Http\Controllers\PersonalAccessTokenController@store')->name('passport.personal.tokens.store');

+ 8 - 16
routes/web.php

@@ -1,6 +1,6 @@
 <?php
 
-// use Illuminate\Support\Facades\Route;
+use Illuminate\Support\Facades\Route;
 use App\Http\Controllers\Auth\WebAuthnManageController;
 use App\Http\Controllers\Auth\WebAuthnRegisterController;
 use App\Http\Controllers\Auth\WebAuthnLoginController;
@@ -18,22 +18,14 @@ use App\Http\Controllers\Auth\WebAuthnRecoveryController;
 |
 */
 
-// Route::get('/', function () {
-//     return view('welcome');
-// });
+Route::post('user', 'Auth\RegisterController@register')->name('user.register');
+Route::post('user/password/lost', 'Auth\ForgotPasswordController@sendResetLinkEmail')->middleware('AvoidResetPassword')->name('user.password.lost');;
+Route::post('user/password/reset', 'Auth\ResetPasswordController@reset')->name('user.password.reset');
+Route::post('webauthn/lost', [WebAuthnDeviceLostController::class, 'sendRecoveryEmail'])->name('webauthn.lost');
+Route::post('webauthn/recover/options', [WebAuthnRecoveryController::class, 'options'])->name('webauthn.recover.options');
+Route::post('webauthn/recover', [WebAuthnRecoveryController::class, 'recover'])->name('webauthn.recover');
 
-// Route::get('twofaccount/{TwoFAccount}', 'TwoFAccountController@show');
-
-Route::group(['middleware' => 'guest:web'], function () {
-    Route::post('user', 'Auth\RegisterController@register')->name('user.register');
-    Route::post('user/password/lost', 'Auth\ForgotPasswordController@sendResetLinkEmail')->middleware('AvoidResetPassword')->name('user.password.lost');;
-    Route::post('user/password/reset', 'Auth\ResetPasswordController@reset')->name('user.password.reset');
-    Route::post('webauthn/lost', [WebAuthnDeviceLostController::class, 'sendRecoveryEmail'])->name('webauthn.lost');
-    Route::post('webauthn/recover/options', [WebAuthnRecoveryController::class, 'options'])->name('webauthn.recover.options');
-    Route::post('webauthn/recover', [WebAuthnRecoveryController::class, 'recover'])->name('webauthn.recover');
-});
-
-Route::group(['middleware' => 'auth:web'], function () {
+Route::group(['middleware' => 'auth:reverse-proxy,web'], function () {
     Route::put('user', 'Auth\UserController@update')->name('user.update');
     Route::patch('user/password', 'Auth\PasswordController@update')->name('user.password.update');
     Route::get('user/logout', 'Auth\LoginController@logout')->name('user.logout');