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

Rebase #213

Merged
merged 17 commits into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,31 @@ MAIL_PASSWORD=
# MAIL_DKIM_PRIVATE = '/path/to/private/key';
# MAIL_DKIM_SELECTOR = 'default'; # Match your DKIM DNS selector
# MAIL_DKIM_PASSPHRASE = ''; # Only if your key has a passphrase

# List of socialite providers separated by a space. Possible value : keycloak, oidc
SOCIALITE_PROVIDERS=""

KEYCLAOK_DISPLAY_NAME="Keycloak"
KEYCLOAK_ALLOW_CREATE_USER=false
KEYCLOAK_ALLOW_UPDATE_USER=false
KEYCLOAK_DEFAULT_ROLE="auditee"
KEYCLOAK_ROLE_CLAIM="resource_access.deming.roles.0"
KEYCLOAK_ADDITIONAL_SCOPES="roles"

KEYCLOAK_CLIENT_ID=deming
KEYCLOAK_CLIENT_SECRET=secret
KEYCLOAK_REDIRECT_URI=${APP_URL}auth/callback/keycloak
KEYCLOAK_BASE_URL=https://keycloak.local
KEYCLOAK_REALM=main

OIDC_DISPLAY_NAME="Generic OIDC"
OIDC_ALLOW_CREATE_USER=false
OIDC_ALLOW_UPDATE_USER=false
OIDC_DEFAULT_ROLE="auditee"
OIDC_ROLE_CLAIM=""
OIDC_ADDITIONAL_SCOPES="deming_role"

OIDC_CLIENT_ID=deming
OIDC_CLIENT_SECRET=deming
OIDC_BASE_URL=http://auth.lan
OIDC_REDIRECT_URI=${APP_URL}auth/callback/oidc
251 changes: 251 additions & 0 deletions app/Http/Controllers/SocialiteController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Log;
use Laravel\Socialite\Two\User as SocialiteUser;
use App\Http\Controllers\Controller;
use App\Models\User;

