Add OAuth module from repository
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Erki Aas 2024-02-01 15:20:10 +02:00
parent 37db204cab
commit 7b5b15aebc
27 changed files with 474 additions and 2 deletions

View File

@ -53,9 +53,8 @@ RUN echo d
# Install freescout # Install freescout
RUN git clone -b add-s3-support https://github.com/veebkolm/freescout.git /app RUN git clone -b add-s3-support https://github.com/veebkolm/freescout.git /app
# Install oauth plugin # Install oauth plugin
RUN git clone https://github.com/bolsunovskyi/freescout-oauth.git /app/Modules/OAuth COPY modules/OAuth /app/Modules/OAuth
# Install Composer dependencies # Install Composer dependencies
RUN cd /app && composer install --no-dev && composer clear-cache RUN cd /app && composer install --no-dev && composer clear-cache

0
modules/OAuth/Config/.gitkeep Executable file
View File

13
modules/OAuth/Config/config.php Executable file
View File

@ -0,0 +1,13 @@
<?php
return [
'name' => 'OAuth',
'options' => [
'active' => ['default' => 'off'],
'client_id' => ['default' => 'freescout'],
'client_secret' => ['default' => ''],
'auth_url' => ['default' => 'https://'],
'token_url' => ['default' => 'https://'],
'user_url' => ['default' => 'https://']
],
];

0
modules/OAuth/Console/.gitkeep Executable file
View File

View File

View File

View File

@ -0,0 +1,21 @@
<?php
namespace Modules\OAuth\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
class OAuthDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Model::unguard();
// $this->call("OthersTableSeeder");
}
}

View File

View File

View File

View File

@ -0,0 +1,60 @@
<?php
namespace Modules\OAuth\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use \App\User;
use Illuminate\Support\Facades\Auth;
class OAuthController extends Controller
{
/**
* Display a listing of the resource.
* @return Response
*/
public function index(Request $request)
{
$settings = \Option::getOptions([
'oauth.active',
'oauth.client_id',
'oauth.client_secret',
'oauth.token_url',
'oauth.user_url',
]);
$ch = curl_init($settings['oauth.token_url']);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query([
'client_id' => $settings['oauth.client_id'],
'client_secret'=> $settings['oauth.client_secret'],
'grant_type' => 'authorization_code',
'code' => $request->get('code'),
'redirect_uri' => route('oauth_callback'),
]),
CURLINFO_HEADER_OUT => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded; charset=utf-8',
],
]);
$data = json_decode(curl_exec($ch), true);
$accessToken = $data['access_token'];
curl_setopt_array($ch, [
CURLOPT_URL => $settings['oauth.user_url'],
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
],
]);
$data = json_decode(curl_exec($ch), true);
$user = User::where('email','=', $data['email'])->first();
Auth::login($user);
return redirect($request->session()->get('url.intended', '/'));
}
}

View File

View File

8
modules/OAuth/Http/routes.php Executable file
View File

@ -0,0 +1,8 @@
<?php
Route::group(['middleware' => 'web',
'prefix' => \Helper::getSubdirectory(),
'namespace' => 'Modules\OAuth\Http\Controllers'], function()
{
Route::get('/oauth_callback', 'OAuthController@index')->name('oauth_callback');
});

View File

View File

