Merge pull request #12999 from snipe/develop

Google Oauth Recap
This commit is contained in:
snipe 2023-05-10 10:01:23 -07:00 committed by GitHub
commit 63c660f306
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 516 additions and 24 deletions

View file

@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\InvalidStateException;
use App\Models\Setting;
class GoogleAuthController extends Controller
{
/**
* We need this constructor so that we override the socialite expected config variables,
* since we want to allow this to be changed via database fields
*/
public function __construct()
{
parent::__construct();
$setting = Setting::getSettings();
config(['services.google.redirect' => config('app.url').'/google/callback']);
config(['services.google.client_id' => $setting->google_client_id]);
config(['services.google.client_secret' => $setting->google_client_secret]);
}
public function redirectToGoogle()
{
return Socialite::driver('google')->redirect();
}
public function handleGoogleCallback()
{
try {
$socialUser = Socialite::driver('google')->user();
\Log::debug('Google user found in Google Workspace');
} catch (InvalidStateException $exception) {
\Log::debug('Google user NOT found in Google Workspace');
return redirect()->route('login')
->withErrors(
[
'username' => [
trans('auth/general.google_login_failed')
],
]
);
}
$user = User::where('username', $socialUser->getEmail())->first();
if ($user) {
\Log::debug('Google user '.$socialUser->getEmail().' found in Snipe-IT');
$user->update([
'avatar' => $socialUser->avatar,
]);
Auth::login($user, true);
return redirect()->route('home');
}
\Log::debug('Google user '.$socialUser->getEmail().' NOT found in Snipe-IT');
return redirect()->route('login')
->withErrors(
[
'username' => [
trans('auth/general.google_login_failed'),
],
]
);
}
}

View file

