mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-06-20 11:47:53 +02:00
Allow PAT usage while useSsoOnly is enabled - Resolves #474
This commit is contained in:
parent
f0eec6582a
commit
12228bc536
@ -32,7 +32,8 @@ class Handler extends ExceptionHandler
|
|||||||
|
|
||||||
$this->renderable(function (\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException $exception, $request) {
|
$this->renderable(function (\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException $exception, $request) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'unauthorized',
|
'message' => 'forbidden',
|
||||||
|
'reason' => $exception->getMessage(),
|
||||||
], 403);
|
], 403);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
59
app/Http/Controllers/Auth/PersonalAccessTokenController.php
Normal file
59
app/Http/Controllers/Auth/PersonalAccessTokenController.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use Laravel\Passport\Http\Controllers\PersonalAccessTokenController as PassportPatController;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
|
||||||
|
class PersonalAccessTokenController extends PassportPatController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get all of the personal access tokens for the authenticated user.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection<int, \Laravel\Passport\Token>|\Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function forUser(Request $request)
|
||||||
|
{
|
||||||
|
if (Gate::denies('manage-pat')) {
|
||||||
|
throw new AccessDeniedHttpException(__('errors.unsupported_with_sso_only'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::forUser($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new personal access token for the user.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Laravel\Passport\PersonalAccessTokenResult|\Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
if (Gate::denies('manage-pat')) {
|
||||||
|
throw new AccessDeniedHttpException(__('errors.unsupported_with_sso_only'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::store($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the given token.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param string $tokenId
|
||||||
|
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, $tokenId)
|
||||||
|
{
|
||||||
|
if (Gate::denies('manage-pat')) {
|
||||||
|
throw new AccessDeniedHttpException(__('errors.unsupported_with_sso_only'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::destroy($request, $tokenId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -5,7 +5,9 @@ namespace App\Http\Controllers\Auth;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\WebauthnRenameRequest;
|
use App\Http\Requests\WebauthnRenameRequest;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
|
||||||
class WebAuthnManageController extends Controller
|
class WebAuthnManageController extends Controller
|
||||||
{
|
{
|
||||||
@ -16,6 +18,10 @@ class WebAuthnManageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
|
if (Gate::denies('manage-webauthn-credentials')) {
|
||||||
|
throw new AccessDeniedHttpException(__('errors.unsupported_with_sso_only'));
|
||||||
|
}
|
||||||
|
|
||||||
$allUserCredentials = $request->user()->webAuthnCredentials()->WhereEnabled()->get();
|
$allUserCredentials = $request->user()->webAuthnCredentials()->WhereEnabled()->get();
|
||||||
|
|
||||||
return response()->json($allUserCredentials, 200);
|
return response()->json($allUserCredentials, 200);
|
||||||
@ -46,6 +52,10 @@ class WebAuthnManageController extends Controller
|
|||||||
public function delete(Request $request, $credential)
|
public function delete(Request $request, $credential)
|
||||||
{
|
{
|
||||||
Log::info('Deletion of security device requested');
|
Log::info('Deletion of security device requested');
|
||||||
|
|
||||||
|
if (Gate::denies('manage-webauthn-credentials')) {
|
||||||
|
throw new AccessDeniedHttpException(__('errors.unsupported_with_sso_only'));
|
||||||
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$user->flushCredential($credential);
|
$user->flushCredential($credential);
|
||||||
|
@ -3,19 +3,19 @@
|
|||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\WebauthnAttestationRequest;
|
||||||
|
use App\Http\Requests\WebauthnAttestedRequest;
|
||||||
use Illuminate\Contracts\Support\Responsable;
|
use Illuminate\Contracts\Support\Responsable;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Laragear\WebAuthn\Enums\UserVerification;
|
use Laragear\WebAuthn\Enums\UserVerification;
|
||||||
use Laragear\WebAuthn\Http\Requests\AttestationRequest;
|
|
||||||
use Laragear\WebAuthn\Http\Requests\AttestedRequest;
|
|
||||||
|
|
||||||
class WebAuthnRegisterController extends Controller
|
class WebAuthnRegisterController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Returns a challenge to be verified by the user device.
|
* Returns a challenge to be verified by the user device.
|
||||||
*/
|
*/
|
||||||
public function options(AttestationRequest $request) : Responsable
|
public function options(WebauthnAttestationRequest $request) : Responsable
|
||||||
{
|
{
|
||||||
switch (config('webauthn.user_verification')) {
|
switch (config('webauthn.user_verification')) {
|
||||||
case UserVerification::DISCOURAGED:
|
case UserVerification::DISCOURAGED:
|
||||||
@ -35,7 +35,7 @@ class WebAuthnRegisterController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Registers a device for further WebAuthn authentication.
|
* Registers a device for further WebAuthn authentication.
|
||||||
*/
|
*/
|
||||||
public function register(AttestedRequest $request) : Response
|
public function register(WebauthnAttestedRequest $request) : Response
|
||||||
{
|
{
|
||||||
$request->save();
|
$request->save();
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ class SinglePageController extends Controller
|
|||||||
'disableRegistration',
|
'disableRegistration',
|
||||||
'enableSso',
|
'enableSso',
|
||||||
'useSsoOnly',
|
'useSsoOnly',
|
||||||
|
'allowPatWhileSsoOnly',
|
||||||
]);
|
]);
|
||||||
$settings = $appSettings->map(function (mixed $item, string $key) {
|
$settings = $appSettings->map(function (mixed $item, string $key) {
|
||||||
return null;
|
return null;
|
||||||
|
31
app/Http/Requests/WebauthnAttestationRequest.php
Normal file
31
app/Http/Requests/WebauthnAttestationRequest.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable;
|
||||||
|
use Laragear\WebAuthn\Http\Requests\AttestationRequest;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
|
||||||
|
class WebauthnAttestationRequest extends AttestationRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle a failed authorization attempt.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||||
|
*/
|
||||||
|
protected function failedAuthorization()
|
||||||
|
{
|
||||||
|
throw new AccessDeniedHttpException(__('errors.unsupported_with_sso_only'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(?WebAuthnAuthenticatable $user): bool
|
||||||
|
{
|
||||||
|
return (bool) $user && Gate::allows('manage-webauthn-credentials');
|
||||||
|
}
|
||||||
|
}
|
31
app/Http/Requests/WebauthnAttestedRequest.php
Normal file
31
app/Http/Requests/WebauthnAttestedRequest.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable;
|
||||||
|
use Laragear\WebAuthn\Http\Requests\AttestedRequest;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
|
||||||
|
class WebauthnAttestedRequest extends AttestedRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle a failed authorization attempt.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||||
|
*/
|
||||||
|
protected function failedAuthorization()
|
||||||
|
{
|
||||||
|
throw new AccessDeniedHttpException(__('errors.unsupported_with_sso_only'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(?WebAuthnAuthenticatable $user): bool
|
||||||
|
{
|
||||||
|
return (bool) $user && Gate::allows('manage-webauthn-credentials');
|
||||||
|
}
|
||||||
|
}
|
@ -4,9 +4,23 @@ namespace App\Http\Requests;
|
|||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
|
||||||
class WebauthnRenameRequest extends FormRequest
|
class WebauthnRenameRequest extends FormRequest
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Handle a failed authorization attempt.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||||
|
*/
|
||||||
|
protected function failedAuthorization()
|
||||||
|
{
|
||||||
|
throw new AccessDeniedHttpException(__('errors.unsupported_with_sso_only'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the user is authorized to make this request.
|
* Determine if the user is authorized to make this request.
|
||||||
*
|
*
|
||||||
@ -14,7 +28,7 @@ class WebauthnRenameRequest extends FormRequest
|
|||||||
*/
|
*/
|
||||||
public function authorize()
|
public function authorize()
|
||||||
{
|
{
|
||||||
return Auth::check();
|
return Auth::check() && Gate::allows('manage-webauthn-credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Facades\Settings;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Laravel\Passport\Console\ClientCommand;
|
use Laravel\Passport\Console\ClientCommand;
|
||||||
@ -39,5 +42,21 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
ClientCommand::class,
|
ClientCommand::class,
|
||||||
KeysCommand::class,
|
KeysCommand::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Gate::before(function (User $user, string $ability) {
|
||||||
|
if ($user->isAdministrator()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Gate::define('manage-pat', function (User $user) {
|
||||||
|
$useSsoOnly = Settings::get('useSsoOnly');
|
||||||
|
|
||||||
|
return ($useSsoOnly && Settings::get('allowPatWhileSsoOnly')) || $useSsoOnly !== true;
|
||||||
|
});
|
||||||
|
|
||||||
|
Gate::define('manage-webauthn-credentials', function (User $user) {
|
||||||
|
return ! Settings::get('useSsoOnly');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,6 +139,7 @@ return [
|
|||||||
'disableRegistration' => false,
|
'disableRegistration' => false,
|
||||||
'enableSso' => true,
|
'enableSso' => true,
|
||||||
'useSsoOnly' => false,
|
'useSsoOnly' => false,
|
||||||
|
'allowPatWhileSsoOnly' => false,
|
||||||
'restrictRegistration' => false,
|
'restrictRegistration' => false,
|
||||||
'restrictList' => '',
|
'restrictList' => '',
|
||||||
'restrictRule' => '',
|
'restrictRule' => '',
|
||||||
|
@ -80,6 +80,8 @@
|
|||||||
<FormCheckbox v-model="appSettings.enableSso" @update:model-value="val => useAppSettingsUpdater('enableSso', val)" fieldName="enableSso" label="admin.forms.enable_sso.label" help="admin.forms.enable_sso.help" />
|
<FormCheckbox v-model="appSettings.enableSso" @update:model-value="val => useAppSettingsUpdater('enableSso', val)" fieldName="enableSso" label="admin.forms.enable_sso.label" help="admin.forms.enable_sso.help" />
|
||||||
<!-- use SSO only -->
|
<!-- use SSO only -->
|
||||||
<FormCheckbox v-model="appSettings.useSsoOnly" @update:model-value="val => useAppSettingsUpdater('useSsoOnly', val)" fieldName="useSsoOnly" label="admin.forms.use_sso_only.label" help="admin.forms.use_sso_only.help" :isDisabled="!appSettings.enableSso" :isIndented="true" />
|
<FormCheckbox v-model="appSettings.useSsoOnly" @update:model-value="val => useAppSettingsUpdater('useSsoOnly', val)" fieldName="useSsoOnly" label="admin.forms.use_sso_only.label" help="admin.forms.use_sso_only.help" :isDisabled="!appSettings.enableSso" :isIndented="true" />
|
||||||
|
<!-- Allow Pat In SSO Only -->
|
||||||
|
<FormCheckbox v-model="appSettings.allowPatWhileSsoOnly" @update:model-value="val => useAppSettingsUpdater('allowPatWhileSsoOnly', val)" fieldName="allowPatWhileSsoOnly" label="admin.forms.allow_pat_in_sso_only.label" help="admin.forms.allow_pat_in_sso_only.help" :isDisabled="!appSettings.useSsoOnly" :isIndented="true" />
|
||||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('admin.registrations') }}</h4>
|
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('admin.registrations') }}</h4>
|
||||||
<!-- restrict registration -->
|
<!-- restrict registration -->
|
||||||
<FormCheckbox v-model="appSettings.restrictRegistration" @update:model-value="val => useAppSettingsUpdater('restrictRegistration', val)" fieldName="restrictRegistration" :isDisabled="appSettings.disableRegistration" label="admin.forms.restrict_registration.label" help="admin.forms.restrict_registration.help" />
|
<FormCheckbox v-model="appSettings.restrictRegistration" @update:model-value="val => useAppSettingsUpdater('restrictRegistration', val)" fieldName="restrictRegistration" :isDisabled="appSettings.disableRegistration" label="admin.forms.restrict_registration.label" help="admin.forms.restrict_registration.help" />
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
import Form from '@/components/formElements/Form'
|
import Form from '@/components/formElements/Form'
|
||||||
import userService from '@/services/userService'
|
import userService from '@/services/userService'
|
||||||
import SettingTabs from '@/layouts/SettingTabs.vue'
|
import SettingTabs from '@/layouts/SettingTabs.vue'
|
||||||
|
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||||
import { useNotifyStore } from '@/stores/notify'
|
import { useNotifyStore } from '@/stores/notify'
|
||||||
import { UseColorMode } from '@vueuse/components'
|
import { UseColorMode } from '@vueuse/components'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import Spinner from '@/components/Spinner.vue'
|
import Spinner from '@/components/Spinner.vue'
|
||||||
|
|
||||||
|
const appSettings = useAppSettingsStore()
|
||||||
const $2fauth = inject('2fauth')
|
const $2fauth = inject('2fauth')
|
||||||
const notify = useNotifyStore()
|
const notify = useNotifyStore()
|
||||||
const user = useUserStore()
|
const user = useUserStore()
|
||||||
@ -20,7 +22,7 @@
|
|||||||
const visibleTokenId = ref(null)
|
const visibleTokenId = ref(null)
|
||||||
|
|
||||||
const isDisabled = computed(() => {
|
const isDisabled = computed(() => {
|
||||||
return (appSettings.enableSso && appSettings.useSsoOnly) || user.authenticated_by_proxy
|
return (appSettings.enableSso && appSettings.useSsoOnly && ! appSettings.allowPatWhileSsoOnly) || user.authenticated_by_proxy
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -51,8 +53,8 @@
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if( error.response.status === 405 ) {
|
if( error.response.status === 403 ) {
|
||||||
// The backend returns a 405 response if the user is authenticated by a reverse proxy
|
// The backend returns a 403 response if the user is authenticated by a reverse proxy
|
||||||
// or if SSO only is enabled.
|
// or if SSO only is enabled.
|
||||||
// The form is already disabled (see isDisabled) so we do nothing more here
|
// The form is already disabled (see isDisabled) so we do nothing more here
|
||||||
}
|
}
|
||||||
|
@ -98,8 +98,8 @@
|
|||||||
credentials.value = response.data
|
credentials.value = response.data
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if( error.response.status === 405 ) {
|
if( error.response.status === 403 ) {
|
||||||
// The backend returns a 405 response if the user is authenticated by a reverse proxy
|
// The backend returns a 403 response if the user is authenticated by a reverse proxy
|
||||||
// or if SSO only is enabled.
|
// or if SSO only is enabled.
|
||||||
// The form is already disabled (see isDisabled) so we do nothing more here
|
// The form is already disabled (see isDisabled) so we do nothing more here
|
||||||
}
|
}
|
||||||
|
@ -118,6 +118,10 @@ return [
|
|||||||
'label' => 'Use SSO only',
|
'label' => 'Use SSO only',
|
||||||
'help' => 'Make SSO the only available method to log in to 2FAuth. Password login and Webauthn are then disabled for regular users. Administrators are not affected by this restriction.',
|
'help' => 'Make SSO the only available method to log in to 2FAuth. Password login and Webauthn are then disabled for regular users. Administrators are not affected by this restriction.',
|
||||||
],
|
],
|
||||||
|
'allow_pat_in_sso_only' => [
|
||||||
|
'label' => 'Allow PAT usage',
|
||||||
|
'help' => 'Let users create personal access tokens and use them to authenticate with third party application like the 2FAuth web extension',
|
||||||
|
],
|
||||||
'keep_sso_registration_enabled' => [
|
'keep_sso_registration_enabled' => [
|
||||||
'label' => 'Keep SSO registration enabled',
|
'label' => 'Keep SSO registration enabled',
|
||||||
'help' => 'Allow new users to sign in for the first time via SSO whereas registration is disabled',
|
'help' => 'Allow new users to sign in for the first time via SSO whereas registration is disabled',
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
use App\Http\Controllers\Auth\ForgotPasswordController;
|
use App\Http\Controllers\Auth\ForgotPasswordController;
|
||||||
use App\Http\Controllers\Auth\LoginController;
|
use App\Http\Controllers\Auth\LoginController;
|
||||||
use App\Http\Controllers\Auth\PasswordController;
|
use App\Http\Controllers\Auth\PasswordController;
|
||||||
|
use App\Http\Controllers\Auth\PersonalAccessTokenController;
|
||||||
use App\Http\Controllers\Auth\RegisterController;
|
use App\Http\Controllers\Auth\RegisterController;
|
||||||
use App\Http\Controllers\Auth\ResetPasswordController;
|
use App\Http\Controllers\Auth\ResetPasswordController;
|
||||||
use App\Http\Controllers\Auth\SocialiteController;
|
use App\Http\Controllers\Auth\SocialiteController;
|
||||||
@ -23,7 +24,6 @@ use Illuminate\Session\Middleware\StartSession;
|
|||||||
// use Illuminate\Foundation\Events\DiagnosingHealth;
|
// use Illuminate\Foundation\Events\DiagnosingHealth;
|
||||||
// use Illuminate\Support\Facades\Event;
|
// use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Laravel\Passport\Http\Controllers\PersonalAccessTokenController;
|
|
||||||
|
|
||||||
// use App\Models\User;
|
// use App\Models\User;
|
||||||
// use App\Notifications\SignedInWithNewDeviceNotification;
|
// use App\Notifications\SignedInWithNewDeviceNotification;
|
||||||
@ -67,7 +67,7 @@ Route::group(['middleware' => ['forceLogout', 'throttle:10,1']], function () {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes protected by an authentication guard but rejected when the reverse-proxy
|
* Routes protected by an authentication guard but rejected when the reverse-proxy
|
||||||
* guard is enabled or SSO only is enabled
|
* guard is enabled
|
||||||
*/
|
*/
|
||||||
Route::group(['middleware' => ['behind-auth', 'rejectIfReverseProxy']], function () {
|
Route::group(['middleware' => ['behind-auth', 'rejectIfReverseProxy']], function () {
|
||||||
Route::put('user', [UserController::class, 'update'])->name('user.update');
|
Route::put('user', [UserController::class, 'update'])->name('user.update');
|
||||||
@ -75,15 +75,16 @@ Route::group(['middleware' => ['behind-auth', 'rejectIfReverseProxy']], function
|
|||||||
Route::get('user/logout', [LoginController::class, 'logout'])->name('user.logout');
|
Route::get('user/logout', [LoginController::class, 'logout'])->name('user.logout');
|
||||||
Route::delete('user', [UserController::class, 'delete'])->name('user.delete')->middleware('rejectIfDemoMode');
|
Route::delete('user', [UserController::class, 'delete'])->name('user.delete')->middleware('rejectIfDemoMode');
|
||||||
|
|
||||||
Route::get('oauth/personal-access-tokens', [PersonalAccessTokenController::class, 'forUser'])->name('passport.personal.tokens.index')->middleware('RejectIfSsoOnlyAndNotForAdmin');
|
// Following routes are also forbidden to regular users when "SSO only" is enabled, but using Authorization gates
|
||||||
Route::post('oauth/personal-access-tokens', [PersonalAccessTokenController::class, 'store'])->name('passport.personal.tokens.store')->middleware('RejectIfSsoOnlyAndNotForAdmin');
|
Route::get('oauth/personal-access-tokens', [PersonalAccessTokenController::class, 'forUser'])->name('passport.personal.tokens.index');
|
||||||
Route::delete('oauth/personal-access-tokens/{token_id}', [PersonalAccessTokenController::class, 'destroy'])->name('passport.personal.tokens.destroy')->middleware('RejectIfSsoOnlyAndNotForAdmin');
|
Route::post('oauth/personal-access-tokens', [PersonalAccessTokenController::class, 'store'])->name('passport.personal.tokens.store');
|
||||||
|
Route::delete('oauth/personal-access-tokens/{token_id}', [PersonalAccessTokenController::class, 'destroy'])->name('passport.personal.tokens.destroy');
|
||||||
|
|
||||||
Route::post('webauthn/register/options', [WebAuthnRegisterController::class, 'options'])->name('webauthn.register.options')->middleware('RejectIfSsoOnlyAndNotForAdmin');
|
Route::post('webauthn/register/options', [WebAuthnRegisterController::class, 'options'])->name('webauthn.register.options');
|
||||||
Route::post('webauthn/register', [WebAuthnRegisterController::class, 'register'])->name('webauthn.register')->middleware('RejectIfSsoOnlyAndNotForAdmin');
|
Route::post('webauthn/register', [WebAuthnRegisterController::class, 'register'])->name('webauthn.register');
|
||||||
Route::get('webauthn/credentials', [WebAuthnManageController::class, 'index'])->name('webauthn.credentials.index')->middleware('RejectIfSsoOnlyAndNotForAdmin');
|
Route::get('webauthn/credentials', [WebAuthnManageController::class, 'index'])->name('webauthn.credentials.index');
|
||||||
Route::patch('webauthn/credentials/{credential}/name', [WebAuthnManageController::class, 'rename'])->name('webauthn.credentials.rename')->middleware('RejectIfSsoOnlyAndNotForAdmin');
|
Route::patch('webauthn/credentials/{credential}/name', [WebAuthnManageController::class, 'rename'])->name('webauthn.credentials.rename');
|
||||||
Route::delete('webauthn/credentials/{credential}', [WebAuthnManageController::class, 'delete'])->name('webauthn.credentials.delete')->middleware('RejectIfSsoOnlyAndNotForAdmin');
|
Route::delete('webauthn/credentials/{credential}', [WebAuthnManageController::class, 'delete'])->name('webauthn.credentials.delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -133,74 +133,4 @@ class RejectIfSsoOnlyAndNotForAdminMiddlewareTest extends FeatureTestCase
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
|
||||||
#[DataProvider('provideProtectedEndPoints')]
|
|
||||||
public function test_protected_endpoint_are_allowed_if_requested_by_an_admin(string $method, string $url)
|
|
||||||
{
|
|
||||||
$expectedResponseCodes = [
|
|
||||||
Response::HTTP_OK,
|
|
||||||
Response::HTTP_UNPROCESSABLE_ENTITY,
|
|
||||||
Response::HTTP_NOT_FOUND,
|
|
||||||
Response::HTTP_CREATED,
|
|
||||||
Response::HTTP_NO_CONTENT,
|
|
||||||
];
|
|
||||||
|
|
||||||
$response = $this->actingAs($this->admin, 'web-guard')
|
|
||||||
->json($method, $url, [
|
|
||||||
'email' => $this->admin->email,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertContains($response->getStatusCode(), $expectedResponseCodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
#[DataProvider('provideProtectedEndPoints')]
|
|
||||||
public function test_protected_endpoint_returns_NOT_ALLOWED_if_requested_by_regular_user(string $method, string $url)
|
|
||||||
{
|
|
||||||
$this->actingAs($this->user, 'web-guard')
|
|
||||||
->json($method, $url)
|
|
||||||
->assertMethodNotAllowed();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide Valid data for validation test
|
|
||||||
*/
|
|
||||||
public static function provideProtectedEndPoints() : array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'WEBAUTHN_REGISTER' => [
|
|
||||||
'method' => 'POST',
|
|
||||||
'url' => '/webauthn/register',
|
|
||||||
],
|
|
||||||
'WEBAUTHN_REGISTER_OPTIONS' => [
|
|
||||||
'method' => 'POST',
|
|
||||||
'url' => '/webauthn/register/options',
|
|
||||||
],
|
|
||||||
'WEBAUTHN_CREDENTIALS_ALL' => [
|
|
||||||
'method' => 'GET',
|
|
||||||
'url' => '/webauthn/credentials',
|
|
||||||
],
|
|
||||||
'WEBAUTHN_CREDENTIALS_PATCH' => [
|
|
||||||
'method' => 'PATCH',
|
|
||||||
'url' => '/webauthn/credentials/FAKE_CREDENTIAL_ID/name',
|
|
||||||
],
|
|
||||||
'WEBAUTHN_CREDENTIALS_DELETE' => [
|
|
||||||
'method' => 'DELETE',
|
|
||||||
'url' => '/webauthn/credentials/FAKE_CREDENTIAL_ID',
|
|
||||||
],
|
|
||||||
'OAUTH_PAT_ALL' => [
|
|
||||||
'method' => 'GET',
|
|
||||||
'url' => '/oauth/personal-access-tokens',
|
|
||||||
],
|
|
||||||
'OAUTH_PAT_STORE' => [
|
|
||||||
'method' => 'POST',
|
|
||||||
'url' => '/oauth/personal-access-tokens',
|
|
||||||
],
|
|
||||||
'OAUTH_PAT_DELETE' => [
|
|
||||||
'method' => 'DELETE',
|
|
||||||
'url' => '/oauth/personal-access-tokens/FAKE_TOKEN_ID',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
142
tests/Feature/Permissions/ManagePatPermissionsTest.php
Normal file
142
tests/Feature/Permissions/ManagePatPermissionsTest.php
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Http\Middlewares;
|
||||||
|
|
||||||
|
use App\Facades\Settings;
|
||||||
|
use App\Http\Controllers\Auth\PersonalAccessTokenController;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\FeatureTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ManagePatPermissionsTest test class
|
||||||
|
*/
|
||||||
|
#[CoversClass(PersonalAccessTokenController::class)]
|
||||||
|
#[CoversMethod(AppServiceProvider::class, 'boot')]
|
||||||
|
class ManagePatPermissionsTest extends FeatureTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||||
|
*/
|
||||||
|
protected $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||||
|
*/
|
||||||
|
protected $admin;
|
||||||
|
|
||||||
|
private const PASSWORD = 'password';
|
||||||
|
|
||||||
|
public function setUp() : void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
$this->admin = User::factory()->administrator()->create([
|
||||||
|
'password' => self::PASSWORD,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('providePatManagementEndPoints')]
|
||||||
|
public function test_pat_management_endpoint_is_permitted_to_regular_user_without_useSsoOnly(string $method, string $url)
|
||||||
|
{
|
||||||
|
Settings::set('useSsoOnly', false);
|
||||||
|
Settings::set('allowPatWhileSsoOnly', false);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->user, 'web-guard')
|
||||||
|
->json($method, $url);
|
||||||
|
|
||||||
|
$this->assertNotEquals($response->getStatusCode(), Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('providePatManagementEndPoints')]
|
||||||
|
public function test_pat_management_endpoint_is_forbidden_to_regular_user_with_useSsoOnly(string $method, string $url)
|
||||||
|
{
|
||||||
|
Settings::set('useSsoOnly', true);
|
||||||
|
Settings::set('allowPatWhileSsoOnly', false);
|
||||||
|
|
||||||
|
$this->actingAs($this->user, 'web-guard')
|
||||||
|
->json($method, $url)
|
||||||
|
->assertForbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('providePatManagementEndPoints')]
|
||||||
|
public function test_pat_management_endpoint_is_permitted_to_regular_user_with_useSsoOnly_bypassed(string $method, string $url)
|
||||||
|
{
|
||||||
|
Settings::set('useSsoOnly', true);
|
||||||
|
Settings::set('allowPatWhileSsoOnly', true);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->user, 'web-guard')
|
||||||
|
->json($method, $url);
|
||||||
|
|
||||||
|
$this->assertNotEquals($response->getStatusCode(), Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('providePatManagementEndPoints')]
|
||||||
|
public function test_pat_management_endpoint_is_permitted_to_admin_without_useSsoOnly(string $method, string $url)
|
||||||
|
{
|
||||||
|
Settings::set('useSsoOnly', false);
|
||||||
|
Settings::set('allowPatWhileSsoOnly', false);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->admin, 'web-guard')
|
||||||
|
->json($method, $url);
|
||||||
|
|
||||||
|
$this->assertNotEquals($response->getStatusCode(), Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('providePatManagementEndPoints')]
|
||||||
|
public function test_pat_management_endpoint_is_permitted_to_admin_with_useSsoOnly(string $method, string $url)
|
||||||
|
{
|
||||||
|
Settings::set('useSsoOnly', true);
|
||||||
|
Settings::set('allowPatWhileSsoOnly', false);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->admin, 'web-guard')
|
||||||
|
->json($method, $url);
|
||||||
|
|
||||||
|
$this->assertNotEquals($response->getStatusCode(), Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('providePatManagementEndPoints')]
|
||||||
|
public function test_pat_management_endpoint_is_permitted_to_admin_with_useSsoOnly_bypassed(string $method, string $url)
|
||||||
|
{
|
||||||
|
Settings::set('useSsoOnly', true);
|
||||||
|
Settings::set('allowPatWhileSsoOnly', true);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->admin, 'web-guard')
|
||||||
|
->json($method, $url);
|
||||||
|
|
||||||
|
$this->assertNotEquals($response->getStatusCode(), Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide Valid data for validation test
|
||||||
|
*/
|
||||||
|
public static function providePatManagementEndPoints() : array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'OAUTH_PAT_ALL' => [
|
||||||
|
'method' => 'GET',
|
||||||
|
'url' => '/oauth/personal-access-tokens',
|
||||||
|
],
|
||||||
|
'OAUTH_PAT_STORE' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'url' => '/oauth/personal-access-tokens',
|
||||||
|
],
|
||||||
|
'OAUTH_PAT_DELETE' => [
|
||||||
|
'method' => 'DELETE',
|
||||||
|
'url' => '/oauth/personal-access-tokens/FAKE_TOKEN_ID',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
122
tests/Feature/Permissions/ManageWebauthnPermissionsTest.php
Normal file
122
tests/Feature/Permissions/ManageWebauthnPermissionsTest.php
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Http\Middlewares;
|
||||||
|
|
||||||
|
use App\Facades\Settings;
|
||||||
|
use App\Http\Controllers\Auth\WebAuthnManageController;
|
||||||
|
use App\Http\Controllers\Auth\WebAuthnRegisterController;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\FeatureTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ManagePatPermissionsTest test class
|
||||||
|
*/
|
||||||
|
#[CoversClass(WebAuthnManageController::class)]
|
||||||
|
#[CoversClass(WebAuthnRegisterController::class)]
|
||||||
|
#[CoversMethod(AppServiceProvider::class, 'boot')]
|
||||||
|
class ManageWebauthnPermissionsTest extends FeatureTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||||
|
*/
|
||||||
|
protected $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||||
|
*/
|
||||||
|
protected $admin;
|
||||||
|
|
||||||
|
private const PASSWORD = 'password';
|
||||||
|
|
||||||
|
public function setUp() : void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
$this->admin = User::factory()->administrator()->create([
|
||||||
|
'password' => self::PASSWORD,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('provideWebauthnManagementEndPoints')]
|
||||||
|
public function test_webauthn_management_endpoint_is_permitted_to_regular_user_without_useSsoOnly(string $method, string $url)
|
||||||
|
{
|
||||||
|
Settings::set('useSsoOnly', false);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->user, 'web-guard')
|
||||||
|
->json($method, $url);
|
||||||
|
|
||||||
|
$this->assertNotEquals($response->getStatusCode(), Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('provideWebauthnManagementEndPoints')]
|
||||||
|
public function test_webauthn_management_endpoint_is_forbidden_to_regular_user_with_useSsoOnly(string $method, string $url)
|
||||||
|
{
|
||||||
|
Settings::set('useSsoOnly', true);
|
||||||
|
|
||||||
|
$this->actingAs($this->user, 'web-guard')
|
||||||
|
->json($method, $url)
|
||||||
|
->assertForbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('provideWebauthnManagementEndPoints')]
|
||||||
|
public function test_webauthn_management_endpoint_is_permitted_to_admin_without_useSsoOnly(string $method, string $url)
|
||||||
|
{
|
||||||
|
Settings::set('useSsoOnly', false);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->admin, 'web-guard')
|
||||||
|
->json($method, $url);
|
||||||
|
|
||||||
|
$this->assertNotEquals($response->getStatusCode(), Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('provideWebauthnManagementEndPoints')]
|
||||||
|
public function test_webauthn_management_endpoint_is_permitted_to_admin_with_useSsoOnly(string $method, string $url)
|
||||||
|
{
|
||||||
|
Settings::set('useSsoOnly', true);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->admin, 'web-guard')
|
||||||
|
->json($method, $url);
|
||||||
|
|
||||||
|
$this->assertNotEquals($response->getStatusCode(), Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide Valid data for validation test
|
||||||
|
*/
|
||||||
|
public static function provideWebauthnManagementEndPoints() : array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'WEBAUTHN_REGISTER_OPTIONS' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'url' => '/webauthn/register/options',
|
||||||
|
],
|
||||||
|
'WEBAUTHN_REGISTER' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'url' => '/webauthn/register',
|
||||||
|
],
|
||||||
|
'WEBAUTHN_CREDENTIALS_ALL' => [
|
||||||
|
'method' => 'GET',
|
||||||
|
'url' => '/webauthn/credentials',
|
||||||
|
],
|
||||||
|
'WEBAUTHN_CREDENTIALS_PATCH' => [
|
||||||
|
'method' => 'PATCH',
|
||||||
|
'url' => '/webauthn/credentials/FAKE_CREDENTIAL_ID/name',
|
||||||
|
],
|
||||||
|
'WEBAUTHN_CREDENTIALS_DELETE' => [
|
||||||
|
'method' => 'DELETE',
|
||||||
|
'url' => '/webauthn/credentials/FAKE_CREDENTIAL_ID',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user