@ -0,0 +1,194 @@
<?php
namespace Modules\OAuth\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Factory;
use Illuminate\Support\Facades\Auth;
define('SAMPLE_OAUTH', 'oauth');
class OAuthServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Boot the application events.
*
* @return void
*/
public function boot()
{
$this->registerConfig();
$this->registerViews();
$this->registerFactories();
$this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
$this->hooks();
}
/**
* Module hooks.
*/
public function hooks()
{
\Eventy::addFilter('settings.sections', function($sections) {
$sections['oauth'] = ['title' => __('OAuth'), 'icon' => 'user', 'order' => 700];
return $sections;
}, 30);
// Section settings
\Eventy::addFilter('settings.section_settings', function($settings, $section) {
if ($section != 'oauth') {
return $settings;
}
$settings = \Option::getOptions([
'oauth.active',
'oauth.client_id',
'oauth.client_secret',
'oauth.auth_url',
'oauth.token_url',
'oauth.user_url',
]);
return $settings;
}, 20, 2);
// Section parameters.
\Eventy::addFilter('settings.section_params', function($params, $section) {
if ($section != 'oauth') {
return $params;
}
$params = [
'template_vars' => [],
'validator_rules' => [],
];
return $params;
}, 20, 2);
// Settings view name
\Eventy::addFilter('settings.view', function($view, $section) {
if ($section != 'oauth') {
return $view;
} else {
return 'oauth::index';
}
}, 20, 2);
\Eventy::addFilter('middleware.web.custom_handle.response', function($prev, $rq, $next) {
$path = $rq->path();
$loggedIn = Auth::check();
$settings = \Option::getOptions([
'oauth.active',
'oauth.client_id',
'oauth.auth_url',
]);
if (!$rq->get('disable_oauth', false) && $path == 'login' && !$loggedIn &&
$settings['oauth.active'] == 'on') {
$con = '?';
if (strpos($settings['oauth.auth_url'], '?') !== false) {
$con = '&';
}
return redirect(
sprintf("%s%sclient_id=%s&response_type=code&redirect_uri=%s",
$settings['oauth.auth_url'],
$con,
$settings['oauth.client_id'],
route('oauth_callback')
)
);
}
return $prev;
}, 10, 3);
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerTranslations();
}
/**
* Register config.
*
* @return void
*/
protected function registerConfig()
{
$this->publishes([
__DIR__.'/../Config/config.php' => config_path('oauth.php'),
], 'config');
$this->mergeConfigFrom(
__DIR__.'/../Config/config.php', 'oauth'
);
}
/**
* Register views.
*
* @return void
*/
public function registerViews()
{
$viewPath = resource_path('views/modules/oauth');
$sourcePath = __DIR__.'/../Resources/views';
$this->publishes([
$sourcePath => $viewPath
],'views');
$this->loadViewsFrom(array_merge(array_map(function ($path) {
return $path . '/modules/oauth';
}, \Config::get('view.paths')), [$sourcePath]), 'oauth');
}
/**
* Register translations.
*
* @return void
*/
public function registerTranslations()
{
$this->loadJsonTranslationsFrom(__DIR__ .'/../Resources/lang');
}
/**
* Register an additional directory of factories.
* @source https://github.com/sebastiaanluca/laravel-resource-flow/blob/develop/src/Modules/ModuleServiceProvider.php#L66
*/
public function registerFactories()
{
if (! app()->environment('production')) {
app(Factory::class)->load(__DIR__ . '/../Database/factories');
}
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return [];
}
}

0
modules/OAuth/Public/.gitkeep Executable file
View File

32
modules/OAuth/README.md Normal file
View File

@ -0,0 +1,32 @@
# OAuth FreeScout
This module is intended to provide oauth authentication to freescout.
Module was tested on keycloak oauth provider with confidential openid-connect client.
Module is require php curl extension on server.
Currently module fully replace login form with redirection to oauth provider login form.
If you need to perform ordinary login with basic form, add `disable_oauth` get parameter to login path (`/login?disable_oauth=1`)
User must be registered before oauth login.
## Installation
- place module source to Modules folder of your FreeScout installation, module must have **OAuth** folder name to work propperly. If you are clonning repo with git, just add folder name in the end of git clone command.
- enable module in modules admin panel
- configure module on settings page (client id/secret/etc)
## Provider Specific
### Azure Active Directory (AAD)
Register an App Registration in Azure Active Directory with scopes `openid`, `email` and `profile`.
| Setting | Value |
| ------------------------------ | ------------------------------------------------------------------------------------------------ |
| **Client ID** | <_App Registration Client ID_> |
| **Client Secret** | <_App Registration Client secret_> |
| **Authorization Endpoint URL** | _https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize?scope=email+profile+openid_ |
| **Token Endpoint URL** | _https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token_ |
| **User Info Endpoint URL** | _https://graph.microsoft.com/oidc/userinfo_ |

View File

View File

View File

View File

