mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-02-16 10:29:16 +01:00
Complete SSO (user model, error cases, tests, views) & Add github provider
This commit is contained in:
parent
717bef16f7
commit
9ff35195f0
@ -221,7 +221,10 @@ WEBAUTHN_ID=null
|
||||
|
||||
WEBAUTHN_USER_VERIFICATION=preferred
|
||||
|
||||
### OpenID settings ###
|
||||
|
||||
#### SSO settings (for Socialite) ####
|
||||
|
||||
# Uncomment lines for the OAuth providers you need.
|
||||
|
||||
# OPENID_AUTHORIZE_URL=
|
||||
# OPENID_TOKEN_URL=
|
||||
@ -229,6 +232,9 @@ WEBAUTHN_USER_VERIFICATION=preferred
|
||||
# OPENID_CLIENT_ID=
|
||||
# OPENID_CLIENT_SECRET=
|
||||
|
||||
# GITHUB_CLIENT_ID=
|
||||
# GITHUB_CLIENT_SECRET=
|
||||
|
||||
|
||||
# Use this setting to declare trusted proxied.
|
||||
# Supported:
|
||||
|
@ -8,6 +8,7 @@
|
||||
* @property mixed $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property string $oauth_provider
|
||||
* @property \Illuminate\Support\Collection<array-key, mixed> $preferences
|
||||
* @property string $is_admin
|
||||
*/
|
||||
@ -22,11 +23,12 @@ class UserResource extends JsonResource
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
'preferences' => $this->preferences,
|
||||
'is_admin' => $this->is_admin,
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
'oauth_provider' => $this->oauth_provider,
|
||||
'preferences' => $this->preferences,
|
||||
'is_admin' => $this->is_admin,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,15 @@ class PasswordController extends Controller
|
||||
*/
|
||||
public function update(UserPatchPwdRequest $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
$validated = $request->validated();
|
||||
|
||||
if (config('auth.defaults.guard') === 'reverse-proxy-guard' || $user->oauth_provider) {
|
||||
Log::notice('Password update rejected: reverse-proxy-guard enabled or account from external sso provider');
|
||||
|
||||
return response()->json(['message' => __('errors.account_managed_by_external_provider')], 400);
|
||||
}
|
||||
|
||||
if (! Hash::check($validated['currentPassword'], Auth::user()->password)) {
|
||||
Log::notice('Password update failed: wrong password provided');
|
||||
|
||||
@ -26,10 +33,10 @@ public function update(UserPatchPwdRequest $request)
|
||||
}
|
||||
|
||||
if (! config('2fauth.config.isDemoApp')) {
|
||||
$request->user()->update([
|
||||
$user->update([
|
||||
'password' => bcrypt($validated['password']),
|
||||
]);
|
||||
Log::info(sprintf('Password of user ID #%s updated', $request->user()->id));
|
||||
Log::info(sprintf('Password of user ID #%s updated', $user->id));
|
||||
}
|
||||
|
||||
return response()->json(['message' => __('auth.forms.password_successfully_changed')]);
|
||||
|
@ -13,32 +13,58 @@
|
||||
|
||||
class SocialiteController extends Controller
|
||||
{
|
||||
public function redirect(Request $request, $driver)
|
||||
/**
|
||||
* Redirect to the provider's authentication url
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function redirect(Request $request, string $driver)
|
||||
{
|
||||
return Socialite::driver($driver)->redirect();
|
||||
}
|
||||
|
||||
public function callback(Request $request, $driver)
|
||||
{
|
||||
$socialiteUser = Socialite::driver($driver)->user();
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::firstOrNew([
|
||||
'email' => $socialiteUser->getEmail(),
|
||||
], [
|
||||
'name' => $socialiteUser->getName(),
|
||||
'password' => bcrypt(Str::random()),
|
||||
]);
|
||||
|
||||
if (!$user->exists && Settings::get('disableRegistrationSso')) {
|
||||
return response(401);
|
||||
if (! config('services.' . $driver . '.client_id') || ! config('services.' . $driver . '.client_secret')) {
|
||||
return redirect('/error?err=sso_bad_provider_setup');
|
||||
}
|
||||
|
||||
return Settings::get('enableSso')
|
||||
? Socialite::driver($driver)->redirect()
|
||||
: redirect('/error?err=sso_disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register (if needed) the user and authenticate him
|
||||
*
|
||||
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function callback(Request $request, string $driver)
|
||||
{
|
||||
try {
|
||||
$socialiteUser = Socialite::driver($driver)->user();
|
||||
} catch (\Exception $e) {
|
||||
return redirect('/error?err=sso_failed');
|
||||
}
|
||||
|
||||
/** @var User|null $user */
|
||||
$user = User::firstOrNew([
|
||||
'oauth_id' => $socialiteUser->getId(),
|
||||
'oauth_provider' => $driver,
|
||||
]);
|
||||
|
||||
if (! $user->exists) {
|
||||
if (User::count() === 0) {
|
||||
$user->is_admin = true;
|
||||
}
|
||||
else if (Settings::get('disableRegistration')) {
|
||||
return redirect('/error?err=no_register');
|
||||
}
|
||||
$user->password = bcrypt(Str::random());
|
||||
}
|
||||
|
||||
$user->email = $socialiteUser->getEmail() ?? $socialiteUser->getId() . '@' . $driver;
|
||||
$user->name = $socialiteUser->getNickname() ?? $socialiteUser->getName() ?? $driver . ' #' . $socialiteUser->getId();
|
||||
$user->last_seen_at = Carbon::now()->format('Y-m-d H:i:s');
|
||||
$user->save();
|
||||
|
||||
Auth::guard()->login($user, true);
|
||||
Auth::guard()->login($user);
|
||||
|
||||
return redirect('/accounts?authenticated');
|
||||
return redirect('/accounts');
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,12 @@ public function update(UserUpdateRequest $request)
|
||||
$user = $request->user();
|
||||
$validated = $request->validated();
|
||||
|
||||
if (config('auth.defaults.guard') === 'reverse-proxy-guard' || $user->oauth_provider) {
|
||||
Log::notice('Account update rejected: reverse-proxy-guard enabled or account from external sso provider');
|
||||
|
||||
return response()->json(['message' => __('errors.account_managed_by_external_provider')], 400);
|
||||
}
|
||||
|
||||
if (! Hash::check($request->password, Auth::user()->password)) {
|
||||
Log::notice('Account update failed: wrong password provided');
|
||||
|
||||
|
@ -27,7 +27,8 @@ public function index()
|
||||
$isTestingApp = config('2fauth.config.isTestingApp') ? 'true' : 'false';
|
||||
$lang = App::getLocale();
|
||||
$locales = collect(config('2fauth.locales'))->toJson(); /** @phpstan-ignore-line */
|
||||
$openidAuth = config('services.openid.client_secret') ? true : false;
|
||||
$openidAuth = config('services.openid.client_secret') ? true : false;
|
||||
$githubAuth = config('services.github.client_secret') ? true : false;
|
||||
|
||||
// if (Auth::user()->preferences)
|
||||
|
||||
@ -36,7 +37,10 @@ public function index()
|
||||
'appConfig' => collect([
|
||||
'proxyAuth' => $proxyAuth,
|
||||
'proxyLogoutUrl' => $proxyLogoutUrl,
|
||||
'openidAuth' => $openidAuth,
|
||||
'sso' => [
|
||||
'openid' => $openidAuth,
|
||||
'github' => $githubAuth,
|
||||
],
|
||||
'subdirectory' => $subdir,
|
||||
])->toJson(),
|
||||
'defaultPreferences' => $defaultPreferences,
|
||||
|
29
app/Listeners/RegisterOpenId.php
Normal file
29
app/Listeners/RegisterOpenId.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Providers\Socialite\OpenId;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
class RegisterOpenId
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(SocialiteWasCalled $socialiteWasCalled)
|
||||
{
|
||||
$socialiteWasCalled->extendSocialite('openid', OpenId::class);
|
||||
}
|
||||
}
|
@ -49,7 +49,7 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name', 'email', 'password',
|
||||
'name', 'email', 'password', 'oauth_id', 'oauth_provider'
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -10,7 +10,7 @@
|
||||
use App\Listeners\DissociateTwofaccountFromGroup;
|
||||
use App\Listeners\ReleaseRadar;
|
||||
use App\Listeners\ResetUsersPreference;
|
||||
use App\Providers\Socialite\RegisterOpenId;
|
||||
use App\Listeners\RegisterOpenId;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
@ -56,9 +56,11 @@ protected function getUserByToken($token)
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* Get a refresh token
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
protected function refreshToken($refreshToken)
|
||||
protected function refreshToken(string $refreshToken)
|
||||
{
|
||||
return $this->getHttpClient()->post($this->getTokenUrl(), [
|
||||
RequestOptions::FORM_PARAMS => [
|
||||
|
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Socialite;
|
||||
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
class RegisterOpenId
|
||||
{
|
||||
public function __invoke(SocialiteWasCalled $socialiteWasCalled)
|
||||
{
|
||||
$socialiteWasCalled->extendSocialite('openid', OpenId::class);
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@
|
||||
'lastRadarScan' => 0,
|
||||
'latestRelease' => false,
|
||||
'disableRegistration' => false,
|
||||
'disableRegistrationSso' => false,
|
||||
'enableSso' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|
@ -159,7 +159,7 @@
|
||||
/*
|
||||
* Package Service Providers...
|
||||
*/
|
||||
|
||||
\SocialiteProviders\Manager\ServiceProvider::class,
|
||||
/*
|
||||
* Application Service Providers...
|
||||
*/
|
||||
@ -170,7 +170,7 @@
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\TwoFAuthServiceProvider::class,
|
||||
App\Providers\MigrationServiceProvider::class,
|
||||
])->toArray(),
|
||||
])->toArray(),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -21,6 +21,27 @@
|
||||
'scheme' => 'https',
|
||||
],
|
||||
|
||||
'openid' => [
|
||||
'token_url' => env('OPENID_TOKEN_URL'),
|
||||
'authorize_url' => env('OPENID_AUTHORIZE_URL'),
|
||||
'userinfo_url' => env('OPENID_USERINFO_URL'),
|
||||
'client_id' => env('OPENID_CLIENT_ID'),
|
||||
'client_secret' => env('OPENID_CLIENT_SECRET'),
|
||||
'redirect' => '/socialite/callback/openid',
|
||||
],
|
||||
|
||||
'github' => [
|
||||
'client_id' => env('GITHUB_CLIENT_ID'),
|
||||
'client_secret' => env('GITHUB_CLIENT_SECRET'),
|
||||
'redirect' => '/socialite/callback/github',
|
||||
],
|
||||
|
||||
// 'google' => [
|
||||
// 'client_id' => env('GOOGLE_CLIENT_ID'),
|
||||
// 'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
||||
// 'redirect' => '/socialite/callback/google ',
|
||||
// ],
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'token_url' => env('OPENID_TOKEN_URL'),
|
||||
'authorize_url' => env('OPENID_AUTHORIZE_URL'),
|
||||
'userinfo_url' => env('OPENID_USERINFO_URL'),
|
||||
|
||||
'client_id' => env('OPENID_CLIENT_ID'),
|
||||
'client_secret' => env('OPENID_CLIENT_SECRET'),
|
||||
'redirect' => '/socialite/callback/openid',
|
||||
];
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('oauth_provider', 100)
|
||||
->after('id')
|
||||
->nullable();
|
||||
$table->string('oauth_id', 200)
|
||||
->after('id')
|
||||
->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('oauth_id');
|
||||
$table->dropColumn('oauth_provider');
|
||||
});
|
||||
}
|
||||
};
|
4
resources/js/icons.js
vendored
4
resources/js/icons.js
vendored
@ -53,7 +53,8 @@ import {
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
import {
|
||||
faGithubAlt
|
||||
faGithubAlt,
|
||||
faOpenid
|
||||
} from '@fortawesome/free-brands-svg-icons'
|
||||
|
||||
library.add(
|
||||
@ -103,6 +104,7 @@ library.add(
|
||||
faVideoSlash,
|
||||
faStar,
|
||||
faChevronRight,
|
||||
faOpenid,
|
||||
);
|
||||
|
||||
export default FontAwesomeIcon
|
1
resources/js/router/middlewares/authGuard.js
vendored
1
resources/js/router/middlewares/authGuard.js
vendored
@ -11,6 +11,7 @@ export default async function authGuard({ to, next, nextMiddleware, stores }) {
|
||||
await user.loginAs({
|
||||
name: currentUser.name,
|
||||
email: currentUser.email,
|
||||
oauth_provider: currentUser.oauth_provider,
|
||||
preferences: currentUser.preferences,
|
||||
isAdmin: currentUser.is_admin,
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
export default function noEmptyError({ to, next, nextMiddleware, stores }) {
|
||||
const { notify } = stores
|
||||
|
||||
if (notify.err == null) {
|
||||
if (notify.err == null && ! to.query.err) {
|
||||
// return to home if no err object is set to prevent an empty error message
|
||||
next({ name: 'accounts' });
|
||||
}
|
||||
|
1
resources/js/stores/user.js
vendored
1
resources/js/stores/user.js
vendored
@ -13,6 +13,7 @@ export const useUserStore = defineStore({
|
||||
return {
|
||||
name: undefined,
|
||||
email: undefined,
|
||||
oauth_provider: undefined,
|
||||
preferences: window.defaultPreferences,
|
||||
isAdmin: false,
|
||||
}
|
||||
|
@ -21,11 +21,17 @@
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.err) {
|
||||
errorHandler.message = trans('errors.' + route.query.err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Exits the error view
|
||||
*/
|
||||
function exit() {
|
||||
window.history.length > 1 && route.name !== '404' && route.name !== 'notFound'
|
||||
window.history.length > 1 && route.name !== '404' && route.name !== 'notFound' && !route.query.err
|
||||
? router.go(-1)
|
||||
: router.push({ name: 'accounts' })
|
||||
}
|
||||
|
@ -35,6 +35,7 @@
|
||||
await user.loginAs({
|
||||
name: response.data.name,
|
||||
email: response.data.email,
|
||||
oauth_provider: response.data.oauth_provider,
|
||||
preferences: response.data.preferences,
|
||||
isAdmin: response.data.is_admin,
|
||||
})
|
||||
@ -63,6 +64,7 @@
|
||||
await user.loginAs({
|
||||
name: response.data.name,
|
||||
email: response.data.email,
|
||||
oauth_provider: response.data.oauth_provider,
|
||||
preferences: response.data.preferences,
|
||||
isAdmin: response.data.is_admin,
|
||||
})
|
||||
@ -115,17 +117,25 @@
|
||||
{{ $t('auth.login_and_password') }}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="appSettings.openidAuth">{{ $t('auth.sign_in_using') }}
|
||||
<a id="lnkSignWithOpenID" class="is-link" href="/socialite/redirect/openid">
|
||||
OpenID
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="appSettings.disableRegistration == false" class="mt-4">
|
||||
{{ $t('auth.forms.dont_have_account_yet') }}
|
||||
<RouterLink id="lnkRegister" :to="{ name: 'register' }" class="is-link">
|
||||
{{ $t('auth.register') }}
|
||||
</RouterLink>
|
||||
</p>
|
||||
<div v-if="appSettings.enableSso" class="columns mt-4 is-variable is-1">
|
||||
<div class="column is-narrow py-1">
|
||||
{{ $t('auth.or_continue_with') }}
|
||||
</div>
|
||||
<div class="column py-1">
|
||||
<a v-if="$2fauth.config.sso.openid" id="lnkSignWithOpenID" class="button is-link is-outlined is-small ml-2" href="/socialite/redirect/openid">
|
||||
OpenID<FontAwesomeIcon class="ml-2" :icon="['fab', 'openid']" />
|
||||
</a>
|
||||
<a v-if="$2fauth.config.sso.github" id="lnkSignWithGithub" class="button is-link is-outlined is-small ml-2" href="/socialite/redirect/github">
|
||||
Github<FontAwesomeIcon class="ml-2" :icon="['fab', 'github-alt']" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
<!-- login/password legacy form -->
|
||||
@ -148,17 +158,25 @@
|
||||
{{ $t('auth.webauthn.security_device') }}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="appSettings.openidAuth">{{ $t('auth.sign_in_using') }}
|
||||
<a id="lnkSignWithOpenID" class="is-link" href="/socialite/redirect/openid">
|
||||
OpenID
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="appSettings.disableRegistration == false" class="mt-4">
|
||||
{{ $t('auth.forms.dont_have_account_yet') }}
|
||||
<RouterLink id="lnkRegister" :to="{ name: 'register' }" class="is-link">
|
||||
{{ $t('auth.register') }}
|
||||
</RouterLink>
|
||||
</p>
|
||||
<div v-if="appSettings.enableSso" class="columns mt-4 is-variable is-1">
|
||||
<div class="column is-narrow py-1">
|
||||
{{ $t('auth.or_continue_with') }}
|
||||
</div>
|
||||
<div class="column py-1">
|
||||
<a v-if="$2fauth.config.sso.openid" id="lnkSignWithOpenID" class="button is-link is-outlined is-small mr-2" href="/socialite/redirect/openid">
|
||||
OpenID<FontAwesomeIcon class="ml-2" :icon="['fab', 'openid']" />
|
||||
</a>
|
||||
<a v-if="$2fauth.config.sso.github" id="lnkSignWithGithub" class="button is-link is-outlined is-small mr-2" href="/socialite/redirect/github">
|
||||
Github<FontAwesomeIcon class="ml-2" :icon="['fab', 'github-alt']" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
<!-- footer -->
|
||||
|
@ -107,10 +107,13 @@
|
||||
<div v-if="user.isAdmin" class="notification is-warning">
|
||||
{{ $t('settings.you_are_administrator') }}
|
||||
</div>
|
||||
<div v-if="user.oauth_provider" class="notification is-info">
|
||||
{{ $t('settings.account_linked_to_sso_x_provider', { provider: user.oauth_provider }) }}
|
||||
</div>
|
||||
<form @submit.prevent="submitProfile" @keydown="formProfile.onKeydown($event)">
|
||||
<div v-if="$2fauth.config.proxyAuth" class="notification is-warning has-text-centered" v-html="$t('auth.user_account_controlled_by_proxy')" />
|
||||
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.profile') }}</h4>
|
||||
<fieldset :disabled="$2fauth.config.proxyAuth">
|
||||
<fieldset :disabled="$2fauth.config.proxyAuth || user.oauth_provider">
|
||||
<FormField v-model="formProfile.name" fieldName="name" :fieldError="formProfile.errors.get('name')" label="auth.forms.name" :maxLength="255" autofocus />
|
||||
<FormField v-model="formProfile.email" fieldName="email" :fieldError="formProfile.errors.get('email')" inputType="email" label="auth.forms.email" :maxLength="255" autofocus />
|
||||
<FormField v-model="formProfile.password" fieldName="password" :fieldError="formProfile.errors.get('password')" inputType="password" label="auth.forms.current_password.label" help="auth.forms.current_password.help" />
|
||||
@ -119,7 +122,7 @@
|
||||
</form>
|
||||
<form @submit.prevent="submitPassword" @keydown="formPassword.onKeydown($event)">
|
||||
<h4 class="title is-4 pt-6 has-text-grey-light">{{ $t('settings.change_password') }}</h4>
|
||||
<fieldset :disabled="$2fauth.config.proxyAuth">
|
||||
<fieldset :disabled="$2fauth.config.proxyAuth || user.oauth_provider">
|
||||
<FormPasswordField v-model="formPassword.password" fieldName="password" :fieldError="formPassword.errors.get('password')" :autocomplete="'new-password'" :showRules="true" label="auth.forms.new_password" />
|
||||
<FormPasswordField v-model="formPassword.password_confirmation" :showRules="false" fieldName="password_confirmation" :fieldError="formPassword.errors.get('password_confirmation')" inputType="password" :autocomplete="'new-password'" label="auth.forms.confirm_new_password" />
|
||||
<FormField v-model="formPassword.currentPassword" fieldName="currentPassword" :fieldError="formPassword.errors.get('currentPassword')" inputType="password" label="auth.forms.current_password.label" help="auth.forms.current_password.help" />
|
||||
@ -129,7 +132,9 @@
|
||||
<form id="frmDeleteAccount" @submit.prevent="submitDelete" @keydown="formDelete.onKeydown($event)">
|
||||
<h4 class="title is-4 pt-6 has-text-danger">{{ $t('auth.forms.delete_account') }}</h4>
|
||||
<div class="field is-size-7-mobile">
|
||||
{{ $t('auth.forms.delete_your_account_and_reset_all_data')}}
|
||||
<p class="block">{{ $t('auth.forms.delete_your_account_and_reset_all_data')}}</p>
|
||||
<p>{{ $t('auth.forms.reset_your_password_to_delete_your_account') }}</p>
|
||||
<p>{{ $t('auth.forms.deleting_2fauth_account_does_not_impact_provider') }}</p>
|
||||
</div>
|
||||
<fieldset :disabled="$2fauth.config.proxyAuth">
|
||||
<FormField v-model="formDelete.password" fieldName="password" :fieldError="formDelete.errors.get('password')" inputType="password" autocomplete="new-password" label="auth.forms.current_password.label" help="auth.forms.current_password.help" />
|
||||
|
@ -189,6 +189,8 @@
|
||||
<FormCheckbox :model-value="appSettings.useEncryption" @update:model-value="val => saveSetting('useEncryption', val)" fieldName="useEncryption" label="settings.forms.use_encryption.label" help="settings.forms.use_encryption.help" />
|
||||
<!-- disable registration -->
|
||||
<FormCheckbox :model-value="appSettings.disableRegistration" @update:model-value="val => saveSetting('disableRegistration', val)" fieldName="disableRegistration" label="settings.forms.disable_registration.label" help="settings.forms.disable_registration.help" />
|
||||
<!-- disable SSO registration -->
|
||||
<FormCheckbox :model-value="appSettings.enableSso" @update:model-value="val => saveSetting('enableSso', val)" fieldName="enableSso" label="settings.forms.enable_sso.label" help="settings.forms.enable_sso.help" />
|
||||
</div>
|
||||
</form>
|
||||
</FormWrapper>
|
||||
|
@ -22,6 +22,7 @@
|
||||
'sign_out' => 'Sign out',
|
||||
'sign_in' => 'Sign in',
|
||||
'sign_in_using' => 'Sign in using',
|
||||
'or_continue_with' => 'You an also continue with:',
|
||||
'sign_in_using_security_device' => 'Sign in using a security device',
|
||||
'login_and_password' => 'login & password',
|
||||
'register' => 'Register',
|
||||
@ -109,7 +110,9 @@
|
||||
'name_this_device' => 'Name this device',
|
||||
'delete_account' => 'Delete account',
|
||||
'delete_your_account' => 'Delete your account',
|
||||
'delete_your_account_and_reset_all_data' => 'This will reset 2FAuth. Your user account will be deleted as well as all 2FA data. There is no going back.',
|
||||
'delete_your_account_and_reset_all_data' => 'Your user account will be deleted as well as all your 2FA data. There is no going back.',
|
||||
'reset_your_password_to_delete_your_account' => 'If you always used SSO to sign in, sign out then use the reset password feature to get a password so you can fill this form.',
|
||||
'deleting_2fauth_account_does_not_impact_provider' => 'Deleting your 2FAuth account has no impact on your external SSO account.',
|
||||
'user_account_successfully_deleted' => 'User account successfully deleted',
|
||||
'has_lower_case' => 'Has lower case',
|
||||
'has_upper_case' => 'Has upper case',
|
||||
|
@ -59,4 +59,9 @@
|
||||
'cannot_delete_the_only_admin' => 'Cannot delete the only admin account',
|
||||
'error_during_data_fetching' => '💀 Something went wrong during data fetching',
|
||||
'check_failed_try_later' => 'Check failed, please retry later',
|
||||
'sso_disabled' => 'SSO is disabled',
|
||||
'sso_bad_provider_setup' => 'This SSO provider is not fully setup in your .env file',
|
||||
'sso_failed' => 'Authentication via SSO rejected',
|
||||
'no_register' => 'Registrations are disabled',
|
||||
'account_managed_by_external_provider' => 'Account managed by an external provider',
|
||||
];
|
@ -29,6 +29,7 @@
|
||||
'administration_legend' => 'While previous settings are user settings (every user can set its own preferences), following settings are global and apply to all users.',
|
||||
'only_an_admin_can_edit_them' => 'Only an administrator can view and edit them.',
|
||||
'you_are_administrator' => 'You are an administrator',
|
||||
'account_linked_to_sso_x_provider' => 'You signed-in via SSO using your :provider account. Your information cannot be changed here but on :provider.',
|
||||
'general' => 'General',
|
||||
'security' => 'Security',
|
||||
'profile' => 'Profile',
|
||||
@ -131,7 +132,11 @@
|
||||
],
|
||||
'disable_registration' => [
|
||||
'label' => 'Disable registration',
|
||||
'help' => 'Prevent new user registration',
|
||||
'help' => 'Prevent new user registration. This affects SSO as well, so new SSO users won\'t be able to sign on',
|
||||
],
|
||||
'enable_sso' => [
|
||||
'label' => 'Enable Single Sign-On (SSO)',
|
||||
'help' => 'Allow visitors to authenticate using an external ID via the Single Sign-On scheme',
|
||||
],
|
||||
'otp_generation' => [
|
||||
'label' => 'Show Password',
|
||||
|
248
tests/Feature/Http/Auth/SocialiteControllerTest.php
Normal file
248
tests/Feature/Http/Auth/SocialiteControllerTest.php
Normal file
@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Http\Auth;
|
||||
|
||||
use App\Facades\Settings;
|
||||
use App\Http\Controllers\Auth\SocialiteController;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
/**
|
||||
* SocialiteControllerTest test class
|
||||
*/
|
||||
#[CoversClass(SocialiteController::class)]
|
||||
class SocialiteControllerTest extends FeatureTestCase
|
||||
{
|
||||
/**
|
||||
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @var \Laravel\Socialite\Two\User
|
||||
*/
|
||||
protected $socialiteUser;
|
||||
|
||||
private const USER_OAUTH_ID = '12345';
|
||||
|
||||
private const USER_OAUTH_PROVIDER = 'github';
|
||||
|
||||
private const USER_NAME = 'John';
|
||||
|
||||
private const USER_NICKNAME = 'Jo';
|
||||
|
||||
private const USER_EMAIL = 'john@provider.com';
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function setUp() : void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
DB::table('users')->delete();
|
||||
$this->user = User::factory()->create([
|
||||
'name' => self::USER_NAME,
|
||||
'email' => self::USER_EMAIL,
|
||||
'password' => 'password',
|
||||
'is_admin' => 1,
|
||||
'oauth_id' => self::USER_OAUTH_ID,
|
||||
'oauth_provider' => self::USER_OAUTH_PROVIDER,
|
||||
]);
|
||||
|
||||
|
||||
$this->socialiteUser = new \Laravel\Socialite\Two\User;
|
||||
$this->socialiteUser->id = self::USER_OAUTH_ID;
|
||||
$this->socialiteUser->name = self::USER_NAME;
|
||||
$this->socialiteUser->email = self::USER_EMAIL;
|
||||
$this->socialiteUser->nickname = self::USER_NICKNAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_redirect_redirects_to_provider_url()
|
||||
{
|
||||
Settings::set('enableSso', true);
|
||||
|
||||
$response = $this->get('/socialite/redirect/github');
|
||||
|
||||
$response->assertRedirectContains('https://github.com/login/oauth/authorize');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_redirect_returns_error_when_registrations_are_disabled()
|
||||
{
|
||||
Settings::set('enableSso', false);
|
||||
|
||||
$response = $this->get('/socialite/redirect/github');
|
||||
|
||||
$response->assertRedirect('/error?err=sso_disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_callback_authenticates_the_user()
|
||||
{
|
||||
Socialite::shouldReceive('driver->user')
|
||||
->andReturn($this->socialiteUser);
|
||||
|
||||
$response = $this->get('/socialite/callback/github', ['driver' => 'github']);
|
||||
|
||||
$this->assertAuthenticatedAs($this->user, 'web-guard');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_callback_redirects_authenticated_user_to_accounts()
|
||||
{
|
||||
Socialite::shouldReceive('driver->user')
|
||||
->andReturn($this->socialiteUser);
|
||||
|
||||
$response = $this->get('/socialite/callback/github', ['driver' => 'github']);
|
||||
|
||||
$response->assertRedirect('/accounts');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_callback_updates_user_informations()
|
||||
{
|
||||
$socialiteUpdatedUser = new \Laravel\Socialite\Two\User;
|
||||
$socialiteUpdatedUser->id = self::USER_OAUTH_ID;
|
||||
$socialiteUpdatedUser->email = 'new_email';
|
||||
$socialiteUpdatedUser->nickname = 'new_nickname';
|
||||
|
||||
Socialite::shouldReceive('driver->user')
|
||||
->andReturn($socialiteUpdatedUser);
|
||||
|
||||
$response = $this->get('/socialite/callback/github', ['driver' => 'github']);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'oauth_id' => self::USER_OAUTH_ID,
|
||||
'oauth_provider' => self::USER_OAUTH_PROVIDER,
|
||||
'name' => 'new_nickname',
|
||||
'email' => 'new_email',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_callback_updates_username_with_fallback_value()
|
||||
{
|
||||
$socialiteUpdatedUser = new \Laravel\Socialite\Two\User;
|
||||
$socialiteUpdatedUser->id = self::USER_OAUTH_ID;
|
||||
$socialiteUpdatedUser->name = 'new_name';
|
||||
$socialiteUpdatedUser->email = 'new_email';
|
||||
|
||||
Socialite::shouldReceive('driver->user')
|
||||
->andReturn($socialiteUpdatedUser);
|
||||
|
||||
$response = $this->get('/socialite/callback/github', ['driver' => 'github']);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'oauth_id' => self::USER_OAUTH_ID,
|
||||
'oauth_provider' => self::USER_OAUTH_PROVIDER,
|
||||
'name' => 'new_name',
|
||||
'email' => 'new_email',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_callback_registers_new_user()
|
||||
{
|
||||
$newSocialiteUser = new \Laravel\Socialite\Two\User;
|
||||
$newSocialiteUser->id = 'new_id';
|
||||
$newSocialiteUser->name = 'jane';
|
||||
$newSocialiteUser->email = 'jane@provider.com';
|
||||
|
||||
Socialite::shouldReceive('driver->user')
|
||||
->andReturn($newSocialiteUser);
|
||||
|
||||
$response = $this->get('/socialite/callback/github', ['driver' => 'github']);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'oauth_id' => 'new_id',
|
||||
'oauth_provider' => self::USER_OAUTH_PROVIDER,
|
||||
'name' => 'jane',
|
||||
'email' => 'jane@provider.com',
|
||||
'is_admin' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_callback_always_registers_first_user_as_admin()
|
||||
{
|
||||
DB::table('users')->delete();
|
||||
Settings::set('disableRegistration', true);
|
||||
Settings::set('enableSso', false);
|
||||
|
||||
Socialite::shouldReceive('driver->user')
|
||||
->andReturn($this->socialiteUser);
|
||||
|
||||
$response = $this->get('/socialite/callback/github', ['driver' => 'github']);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'oauth_id' => self::USER_OAUTH_ID,
|
||||
'oauth_provider' => self::USER_OAUTH_PROVIDER,
|
||||
'is_admin' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_callback_returns_error_when_registrations_are_closed()
|
||||
{
|
||||
Settings::set('disableRegistration', true);
|
||||
|
||||
$newSocialiteUser = new \Laravel\Socialite\Two\User;
|
||||
$newSocialiteUser->id = 'rejected_id';
|
||||
$newSocialiteUser->name = 'jane';
|
||||
$newSocialiteUser->email = 'jane@provider.com';
|
||||
|
||||
Socialite::shouldReceive('driver->user')
|
||||
->andReturn($newSocialiteUser);
|
||||
|
||||
$response = $this->get('/socialite/callback/github', ['driver' => 'github']);
|
||||
|
||||
$response->assertRedirect('/error?err=no_register');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_callback_skips_registration_when_registrations_are_closed()
|
||||
{
|
||||
Settings::set('disableRegistration', true);
|
||||
|
||||
$newSocialiteUser = new \Laravel\Socialite\Two\User;
|
||||
$newSocialiteUser->id = 'rejected_id';
|
||||
$newSocialiteUser->name = 'jane';
|
||||
$newSocialiteUser->email = 'jane@provider.com';
|
||||
|
||||
Socialite::shouldReceive('driver->user')
|
||||
->andReturn($newSocialiteUser);
|
||||
|
||||
$response = $this->get('/socialite/callback/github', ['driver' => 'github']);
|
||||
|
||||
$this->assertDatabaseMissing('users', [
|
||||
'oauth_id' => 'rejected_id',
|
||||
'oauth_provider' => self::USER_OAUTH_PROVIDER,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user