/**
* Socialite Controller for OpenID Connect Autentication
*/
class SocialiteController extends Controller
{

public const ROLES_MAP = [
//'admin' => '1',
'user' => '2',
'auditee' => '5',
'auditor' => '3',
//'api' => '4',
];

public const LOCALES = ['en', 'fr'];

public function __construct()
{
$this->middleware('auth:api', ['except' => ['redirect', 'callback']]);
}

/**
* Redirect action use to redirect user to OIDC provider.
*/
public function redirect(string $provider)
{
$providers = config('services.socialite_controller.providers', []);

if (in_array($provider, $providers)) {
Log::debug("Redirect with '$provider' provider");
$config_name = 'services.socialite_controller.'.$provider;
$additional_scopes = config($config_name.'.additional_scopes');
return Socialite::with($provider)->scopes($additional_scopes)->redirect();
}

Log::warning("Redirect: Provider '$provider' not found.");
abort(404);
}

/**
* Callback action use when OIDC provider redirect user to app.
*/
public function callback(Request $request, string $provider)
{
$providers = config('services.socialite_controller.providers', []);

if (! in_array($provider, $providers)) {
Log::warning("Callback: Provider '$provider' not found.");
abort(404);
}

Log::debug("Callback provider : '$provider'");

// Get additionnal config for current provider
$config_name = 'services.socialite_controller.'.$provider;
$allow_create_user = false;
$allow_update_user = false;
if(config($config_name)){
$allow_create_user = config($config_name.'.allow_create_user', $allow_create_user);
$allow_update_user = config($config_name.'.allow_update_user', $allow_update_user);
}
Log::debug('CONFIG: allow_create_user='.($allow_create_user ? 'true' : 'false'));
Log::debug('CONFIG: allow_update_user='.($allow_update_user ? 'true' : 'false'));
if($allow_create_user || $allow_update_user){
$role_claim = config($config_name.'.role_claim', '');
Log::debug('CONFIG: role_claim='.$role_claim);
$default_role = config($config_name.'.default_role', '');
Log::debug('CONFIG: default_role='.$default_role);
}

try {
$socialite_user = Socialite::with($provider)->user();
$user = null;

// Search user by email
if($socialite_user->email){
$user = User::query()->whereEmail($socialite_user->email)->first();
} else {
Log::warning("User has no attribute email");
}

// If not exist and allow to create user then create it
if (!$user && $allow_create_user) {
$user = $this->create_user($socialite_user, $provider, $role_claim, $default_role);
}

// If no user redirect to login with error message
if (!$user) {
Log::warning("User [$socialite_user->id, $socialite_user->email] not found in deming database");
return redirect('login')->withErrors(['socialite' => trans('cruds.login.error.user_not_exist') ]);
}

if($allow_update_user){
$this->update_user($user, $socialite_user, $provider, $role_claim, $default_role);
}

Log::info("User '$user->login' login with $provider provider");

Auth::guard('web')->login($user);

return redirect('/');
} catch (Exception $exception) {
return redirect('login');
}
}

/**
* Create user with claims provided.
*/
protected function create_user(SocialiteUser $socialite_user, string $provider, string $role_claim, string $default_role)
{
$user = new User();

$user->login = $this->get_user_login($socialite_user);
$user->name = $socialite_user->name;
$user->email = $socialite_user->email;
$user->title = "User provide by $provider";
$user->role = $this->get_user_role($socialite_user, $role_claim, $default_role);
$user->language = $this->get_user_langage($socialite_user);

// TODO allow null password
$user->password = bin2hex(random_bytes(32));

Log::info("Create new user '$user->login' with role '$user->role' from $provider provider");
try {
$user->save();
} catch(QueryException $exception){
Log::debug($exception->getMessage());
Log::error("Unable to create user");
return null;
}

return $user;
}

/**
* Update user with claims providid.
*/
protected function update_user(User $user, SocialiteUser $socialite_user, string $provider, string $role_claim, string $default_role)
{
$updated = false;

$login = $this->get_user_login($socialite_user);
if ($login !== $user->login) {
Log::debug("Login changed $user->login => $login");
$user->login = $login;
$updated = true;
}

if ($socialite_user->name !== $user->name) {
Log::debug("Name changed $user->name => $socialite_user->name");
$user->name = $socialite_user->name;
$updated = true;
}

$role = $this->get_user_role($socialite_user, $role_claim, $default_role);
if($role != $user->role){
Log::debug("Role changed $user->role => $role");
$user->role = $role;
$updated = true;
}

$language = $this->get_user_langage($socialite_user);
if ($language !== $user->language) {
Log::debug("Lauguage change $user->language => $language");
$user->language = $language;
$updated = true;
}

if ($updated) {
Log::info("Update user '$user->login' with role '$user->role' from $provider provider");
$user->save();
}
return $user;
}

/**
* Return user's login.
*/
private function get_user_login(SocialiteUser $socialite_user)
{
// set login with preferred_username, otherwise use id
if($socialite_user->offsetExists('preferred_username')){
return $socialite_user->offsetGet("preferred_username");
}
return $socialite_user->id;
}

/**
* Return user's role.
* If no role provided, use $default_role value.
* If $default_role is null and no role provided, null return.
*/
private function get_user_role(SocialiteUser $socialite_user, string $role_claim, string $default_role)
{
$role_name = "";
if(!empty($role_claim)){
$role_name = $this->get_claim_value($socialite_user, $role_claim);
Log::debug("Provided claim '$role_claim'='$role_name'");
}
if(!array_key_exists($role_name, self::ROLES_MAP)){
if(!empty($default_role)){
$role_name = $default_role;
} else {
Log::error("No default role set! A valid role must be provided. role='$role_name'");
return null;
}
}
return self::ROLES_MAP[$role_name];
}

/**
* Return user's language.
* Use locale claim to dertermine user's language.
*/
private function get_user_langage(SocialiteUser $socialite_user)
{
if ($socialite_user->offsetExists('locale')){
$locale = explode('-', $socialite_user->offsetGet('locale'))[0];
if (in_array($locale, self::LOCALES)) return $locale;
}
return self::LOCALES[0];
}

private function get_claim_value(SocialiteUser $user, string $claim){
$value = null;
foreach(explode('.', $claim) as $offset) {
if(! $value){
if (! $user->offsetExists($offset)) return null;
$value = $user->offsetGet($offset);
continue;
}
if (! array_key_exists($offset, $value)) return null;
$value = $value[$offset];
}
return $value;
}
}
26 changes: 26 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use DB;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
Expand Down Expand Up @@ -40,5 +41,30 @@ public function boot()
);
});
}

if (in_array('keycloak', Config::get('services.socialite_controller.providers'))){
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('keycloak', \SocialiteProviders\Keycloak\Provider::class);
});
}

if (in_array('oidc', Config::get('services.socialite_controller.providers'))){
$this->bootOIDCSocialite();
}
}

/**
* Register Generic OpenID Connect Provider.
*/
private function bootOIDCSocialite()
{
$socialite = $this->app->make('Laravel\Socialite\Contracts\Factory');
$socialite->extend(
'oidc',
function ($app) use ($socialite) {
$config = $app['config']['services.oidc'];
return $socialite->buildProvider(\App\Providers\Socialite\GenericSocialiteProvider::class, $config);
}
);
}
}
Loading
Loading