Complete SSO (user model, error cases, tests, views) & Add github provider

This commit is contained in:
Bubka 2023-12-09 17:22:24 +01:00
parent 717bef16f7
commit 9ff35195f0
28 changed files with 488 additions and 79 deletions

View File

@ -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:

View File

@ -8,6 +8,7 @@ use Illuminate\Http\Resources\Json\JsonResource;
* @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,
];
}
}

View File

@ -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 @@ class PasswordController extends Controller
}
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')]);

View File

@ -13,32 +13,58 @@ use Laravel\Socialite\Facades\Socialite;
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');
}
}

View File

@ -24,6 +24,12 @@ class UserController extends Controller
$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');

View File

@ -27,7 +27,8 @@ class SinglePageController extends Controller
$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 @@ class SinglePageController extends Controller
'appConfig' => collect([
'proxyAuth' => $proxyAuth,
'proxyLogoutUrl' => $proxyLogoutUrl,
'openidAuth' => $openidAuth,
'sso' => [
'openid' => $openidAuth,
'github' => $githubAuth,
],
'subdirectory' => $subdir,
])->toJson(),
'defaultPreferences' => $defaultPreferences,

View 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);
}
}

View File

@ -49,7 +49,7 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
* @var string[]
*/
protected $fillable = [
'name', 'email', 'password',
'name', 'email', 'password', 'oauth_id', 'oauth_provider'
];
/**

View File

@ -10,7 +10,7 @@ use App\Listeners\CleanIconStorage;
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;

View File

@ -56,9 +56,11 @@ class OpenId extends AbstractProvider
}
/**
* {@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 => [

View File

@ -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);
}
}

View File

@ -71,7 +71,7 @@ return [
'lastRadarScan' => 0,
'latestRelease' => false,
'disableRegistration' => false,
'disableRegistrationSso' => false,
'enableSso' => true,
],
/*

View File

@ -159,7 +159,7 @@ return [
/*
* Package Service Providers...
*/
\SocialiteProviders\Manager\ServiceProvider::class,
/*
* Application Service Providers...
*/
@ -170,7 +170,7 @@ return [
App\Providers\RouteServiceProvider::class,
App\Providers\TwoFAuthServiceProvider::class,
App\Providers\MigrationServiceProvider::class,
])->toArray(),
])->toArray(),
/*
|--------------------------------------------------------------------------

View File

@ -21,6 +21,27 @@ return [
'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'),
],

View File

@ -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',
];

View File

@ -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');
});
}
};

View File

@ -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

View File

@ -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,
})

View File

@ -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' });
}

View File

@ -13,6 +13,7 @@ export const useUserStore = defineStore({
return {
name: undefined,
email: undefined,
oauth_provider: undefined,
preferences: window.defaultPreferences,
isAdmin: false,
}

View File

@ -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' })
}

View File

@ -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') }}&nbsp;
<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') }}&nbsp;
<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') }}&nbsp;
<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') }}&nbsp;
<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 -->

View File

@ -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" />

View File

@ -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>

View File

@ -22,6 +22,7 @@ return [
'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 @@ return [
'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',

View File

@ -59,4 +59,9 @@ return [
'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',
];

View File

@ -29,6 +29,7 @@ return [
'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 @@ return [
],
'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',

View 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,
]);
}
}