@ -0,0 +1,69 @@
<form class="form-horizontal margin-top margin-bottom" method="POST" action="" id="oauth_form">
{{ csrf_field() }}
<div class="form-group{{ $errors->has('settings.oauth.active') ? ' has-error' : '' }} margin-bottom-10">
<label for="oauth.active" class="col-sm-2 control-label">{{ __('Active') }}</label>
<div class="col-sm-6">
<input id="oauth.active" type="checkbox" class=""
name="settings[oauth.active]"
@if (old('settings[oauth.active]', $settings['oauth.active']) == 'on') checked="checked" @endif
/>
</div>
</div>
<div class="form-group{{ $errors->has('settings.oauth.client_id') ? ' has-error' : '' }} margin-bottom-10">
<label for="oauth.client_id" class="col-sm-2 control-label">{{ __('Client ID') }}</label>
<div class="col-sm-6">
<input id="oauth.client_id" type="text" class="form-control input-sized-lg"
name="settings[oauth.client_id]" value="{{ old('settings.oauth.client_id', $settings['oauth.client_id']) }}">
@include('partials/field_error', ['field'=>'settings.oauth.client_id'])
</div>
</div>
<div class="form-group{{ $errors->has('settings.oauth.client_secret') ? ' has-error' : '' }} margin-bottom-10">
<label for="oauth.client_secret" class="col-sm-2 control-label">{{ __('Client Secret') }}</label>
<div class="col-sm-6">
<input id="oauth.client_secret" type="text" class="form-control input-sized-lg"
name="settings[oauth.client_secret]" value="{{ old('settings.oauth.client_secret', $settings['oauth.client_secret']) }}">
</div>
</div>
<div class="form-group{{ $errors->has('settings.oauth.auth_url') ? ' has-error' : '' }} margin-bottom-10">
<label for="oauth.auth_url" class="col-sm-2 control-label">{{ __('Authorization Endpoint URL') }}</label>
<div class="col-sm-6">
<input id="oauth.auth_url" type="text" class="form-control input-sized-lg"
name="settings[oauth.auth_url]" value="{{ old('settings.oauth.auth_url', $settings['oauth.auth_url']) }}">
</div>
</div>
<div class="form-group{{ $errors->has('settings.oauth.token_url') ? ' has-error' : '' }} margin-bottom-10">
<label for="oauth.token_url" class="col-sm-2 control-label">{{ __('Token Endpoint URL') }}</label>
<div class="col-sm-6">
<input id="oauth.token_url" type="text" class="form-control input-sized-lg"
name="settings[oauth.token_url]" value="{{ old('settings.oauth.token_url', $settings['oauth.token_url']) }}">
</div>
</div>
<div class="form-group{{ $errors->has('settings.oauth.user_url') ? ' has-error' : '' }} margin-bottom-10">
<label for="oauth.user_url" class="col-sm-2 control-label">{{ __('User Info Endpoint URL') }}</label>
<div class="col-sm-6">
<input id="oauth.user_url" type="text" class="form-control input-sized-lg"
name="settings[oauth.user_url]" value="{{ old('settings.oauth.user_url', $settings['oauth.user_url']) }}">
</div>
</div>
<div class="form-group">
<label for="oauth.user_url" class="col-sm-2 control-label">{{ __('OAuth callback URL') }}</label>
<a href="{{ route('oauth_callback') }}">{{ route('oauth_callback') }}</a>
</div>
<div class="form-group margin-top margin-bottom">
<div class="col-sm-6 col-sm-offset-2">
<button type="submit" class="btn btn-primary">
{{ __('Save') }}
</button>
</div>
</div>
</form>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Module OAuth</title>
</head>
<body>
@yield('content')
</body>
</html>

0
modules/OAuth/Tests/.gitkeep Executable file
View File

25
modules/OAuth/composer.json Executable file
View File

@ -0,0 +1,25 @@
{
"name": "freescout/oauth",
"description": "",
"authors": [
{
"name": "Mike Bolsunovskyi",
"email": "mike@bolsunovskyi.com"
}
],
"extra": {
"laravel": {
"providers": [
"Modules\\OAuth\\Providers\\OAuthServiceProvider"
],
"aliases": {
}
}
},
"autoload": {
"psr-4": {
"Modules\\OAuth\\": ""
}
}
}

22
modules/OAuth/module.json Executable file
View File

@ -0,0 +1,22 @@
{
"name": "OAuth",
"alias": "oauth",
"description": "",
"version": "1.0.0",
"detailsUrl": "",
"author": "",
"authorUrl": "",
"requiredAppVersion": "1.0.2",
"license": "AGPL-3.0",
"keywords": [],
"active": 0,
"order": 0,
"providers": [
"Modules\\OAuth\\Providers\\OAuthServiceProvider"
],
"aliases": {},
"files": [
"start.php"
],
"requires": []
}

17
modules/OAuth/start.php Executable file
View File

@ -0,0 +1,17 @@
<?php
/*
|--------------------------------------------------------------------------
| Register Namespaces And Routes
|--------------------------------------------------------------------------
|
| When a module starting, this file will executed automatically. This helps
| to register some namespaces like translator or view. Also this file
| will load the routes file for each module. You may also modify
| this file as you want.
|
*/
if (!app()->routesAreCached()) {
require __DIR__ . '/Http/routes.php';
}