@ -1039,6 +1039,48 @@ class SettingsController extends Controller
return $pdf_branding;
}
/**
* Show Google login settings form
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.1.1]
* @return View
*/
public function getGoogleLoginSettings()
{
$setting = Setting::getSettings();
return view('settings.google', compact('setting'));
}
/**
* ShSaveow Google login settings form
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.1.1]
* @return View
*/
public function postGoogleLoginSettings(Request $request)
{
if (!config('app.lock_passwords')) {
$setting = Setting::getSettings();
$setting->google_login = $request->input('google_login', 0);
$setting->google_client_id = $request->input('google_client_id');
$setting->google_client_secret = $request->input('google_client_secret');
if ($setting->save()) {
return redirect()->route('settings.index')
->with('success', trans('admin/settings/message.update.success'));
}
return redirect()->back()->withInput()->withErrors($setting->getErrors());
}
return redirect()->back()->with('error', trans('general.feature_disabled'));
}
/**
* Show the listing of backups.
*

View file

@ -76,6 +76,7 @@ class Setting extends Model
'audit_interval' => 'numeric|nullable',
'custom_forgot_pass_url' => 'url|nullable',
'privacy_policy_link' => 'nullable|url',
'google_client_id' => 'nullable|ends_with:apps.googleusercontent.com'
];
protected $fillable = [
@ -86,6 +87,9 @@ class Setting extends Model
'webhook_endpoint',
'webhook_channel',
'webhook_botname',
'google_login',
'google_client_id',
'google_client_secret',
];
/**

View file

@ -7,6 +7,7 @@ use App\Models\Setting;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class UserPresenter
@ -399,17 +400,25 @@ class UserPresenter extends Presenter
public function gravatar()
{
if ($this->avatar) {
// Check if it's a google avatar or some external avatar
if (Str::startsWith($this->avatar, ['http://', 'https://'])) {
return $this->avatar;
}
// Otherwise assume it's an uploaded image
return Storage::disk('public')->url('avatars/'.e($this->avatar));
}
if (Setting::getSettings()->load_remote == '1') {
if ($this->model->gravatar != '') {
$gravatar = md5(strtolower(trim($this->model->gravatar)));
return '//gravatar.com/avatar/'.$gravatar;
} elseif ($this->email != '') {
$gravatar = md5(strtolower(trim($this->email)));
} elseif ($this->email != '') {
$gravatar = md5(strtolower(trim($this->email)));
return '//gravatar.com/avatar/'.$gravatar;
}
}

View file

@ -46,6 +46,7 @@
"laravel/helpers": "^1.4",
"laravel/passport": "^10.1",
"laravel/slack-notification-channel": "^2.3",
"laravel/socialite": "^5.6",
"laravel/tinker": "^2.6",
"laravel/ui": "^3.3",
"laravelcollective/html": "^6.2",

149
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c0444af6de1d16a70c7e5520db745170",
"content-hash": "4c82b2e171fb02a3ef024906db5d74c9",
"packages": [
{
"name": "alek13/slack",
@ -3605,6 +3605,75 @@
},
"time": "2022-01-12T18:07:54+00:00"
},
{
"name": "laravel/socialite",
"version": "v5.6.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "a14a177f2cc71d8add71e2b19e00800e83bdda09"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/a14a177f2cc71d8add71e2b19e00800e83bdda09",
"reference": "a14a177f2cc71d8add71e2b19e00800e83bdda09",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0",
"league/oauth1-client": "^1.10.1",
"php": "^7.2|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0",
"phpunit/phpunit": "^8.0|^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Socialite\\SocialiteServiceProvider"
],
"aliases": {
"Socialite": "Laravel\\Socialite\\Facades\\Socialite"
}
}
},
"autoload": {
"psr-4": {
"Laravel\\Socialite\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.",
"homepage": "https://laravel.com",
"keywords": [
"laravel",
"oauth"
],
"support": {
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
"time": "2023-01-20T15:42:35+00:00"
},
{
"name": "laravel/tinker",
"version": "v2.7.2",
@ -4534,6 +4603,82 @@
],
"time": "2022-04-17T13:12:02+00:00"
},
{
"name": "league/oauth1-client",
"version": "v1.10.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/oauth1-client.git",
"reference": "d6365b901b5c287dd41f143033315e2f777e1167"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167",
"reference": "d6365b901b5c287dd41f143033315e2f777e1167",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-openssl": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
"guzzlehttp/psr7": "^1.7|^2.0",
"php": ">=7.1||>=8.0"
},
"require-dev": {
"ext-simplexml": "*",
"friendsofphp/php-cs-fixer": "^2.17",
"mockery/mockery": "^1.3.3",
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5||9.5"
},
"suggest": {
"ext-simplexml": "For decoding XML-based responses."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev",
"dev-develop": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"League\\OAuth1\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ben Corlett",
"email": "bencorlett@me.com",
"homepage": "http://www.webcomm.com.au",
"role": "Developer"
}
],
"description": "OAuth 1.0 Client Library",
"keywords": [
"Authentication",
"SSO",
"authorization",
"bitbucket",
"identity",
"idp",
"oauth",
"oauth1",
"single sign on",
"trello",
"tumblr",
"twitter"
],
"support": {
"issues": "https://github.com/thephpleague/oauth1-client/issues",
"source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.1"
},
"time": "2022-04-15T14:02:14+00:00"
},
{
"name": "league/oauth2-server",
"version": "8.3.5",
@ -16425,5 +16570,5 @@
"ext-pdo": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.0.0"
}

View file

@ -216,7 +216,8 @@ return [
*/
'require_saml' => env('REQUIRE_SAML', false),
/*
|--------------------------------------------------------------------------
| Demo Mode Lockdown
@ -294,6 +295,7 @@ return [
Laravel\Tinker\TinkerServiceProvider::class,
Unicodeveloper\DumbPassword\DumbPasswordServiceProvider::class,
Eduardokum\LaravelMailAutoEmbed\ServiceProvider::class,
Laravel\Socialite\SocialiteServiceProvider::class,
/*
* Application Service Providers...
@ -366,6 +368,7 @@ return [
'Image' => Intervention\Image\ImageServiceProvider::class,
'Carbon' => Carbon\Carbon::class,
'Helper' => App\Helpers\Helper::class, // makes it much easier to use 'Helper::blah' in blades (which is where we usually use this)
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
],

View file

@ -1,10 +1,11 @@
<?php
return array (
'app_version' => 'v6.1.1-pre',
'full_app_version' => 'v6.1.1-pre - build 10534-g609b1646e',
'build_version' => '10534',
'full_app_version' => 'v6.1.1-pre - build 10653-g11cd875c6',
'build_version' => '10653',
'prerelease_version' => '',
'hash_version' => 'g609b1646e',
'full_hash' => 'v6.1.1-pre-292-g609b1646e',
'hash_version' => 'g11cd875c6',
'full_hash' => 'v6.1.1-pre-411-g11cd875c6',
'branch' => 'master',
);

View file

@ -65,6 +65,20 @@ class UserFactory extends Factory
});
}
public function testAdmin()
{
return $this->state(function () {
return [
'first_name' => 'Alison',
'last_name' => 'Gianotto',
'username' => 'agianotto@grokability.com',
'avatar' => '2.jpg',
'email' => 'agianotto@grokability.com',
'permissions' => '{"superuser":"1"}',
];
});
}
public function superuser()
{
return $this->state(function () {

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddGoogleAuthToSettings extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('settings', function (Blueprint $table) {
$table->boolean('google_login')->nullable()->default(0);
$table->string('google_client_id')->nullable()->default(null);
$table->string('google_client_secret')->nullable()->default(null);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('settings', function (Blueprint $table) {
$table->dropColumn('google_login');
$table->dropColumn('google_client_id');
$table->dropColumn('google_client_secret');
});
}
}

View file

@ -47,6 +47,13 @@ class UserSeeder extends Seeder
]))
->create();
User::factory()->count(1)->testAdmin()
->state(new Sequence(fn($sequence) => [
'company_id' => $companyIds->random(),
'department_id' => $departmentIds->random(),
]))
->create();
User::factory()->count(3)->superuser()
->state(new Sequence(fn($sequence) => [
'company_id' => $companyIds->random(),

View file

@ -330,4 +330,9 @@ return [
'setup_migration_create_user' => 'Next: Create User',
'ldap_settings_link' => 'LDAP Settings Page',
'slack_test' => 'Test <i class="fab fa-slack"></i> Integration',
'google_callback_help' => 'This should be entered as the callback URL in your Google OAuth app settings in your organization&apos;s <strong><a href="https://console.cloud.google.com/" target="_blank">Google developer console <i class="fa fa-external-link" aria-hidden="true"></i></a></strong>.',
'google_login' => 'Google Workspace Login Settings',
'enable_google_login' => 'Enable users to login with Google Workspace',
'enable_google_login_help' => 'Users will not be automatically provisioned. They must have an existing account here AND in Google Workspace, and their username here must match their Google Workspace email address. ',
];

View file

@ -6,7 +6,7 @@ return array(
'declined' => 'You have successfully declined this asset.',
'bulk_manager_warn' => 'Your users have been successfully updated, however your manager entry was not saved because the manager you selected was also in the user list to be edited, and users may not be their own manager. Please select your users again, excluding the manager.',
'user_exists' => 'User already exists!',
'user_not_found' => 'User [:id] does not exist.',
'user_not_found' => 'User does not exist.',
'user_login_required' => 'The login field is required',
'user_password_required' => 'The password is required.',
'insufficient_permissions' => 'Insufficient Permissions.',

View file

@ -12,5 +12,8 @@ return [
'remember_me' => 'Remember Me',
'username_help_top' => 'Enter your <strong>username</strong> to be emailed a password reset link.',
'username_help_bottom' => 'Your username and email address <em>may</em> be the same, but may not be, depending on your configuration. If you cannot remember your username, contact your administrator. <br><br><strong>Usernames without an associated email address will not be emailed a password reset link.</strong> ',
];
'google_login' => 'Or login with Google Workspace',
'google_login_failed' => 'Google Login failed, please try again.',
];

View file

@ -67,6 +67,8 @@ return [
'array' => 'The :attribute must have at least :min items.',
],
'starts_with' => 'The :attribute must start with one of the following: :values.',
'ends_with' => 'The :attribute must end with one of the following: :values.',
'not_in' => 'The selected :attribute is invalid.',
'numeric' => 'The :attribute must be a number.',
'present' => 'The :attribute field must be present.',

View file

@ -64,7 +64,7 @@
</div> <!-- end row -->
@if (!config('app.require_saml') && $snipeSettings->saml_enabled)
<div class="row ">
<div class="row">
<div class="text-right col-md-12">
<a href="{{ route('saml.login') }}">{{ trans('auth/general.saml_login') }}</a>
</div>
@ -73,22 +73,32 @@
</div>
<div class="box-footer">
@if (config('app.require_saml'))
<a class="btn btn-lg btn-primary btn-block" href="{{ route('saml.login') }}">{{ trans('auth/general.saml_login') }}</a>
<a class="btn btn-primary btn-block" href="{{ route('saml.login') }}">{{ trans('auth/general.saml_login') }}</a>
@else
<button class="btn btn-lg btn-primary btn-block">{{ trans('auth/general.login') }}</button>
<button class="btn btn-primary btn-block">{{ trans('auth/general.login') }}</button>
@endif
</div>
<div class="text-right col-md-12 col-sm-12 col-xs-12" style="padding-top: 10px;">
@if ($snipeSettings->custom_forgot_pass_url)
<a href="{{ $snipeSettings->custom_forgot_pass_url }}" rel="noopener">{{ trans('auth/general.forgot_password') }}</a>
<div class="col-md-12 text-right" style="padding-top: 15px;">
<a href="{{ $snipeSettings->custom_forgot_pass_url }}" rel="noopener">{{ trans('auth/general.forgot_password') }}</a>
</div>
@elseif (!config('app.require_saml'))
<a href="{{ route('password.request') }}">{{ trans('auth/general.forgot_password') }}</a>
<div class="col-md-12 text-right" style="padding-top: 15px;">
<a href="{{ route('password.request') }}">{{ trans('auth/general.forgot_password') }}</a>
</div>
@endif
</div>
</div> <!-- end login box -->
@if (($snipeSettings->google_login=='1') && ($snipeSettings->google_client_id!='') && ($snipeSettings->google_client_secret!=''))
<a href="{{ route('google.redirect') }}" class="btn btn-block btn-social btn-google">
<i class="fa-brands fa-google"></i> {{ trans('auth/general.google_login') }}
</a>
@endif
</div> <!-- col-md-4 -->
</div> <!-- end row -->

View file

@ -616,7 +616,7 @@
@if (($asset->model->manufacturer) && ($asset->model->manufacturer->warranty_lookup_url!=''))
<a href="{{ $asset->present()->dynamicWarrantyUrl() }}" target="_blank">
<i class="fa fa-external-link"><span class="sr-only">{{ trans('admin/hardware/general.mfg_warranty_lookup', ['manufacturer' => $asset->model->manufacturer->name]) }}</span></i>
<i class="fa fa-external-link" aria-hidden="true"><span class="sr-only">{{ trans('admin/hardware/general.mfg_warranty_lookup', ['manufacturer' => $asset->model->manufacturer->name]) }}</span></i>
</a>
@endif
</div>

View file

@ -0,0 +1,117 @@
@extends('layouts/default')
{{-- Page title --}}
@section('title')
{{ trans('admin/settings/general.google_login') }}
@parent
@stop
@section('header_right')
<a href="{{ route('settings.index') }}" class="btn btn-primary"> {{ trans('general.back') }}</a>
@stop
{{-- Page content --}}
@section('content')
{{ Form::open(['method' => 'POST', 'files' => false, 'autocomplete' => 'off', 'class' => 'form-horizontal', 'role' => 'form' ]) }}
<!-- CSRF Token -->
{{csrf_field()}}
<div class="row">
<div class="col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2">
<div class="panel box box-default">
<div class="box-header with-border">
<h2 class="box-title">
<i class="fa-brands fa-google"></i> {{ trans('admin/settings/general.google_login') }}
</h2>
</div>
<div class="box-body">
<div class="col-md-12">
<!-- Google Redirect URL -->
<div class="form-group">
<div class="col-md-3 text-right">
<strong>Redirect URL</strong>
</div>
<div class="col-md-8">
<p class="form-control-static" style="margin-top: -5px"><code>{{ config('app.url') }}/google/callback</code></p>
<p class="help-block">{!! trans('admin/settings/general.google_callback_help') !!}</p>
</div>
</div>
<!-- Google login -->
<div class="form-group {{ $errors->has('google') ? 'error' : '' }}">
<div class="col-md-8 col-md-offset-3">
<label class="form-control{{ (config('app.lock_passwords')===true) ? ' form-control--disabled': '' }}">
<span class="sr-only">{{ trans('admin/settings/general.pwd_secure_uncommon') }}</span>
{{ Form::checkbox('google_login', '1', old('google_login', $setting->google_login),array('aria-label'=>'google_login', (config('app.lock_passwords')===true) ? 'disabled': '')) }}
{{ trans('admin/settings/general.enable_google_login') }}
</label>
<p class="help-block">{{ trans('admin/settings/general.enable_google_login_help') }}</p>
</div>
</div>
<!-- Google Client ID -->
<div class="form-group {{ $errors->has('google_client_id') ? 'error' : '' }}">
<div class="col-md-3 text-right">
{{ Form::label('google_client_id', 'Client ID') }}
</div>
<div class="col-md-8">
{{ Form::text('google_client_id', old('google_client_id', $setting->google_client_id), ['class' => 'form-control','placeholder' => trans('general.example') .'000000000000-XXXXXXXXXXX.apps.googleusercontent.com', (config('app.lock_passwords')===true) ? 'disabled': '']) }}
{!! $errors->first('google_client_id', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
@if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p>
@endif
</div>
</div>
<!-- Google Client Secret -->
<div class="form-group {{ $errors->has('google_client_secret') ? 'error' : '' }}">
<div class="col-md-3 text-right">
{{ Form::label('google_client_secret', 'Client Secret') }}
</div>
<div class="col-md-8">
@if (config('app.lock_passwords')===true)
{{ Form::text('google_client_secret', 'XXXXXXXXXXXXXXXXXXXXXXX', ['class' => 'form-control', 'disabled']) }}
@else
{{ Form::text('google_client_secret', old('google_client_secret', $setting->google_client_secret), ['class' => 'form-control','placeholder' => trans('general.example') .'XXXXXXXXXXXX']) }}
@endif
{!! $errors->first('google_client_secret', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
@if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p>
@endif
</div>
</div>
</div>
</div> <!--/.box-body-->
<div class="box-footer">
<div class="text-left col-md-6">
<a class="btn btn-link text-left" href="{{ route('settings.index') }}">{{ trans('button.cancel') }}</a>
</div>
<div class="text-right col-md-6">
<button type="submit" class="btn btn-success"{{ (config('app.lock_passwords')===true) ? ' disabled': '' }}><i class="fas fa-check icon-white" aria-hidden="true"></i> {{ trans('general.save') }}</button>
</div>
</div>
</div> <!-- /box -->
</div> <!-- /.col-md-8-->
</div> <!-- /.row-->
{{Form::close()}}
@stop

View file

@ -235,6 +235,21 @@
</div>
</div>
<div class="col-md-4 col-lg-3 col-sm-6 col-xl-1">
<div class="box box-default">
<div class="box-body text-center">
<h5>
<a href="{{ route('settings.google.index') }}" class="settings_button">
<i class="fa-brands fa-google fa-4x" aria-hidden="true"></i>
<br><br>
<span class="name">Google</span>
</a>
</h5>
<p class="help-block">{{ trans('admin/settings/general.google_login') }}</p>
</div>
</div>
</div>
<div class="col-md-4 col-lg-3 col-sm-6 col-xl-1">
<div class="box box-default">
<div class="box-body text-center">

View file

@ -192,6 +192,9 @@ Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'authorize:superuser
Route::get('oauth', [SettingsController::class, 'api'])->name('settings.oauth.index');
Route::get('google', [SettingsController::class, 'getGoogleLoginSettings'])->name('settings.google.index');
Route::post('google', [SettingsController::class, 'postGoogleLoginSettings'])->name('settings.google.save');
Route::get('purge', [SettingsController::class, 'getPurge'])->name('settings.purge.index');
Route::post('purge', [SettingsController::class, 'postPurge'])->name('settings.purge.save');
@ -453,8 +456,6 @@ Route::group(['middleware' => 'web'], function () {
[LoginController::class, 'postTwoFactorAuth']
);
Route::post(
'password/email',
[ForgotPasswordController::class, 'sendResetLinkEmail']
@ -483,7 +484,9 @@ Route::group(['middleware' => 'web'], function () {
)->name('password.email')->middleware('throttle:forgotten_password');
// Socialite Google login
Route::get('google', 'App\Http\Controllers\GoogleAuthController@redirectToGoogle')->name('google.redirect');
Route::get('google/callback', 'App\Http\Controllers\GoogleAuthController@handleGoogleCallback')->name('google.callback');
Route::get(