Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Varied fixes and additions #3

Open
wants to merge 64 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
02d58f6
add linkedin-openid provider
mjauvin May 18, 2024
851d39e
moved fr folder in the right place
mjauvin May 18, 2024
c63f281
add linkedin openid provider
mjauvin May 18, 2024
5f0beff
extra scopes are allowed from the config
mjauvin May 19, 2024
fdb3a69
add basic fields definition for Log model
mjauvin May 19, 2024
4b14551
persist access_token in user profile metadata
mjauvin May 19, 2024
42834b2
handle oAuth errors before going further
mjauvin May 19, 2024
9f4b4da
use oAuth standard error request params
mjauvin May 19, 2024
a11e675
openid in parenthesis
mjauvin May 19, 2024
8d6e6c0
new provider model and controller
mjauvin May 20, 2024
5d4b2f2
simplify model
mjauvin May 20, 2024
18980c0
minor tweaks to provider model form
mjauvin May 20, 2024
edaef1f
handle errors by flashing the error to the session and returning to `…
mjauvin May 20, 2024
f3f8e4e
return to signin page with proper error for inactive providers
mjauvin May 20, 2024
9592856
initial code to fire an event after provider signin
mjauvin May 20, 2024
11dda3c
add/improve some string localizations
mjauvin May 20, 2024
29816ad
add more events
mjauvin May 20, 2024
32eb2be
use fireSystemEvent
mjauvin May 20, 2024
0448226
first param is implicit
mjauvin May 20, 2024
a0f924d
fireSystemEvent is not appropriate here
mjauvin May 20, 2024
dbe5f87
move to system settings
mjauvin May 20, 2024
db1b584
handle more errors and add localizations
mjauvin May 20, 2024
436ed5e
extract providers code into a view
mjauvin May 20, 2024
c8505ca
cleanup code and and more generic dynamic methods to User model
mjauvin May 20, 2024
7491d99
cleanup
mjauvin May 20, 2024
4016c00
add method docbloc
mjauvin May 20, 2024
2fadbd6
not all providers required a client_secret
mjauvin May 20, 2024
61f6df1
Merge branch 'main' into mjauvin-fix
mjauvin May 20, 2024
3c7a95b
fix to allow username login for regular signin
mjauvin May 20, 2024
6c757a3
Request::path() does not start with slash
mjauvin May 20, 2024
4429b9e
match the full request url against backend url for the callback action
mjauvin May 20, 2024
2023b00
improve error handling in redirect when enabled provider is not confi…
mjauvin May 20, 2024
5a03bf6
redirect to backend
mjauvin May 20, 2024
85087e1
add a way to save/retrieve the signin_url
mjauvin May 20, 2024
fe25cb9
use Lang instead of trans helper
mjauvin May 21, 2024
dd2246f
Apply suggestions from code review
mjauvin May 21, 2024
8554d6a
add :provider param to string localizations
mjauvin May 21, 2024
28ca708
remove deprecated linkedin provider
mjauvin May 21, 2024
d711d63
allow the event to be halted
mjauvin May 21, 2024
189f3d1
first pass at new event methods
mjauvin May 21, 2024
9f47d81
save the user
mjauvin May 21, 2024
f3f809f
abort signin if handler returns false
mjauvin May 21, 2024
8671b14
improve events and error handling
mjauvin May 21, 2024
24cc68d
use event until we have the event methods
mjauvin May 21, 2024
3fb536a
missed one
mjauvin May 21, 2024
79d4de5
reuse catch bloc to simplify code
mjauvin May 21, 2024
03d7568
check if event methods are available
mjauvin May 21, 2024
47bc39c
do not prevent the actual message to be shown when not in debug mode
mjauvin May 22, 2024
983a293
move migration code in event handler
mjauvin May 22, 2024
c959f8f
get remember config from session, defaults to false
mjauvin May 22, 2024
04aa622
normalize sso user email
mjauvin May 22, 2024
b01b917
use simpler string concat
mjauvin May 22, 2024
38dc10b
verify previous/current ssoId match
mjauvin May 22, 2024
6945bed
improve localization strings for invalid_ssoid
mjauvin May 22, 2024
cebb6d1
add some security verifications
mjauvin May 22, 2024
5c9c086
add config documentation for `require_explicit_permission`
mjauvin May 22, 2024
3a6aa4c
move todo comment in the controller
mjauvin May 22, 2024
f722e2a
use encryptable trait to encrypt client_secret
mjauvin Aug 19, 2024
b8e7036
update comment
mjauvin Sep 10, 2024
6b00a30
improve error handling; add not about InvalidState exception
mjauvin Sep 10, 2024
8f0c66c
force session.same_site to lax for now when session.secure is true
mjauvin Sep 10, 2024
aa027f2
use with() method instead of driver()
mjauvin Sep 11, 2024
9888eae
fix email variable name
mjauvin Sep 11, 2024
83ba1a8
fix issue if metadata is a string
mjauvin Sep 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 32 additions & 21 deletions Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
use Backend\Models\UserRole;
use Config;
use Event;
use Lang;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\SocialiteServiceProvider;
use Request;
use Session;
use System\Classes\PluginBase;
use System\Classes\SettingsManager;
use Url;
use View;

/**
Expand Down Expand Up @@ -52,6 +52,11 @@ public function registerPermissions(): array
'tab' => 'winter.sso::lang.plugin.name',
'roles' => [UserRole::CODE_DEVELOPER],
],
'winter.sso.view_providers' => [
'label' => 'winter.sso::lang.permissions.view_providers',
'tab' => 'winter.sso::lang.plugin.name',
'roles' => [UserRole::CODE_DEVELOPER],
],
];
}

Expand All @@ -69,6 +74,14 @@ public function registerSettings(): array
'permissions' => ['winter.sso.view_logs'],
'category' => SettingsManager::CATEGORY_LOGS,
],
'providers' => [
'label' => 'winter.sso::lang.models.provider.label_plural',
'description' => 'winter.sso::lang.models.provider.menu_description',
'icon' => 'icon-openid',
'url' => Backend::url('winter/sso/providers'),
'permissions' => ['winter.sso.view_providers'],
'category' => SettingsManager::CATEGORY_SYSTEM,
],
];
}

Expand All @@ -86,15 +99,23 @@ public function register(): void
*/
protected function forceEmailLogin(): void
{
User::$loginAttribute = 'email';
// only do this for the sso callback route, still allow username login for regular signin
if (str_starts_with(Request::url(), Backend::url('winter/sso/handle/callback/'))) {
User::$loginAttribute = 'email';
}
User::extend(function ($model) {
$model->addDynamicMethod('getSsoId', function (string $provider) use ($model) {
return $model->metadata['winter.sso'][$provider]['id'] ?? null;
$model->addDynamicMethod('getSsoValue', function (string $provider, mixed $key) use ($model) {
return $model->metadata['winter.sso'][$provider][$key] ?? null;
});
$model->addDynamicMethod('setSsoId', function (string $provider, string $id) use ($model) {
$model->addDynamicMethod('setSsoValues', function (string $provider, array $values, bool $save = false) use ($model) {
$metadata = $model->metadata ?? [];
$metadata['winter.sso'][$provider]['id'] = $id;
foreach ($values as $key => $value) {
$metadata['winter.sso'][$provider][$key] = $value;
}
$model->metadata = $metadata;
if ($save) {
$model->save();
}
});
});
}
Expand Down Expand Up @@ -142,22 +163,12 @@ protected function extendAuthController(): void
{
// Extend the signin view to add the SSO buttons for the enabled providers
Event::listen('backend.auth.extendSigninView', function ($controller) {
$buttonsHtml = '';

foreach (Config::get('winter.sso::enabled_providers', []) as $provider) {
$providerName = Lang::get("winter.sso::lang.providers.$provider");
$buttonsHtml .= View::make("winter.sso::buttons.provider", [
'logoUrl' => Url::asset('/plugins/winter/sso/assets/images/providers/' . $provider . '.svg'),
'logoAlt' => Lang::get('winter.sso::lang.provider_btn.alt_text', ['provider' => $providerName]),
'url' => Backend::url('winter/sso/handle/redirect/' . $provider),
'label' => Lang::get('winter.sso::lang.provider_btn.label', ['provider' => $providerName]),
]);
}

$controller->addCss('/plugins/winter/sso/assets/dist/css/sso.css', 'Winter.SSO');

if (!empty($buttonsHtml)) {
echo $buttonsHtml;
if ($view = View::make("winter.sso::providers", ['providers' => Config::get('winter.sso::enabled_providers', [])])) {
// save signin_url to redirect
Session::put('signin_url', Request::url());
echo $view;
}
});
}
Expand Down
1 change: 1 addition & 0 deletions assets/images/providers/linkedin-openid.svg
7 changes: 7 additions & 0 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
// 'gitlab',
// 'google',
// 'linkedin',
// 'linkedin-openid',
// 'twitter',
// 'twitter-oauth-2',
],
Expand Down Expand Up @@ -112,6 +113,12 @@
'guzzle' => [],
],

'linkedin-openid' => [
'client_id' => env('LINKEDIN_OPENID_CLIENT_ID'),
'client_secret' => env('LINKEDIN_OPENID_CLIENT_SECRET'),
'guzzle' => [],
],
mjauvin marked this conversation as resolved.
Show resolved Hide resolved

'linkedin' => [
'client_id' => env('LINKEDIN_CLIENT_ID'),
'client_secret' => env('LINKEDIN_CLIENT_SECRET'),
Expand Down
109 changes: 82 additions & 27 deletions controllers/Handle.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
namespace Winter\SSO\Controllers;

use Backend;
use BackendAuth;
use Backend\Classes\Controller;
use Backend\Models\AccessLog;
use BackendAuth;
use Config;
use Event;
use Exception;
use Flash;
use Illuminate\Http\RedirectResponse;
use Laravel\Socialite\Two\InvalidStateException;
use Laravel\Socialite\Two\User as SocialiteUser;
use Redirect;
use Request;
use Session;
use Socialite;
use System\Classes\UpdateManager;
use Winter\SSO\Models\Log;
Expand Down Expand Up @@ -49,23 +53,47 @@ public function __construct()
$this->enabledProviders = Config::get('winter.sso::enabled_providers', []);
}

/**
* Returns the $authManager property.
*/
public function getAuthManager() : AuthManager
{
return $this->authManager;
}

/**
* Processes a callback from the SSO provider
* @throws HttpException if the provider is not enabled
* @throws HttpException if the user cannot be found
* Redirect back to signin form on errors with Flash message.
*/
public function callback(string $provider): RedirectResponse
{
if (!in_array($provider, $this->enabledProviders)) {
abort(404);
Flash::error(trans('winter.sso::lang.messages.inactive_provider'));
return $this->redirectToSignInPage();
}

if (!Request::input('code')) {
$error = sprintf("%s: %s", Request::input('error'), Request::input('error_description'));
Flash::error($error);
return $this->redirectToSignInPage();
}

// @TODO: Login or register the user / provide an event for plugins to handle
// user registration themselves. Would like plugin to be able to handle frontend
// or backend or even both. If event is used follow naming conventions from in progress
// issues

$ssoUser = Socialite::driver($provider)->user();
try {
$ssoUser = Socialite::driver($provider)->user();
if ($signInResult = Event::fire('winter.sso.signin', [$this, $provider, $ssoUser])) {
// @TODO: handle event results
}
} catch (InvalidStateException $e) {
Flash::error(trans('winter.sso::lang.messages.invalid_state'));
return $this->redirectToSignInPage();
} catch (\Exception $e) {
Flash::error($e->getMessage());
return $this->redirectToSignInPage();
}

try {
// @TODO: Protection against service saying that [email protected] is authenticated
Expand All @@ -81,25 +109,39 @@ public function callback(string $provider): RedirectResponse
}

if (!$user) {
// $password = Str::random(400);
// $user = $this->authManager->register([
// 'email' => $ssoUser->getEmail(),
// 'password' => $password,
// 'password_confirmation' => $password,
// 'name' => $ssoUser->getName(),
// ]);
// $user->setSsoConfig('allow_password_auth', false);
// @TODO: Event here for registering user if desired, default fallback abort behaviour
abort(403, 'User not found');
Event::listen('winter.sso.register', function ($controller, $provider, $ssoUser) {
return;
$password = Str::random(400);
$user = $controller->getAuthManager()->register([
'email' => $ssoUser->getEmail(),
'password' => $password,
'password_confirmation' => $password,
'name' => $ssoUser->getName(),
]);
$user->setSsoValues($provider, ['allow_password_auth' => false]);
return $user;
});
if (Config::get('winter.sso::allow_registration')) {
$user = Event::fire('winter.sso.register', [$this, $provider, $ssoUser]);
}
if (!$user) {
Flash::error(trans('winter.sso::lang.messages.user_not_found', ['user' => $ssoUser->getEmail()]));
return $this->redirectToSignInPage();
}
mjauvin marked this conversation as resolved.
Show resolved Hide resolved
}

if (
$ssoUser->getId()
&& $user->getSsoId($provider) !== $ssoUser->getId()
) {
$updates = [];
if ($ssoUser->getId() && $user->getSsoValue($provider, 'id') !== $ssoUser->getId()) {
// @TODO: Check if request / user is allowed to associate this account to this provider's ID
$user->setSsoId($provider, $ssoUser->getId());
$user->save();
$updates['id'] = $ssoUser->getId();
}

if ($ssoUser->token && $user->getSsoValue($provider, 'token') !== $ssoUser->token) {
$updates['token'] = $ssoUser->token;
}

if ($updates) {
$user->setSsoValues($provider, $updates, save:true);
}

// Check if the user is allowed to keep a persistent session
Expand Down Expand Up @@ -146,21 +188,34 @@ public function callback(string $provider): RedirectResponse

/**
* Redirects the user to the authentication page of the given provider.
* Redirect back to signin form on errors with Flash message.
*/
public function redirect(string $provider): RedirectResponse
{
if (!in_array($provider, $this->enabledProviders)) {
abort(404);
Flash::error(trans('winter.sso::lang.messages.inactive_provider'));
mjauvin marked this conversation as resolved.
Show resolved Hide resolved
return $this->redirectToSignInPage();
}

$config = Config::get('services.' . $provider, []);
if (!isset($config['client_id'])) {
Flash::error(trans('winter.sso::lang.messages.misconfigured_provider'));
return $this->redirectToSignInPage();
}

if ($this->authManager->getUser()) {
// @TODO:
// - Handle case of user explicitly attaching a SSO provider to their account
// - Localization
Flash::error("You are already logged in. Please log out first.");
return Redirect::back();
Flash::error(trans('winter.sso::lang.messages.already_logged_in'));
return Backend::redirect('backend');
}

return Socialite::driver($provider)->redirect();
return Socialite::driver($provider)->scopes($config['scopes'] ?? [])->redirect();
}

public function redirectToSignInPage()
{
$signin_url = Session::pull('signin_url', Backend::url('backend/auth/signin'));
return Redirect::to($signin_url);
}
}
27 changes: 27 additions & 0 deletions controllers/Providers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php namespace Winter\SSO\Controllers;

use BackendMenu;
use Backend\Classes\Controller;

/**
* Providers Backend Controller
*/
class Providers extends Controller
{
/**
* @var array Behaviors that are implemented by this controller.
*/
public $implement = [
\Backend\Behaviors\FormController::class,
\Backend\Behaviors\ListController::class,
];

public $bodyClass = 'compact-container';

public function __construct()
{
parent::__construct();

BackendMenu::setContext('Winter.SSO', 'sso', 'providers');
}
}
21 changes: 21 additions & 0 deletions controllers/providers/_list_toolbar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div data-control="toolbar">
<a
href="<?= Backend::url('winter/sso/providers/create') ?>"
class="btn btn-primary wn-icon-plus">
<?= e(trans('backend::lang.form.create_title', ['name' => trans('winter.sso::lang.models.provider.label')])); ?>
</a>

<button
class="btn btn-danger wn-icon-trash-o"
disabled="disabled"
onclick="$(this).data('request-data', { checked: $('.control-list').listWidget('getChecked') })"
data-request="onDelete"
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')); ?>"
data-trigger-action="enable"
data-trigger=".control-list input[type=checkbox]"
data-trigger-condition="checked"
data-request-success="$(this).prop('disabled', 'disabled')"
data-stripe-load-indicator>
<?= e(trans('backend::lang.list.delete_selected')); ?>
</button>
</div>
31 changes: 31 additions & 0 deletions controllers/providers/config_form.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ===================================
# Form Behavior Config
# ===================================

# Record name
name: 'winter.sso::lang.models.provider.label'

# Model Form Field configuration
form: $/winter/sso/models/provider/fields.yaml

# Model Class name
modelClass: Winter\SSO\Models\Provider

# Default redirect location
defaultRedirect: winter/sso/providers

# Create page
create:
title: backend::lang.form.create_title
redirect: winter/sso/providers/update/:id
redirectClose: winter/sso/providers

# Update page
update:
title: backend::lang.form.update_title
redirect: winter/sso/providers
redirectClose: winter/sso/providers

# Preview page
preview:
title: backend::lang.form.preview_title
Loading