GWM : saml integration
This commit is contained in:
parent
76a41264e8
commit
3865a9b0e3
@ -571,3 +571,19 @@ $routes->group('test',function($routes){
|
||||
$routes->get('logo_renaming','TestingController::logo_renaming');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
//saml - routes
|
||||
$routes->group("employeeRest", ['filter' => ['appSignature','AuthApiRateLimitFilter'] ], function ($routes) {
|
||||
|
||||
$routes->post('login/start', 'SamlController::startLogin');
|
||||
$routes->get('saml/login', 'SamlController::login');
|
||||
$routes->match(['get', 'post'], 'saml/slo', 'SamlController::slo');
|
||||
|
||||
});
|
||||
|
||||
$routes->post('saml/acs', 'SamlController::acs');
|
||||
$routes->get('saml/logout', 'SamlController::logout');
|
||||
$routes->get('saml/metadata', 'SamlController::metadata');
|
||||
|
||||
|
||||
248
app/Controllers/SamlController.php
Normal file
248
app/Controllers/SamlController.php
Normal file
@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\SamlClientModel;
|
||||
use App\Models\UserModel;
|
||||
use OneLogin\Saml2\Auth as SamlAuth;
|
||||
use OneLogin\Saml2\Error as SamlError;
|
||||
use OneLogin\Saml2\Settings as SamlSettings;
|
||||
|
||||
class SamlController extends BaseController
|
||||
{
|
||||
|
||||
public function startLogin()
|
||||
{
|
||||
$email = $this->request->getPost('email');
|
||||
|
||||
if (! $email) {
|
||||
return $this->response->setStatusCode(400)->setBody('Email required');
|
||||
}
|
||||
|
||||
$email = trim((string) $email);
|
||||
$at = strrchr($email, '@');
|
||||
if ($at === false) {
|
||||
return $this->response->setStatusCode(400)->setBody('Invalid email');
|
||||
}
|
||||
|
||||
$domain = strtolower(substr($at, 1));
|
||||
|
||||
$model = new SamlClientModel();
|
||||
$samlClient = $model->getByDomain($domain);
|
||||
|
||||
if (! $samlClient) {
|
||||
return $this->response->setStatusCode(404)->setBody('SAML not configured for this domain');
|
||||
}
|
||||
|
||||
session()->set('saml_client_id', $samlClient['id']);
|
||||
|
||||
return redirect()->to(site_url('saml/login'));
|
||||
}
|
||||
private function loadMergedSettings(): array
|
||||
{
|
||||
$settings = require APPPATH . 'Libraries/Saml/settings.php';
|
||||
$advanced = require APPPATH . 'Libraries/Saml/advanced_settings.php';
|
||||
|
||||
return array_merge($settings, $advanced);
|
||||
}
|
||||
|
||||
private function getSamlAuth(): SamlAuth
|
||||
{
|
||||
$samlClientId = session()->get('saml_client_id');
|
||||
|
||||
if (! $samlClientId) {
|
||||
throw new \RuntimeException('SAML client not found in session');
|
||||
}
|
||||
|
||||
$model = new SamlClientModel();
|
||||
$samlClient = $model->find($samlClientId);
|
||||
|
||||
if (! $samlClient) {
|
||||
throw new \RuntimeException('Invalid SAML client');
|
||||
}
|
||||
|
||||
$settings = $this->loadMergedSettings();
|
||||
|
||||
$settings['idp'] = [
|
||||
'entityId' => $samlClient['saml_entity_id'],
|
||||
'singleSignOnService' => [
|
||||
'url' => $samlClient['saml_sso_url'],
|
||||
],
|
||||
'singleLogoutService' => [
|
||||
'url' => $samlClient['saml_slo_url'] ?? '',
|
||||
],
|
||||
'x509cert' => $samlClient['saml_x509_cert'],
|
||||
];
|
||||
|
||||
return new SamlAuth($settings);
|
||||
}
|
||||
|
||||
public function login()
|
||||
{
|
||||
try {
|
||||
$auth = $this->getSamlAuth();
|
||||
} catch (\Throwable $e) {
|
||||
return redirect()->to(site_url('login'))->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$url = $auth->login(null, [], false, false, true);
|
||||
|
||||
return redirect()->to($url);
|
||||
}
|
||||
|
||||
public function acs()
|
||||
{
|
||||
try {
|
||||
$auth = $this->getSamlAuth();
|
||||
} catch (\Throwable $e) {
|
||||
return redirect()->to(site_url('login'))->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$auth->processResponse();
|
||||
|
||||
if (! $auth->isAuthenticated()) {
|
||||
return $this->response->setJSON($auth->getErrors());
|
||||
}
|
||||
|
||||
$attributes = $auth->getAttributes();
|
||||
$nameId = $auth->getNameId();
|
||||
|
||||
$email = $this->resolveEmailFromSaml($nameId, $attributes);
|
||||
|
||||
$UserModel = new UserModel();
|
||||
$user = $UserModel->getUserByEmail($email);
|
||||
|
||||
if (! $user || $user->is_active === '0') {
|
||||
session()->remove('saml_client_id');
|
||||
|
||||
return redirect()->to(site_url('login'))->with('error', 'User not registered or inactive');
|
||||
}
|
||||
|
||||
$user_team = $UserModel->getUserTeamsByUserID($user->id);
|
||||
|
||||
session()->regenerate(true);
|
||||
|
||||
$session_data = [
|
||||
'isLoggedIn' => true,
|
||||
'userid' => $user->id,
|
||||
'userData' => $user,
|
||||
'userProfile' => null,
|
||||
'user_team' => $user_team,
|
||||
'saml_name_id' => $nameId,
|
||||
'saml_attrs' => $attributes,
|
||||
];
|
||||
set_session_data($session_data);
|
||||
set_session_data(['fingerprint' => generateFingerprint()]);
|
||||
|
||||
$this->getUserDeviceInfo($user->id, 'NhanceUser');
|
||||
|
||||
return redirect()->to(site_url('dashboard/view'));
|
||||
}
|
||||
|
||||
protected function getUserDeviceInfo(int $userId, string $type_of_user): void
|
||||
{
|
||||
$userAgent = $this->request->getUserAgent();
|
||||
$datd = [
|
||||
'user_id' => $userId,
|
||||
'user_type' => $type_of_user,
|
||||
'ip' => $this->request->getIPAddress(),
|
||||
'platform' => $userAgent->getPlatform(),
|
||||
'broswer' => $userAgent->getBrowser(),
|
||||
];
|
||||
$AuthHistoryModel = new \App\Models\AuthHistoryModel();
|
||||
$AuthHistoryModel->insert($datd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
private function resolveEmailFromSaml(?string $nameId, array $attributes): string
|
||||
{
|
||||
if ($nameId && filter_var($nameId, FILTER_VALIDATE_EMAIL)) {
|
||||
return $nameId;
|
||||
}
|
||||
|
||||
$keys = [
|
||||
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
|
||||
'http://schemas.microsoft.com/identity/claims/emailaddress',
|
||||
'email',
|
||||
'Email',
|
||||
'mail',
|
||||
];
|
||||
|
||||
foreach ($keys as $k) {
|
||||
if (! empty($attributes[$k][0])) {
|
||||
return (string) $attributes[$k][0];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($attributes as $vals) {
|
||||
if (is_array($vals) && ! empty($vals[0]) && filter_var($vals[0], FILTER_VALIDATE_EMAIL)) {
|
||||
return (string) $vals[0];
|
||||
}
|
||||
}
|
||||
|
||||
return (string) $nameId;
|
||||
}
|
||||
|
||||
public function logout()
|
||||
{
|
||||
try {
|
||||
$auth = $this->getSamlAuth();
|
||||
$url = $auth->logout(null, [], null, null, true);
|
||||
} catch (SamlError $e) {
|
||||
session()->destroy();
|
||||
|
||||
return redirect()->to(site_url('login'));
|
||||
} catch (\Throwable $e) {
|
||||
session()->destroy();
|
||||
|
||||
return redirect()->to(site_url('login'));
|
||||
}
|
||||
|
||||
session()->destroy();
|
||||
|
||||
return redirect()->to($url);
|
||||
}
|
||||
|
||||
public function slo()
|
||||
{
|
||||
try {
|
||||
$auth = $this->getSamlAuth();
|
||||
} catch (\Throwable $e) {
|
||||
return redirect()->to(site_url('login'));
|
||||
}
|
||||
|
||||
try {
|
||||
$redirectUrl = $auth->processSLO(false, null, false, null, true);
|
||||
} catch (SamlError $e) {
|
||||
return redirect()->to(site_url('login'));
|
||||
}
|
||||
|
||||
if (! empty($auth->getErrors())) {
|
||||
return $this->response->setJSON($auth->getErrors());
|
||||
}
|
||||
|
||||
if ($redirectUrl) {
|
||||
return redirect()->to($redirectUrl);
|
||||
}
|
||||
|
||||
return redirect()->to(site_url('login'));
|
||||
}
|
||||
|
||||
public function metadata()
|
||||
{
|
||||
$settings = $this->loadMergedSettings();
|
||||
|
||||
try {
|
||||
$samlSettings = new SamlSettings($settings, true);
|
||||
$metadata = $samlSettings->getSPMetadata();
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setStatusCode(500)->setBody($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->response
|
||||
->setHeader('Content-Type', 'application/xml; charset=utf-8')
|
||||
->setBody($metadata);
|
||||
}
|
||||
}
|
||||
28
app/Libraries/Saml/advanced_settings.php
Normal file
28
app/Libraries/Saml/advanced_settings.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'compress' => [
|
||||
'requests' => true,
|
||||
'responses' => true,
|
||||
],
|
||||
'security' => [
|
||||
'nameIdEncrypted' => false,
|
||||
'authnRequestsSigned' => false,
|
||||
'logoutRequestSigned' => false,
|
||||
'logoutResponseSigned' => false,
|
||||
'signMetadata' => false,
|
||||
'wantMessagesSigned' => false,
|
||||
'wantAssertionsEncrypted' => false,
|
||||
'wantAssertionsSigned' => false,
|
||||
'wantNameId' => true,
|
||||
'wantNameIdEncrypted' => false,
|
||||
'requestedAuthnContext' => false,
|
||||
'wantXMLValidation' => true,
|
||||
'relaxDestinationValidation' => false,
|
||||
'destinationStrictlyMatches' => false,
|
||||
'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
|
||||
'digestAlgorithm' => 'http://www.w3.org/2001/04/xmlenc#sha256',
|
||||
'lowercaseUrlencoding' => false,
|
||||
],
|
||||
// Add contactPerson/organization only when fully populated.
|
||||
];
|
||||
20
app/Libraries/Saml/certs/sp.crt
Normal file
20
app/Libraries/Saml/certs/sp.crt
Normal file
@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDTzCCAjegAwIBAgIUGlFK78mvjuT7qCQCzb31hoEopdUwDQYJKoZIhvcNAQEL
|
||||
BQAwNzEXMBUGA1UEAwwObmhhbmNlLXNhbWwtc3AxDzANBgNVBAoMBk5IQU5DRTEL
|
||||
MAkGA1UEBhMCSU4wHhcNMjYwNDAzMDYzMDU2WhcNMjcwNDAzMDYzMDU2WjA3MRcw
|
||||
FQYDVQQDDA5uaGFuY2Utc2FtbC1zcDEPMA0GA1UECgwGTkhBTkNFMQswCQYDVQQG
|
||||
EwJJTjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJIC9MYFsI24T1/4
|
||||
AFo+OM9BuHec92t5YVj16+cKQ4Nozq7KccnDJg8S9shU75+aCr4X6wmdVq6cJfBA
|
||||
UaBh1QdIFPJZIwjCUGLnpN+LYX6szFt3YjiRya7Yb3WOQGuMON5i7If/QR5aAHA+
|
||||
bRsXPu2XGBnLoZOdFhxRogCy5bXAPug668mJPcMh7/pJdkuAFg7Kdnso8PUsjkAf
|
||||
D83GHutX0U+8o2thzM9apWv8/7f7WGxR/COyWszZ++xKpxt7k6oehnoHJhMqkEiD
|
||||
BCBNcKc7eHnxiR/SpIMqbbuDPp0eUo1ohT17hJXeAo9eFjbH9cstmv+ADuzm9YyH
|
||||
tfj1ohcCAwEAAaNTMFEwHQYDVR0OBBYEFKPaXcBR6Zsnq/mSl8kuXNS6doszMB8G
|
||||
A1UdIwQYMBaAFKPaXcBR6Zsnq/mSl8kuXNS6doszMA8GA1UdEwEB/wQFMAMBAf8w
|
||||
DQYJKoZIhvcNAQELBQADggEBAHndqjXvGdTj5Bu2X1H+/SVeq9gYvX3jJh7x2Sam
|
||||
RaVq9swZS6EPfZP6oMOmzkgVzQBPk/QRHlXpuVjdMn4shdWUhNJJ1CGiQPNmg2el
|
||||
cYhnwCcRcf/MO4a2zyiv3ZhRW8BmhnnxYLFZuS6F95RMbQgXMfJAwQEX29L8L/w9
|
||||
Js93wqqObjFh8kaJRKCrLtph736Dj6juB+o6Lx1tMpZbTssXFiVHtlqn6/vO2R0i
|
||||
BMsbHNcSp6gkNRituEMlO9pto4HBDXeBD4gC28BmXnqem7LoMmeLVH9xcD9KFoCq
|
||||
DXYNYOfCjZc6z/mdbDmKpWvyQAlaqrekyZZaMTPzCl3c5w0=
|
||||
-----END CERTIFICATE-----
|
||||
28
app/Libraries/Saml/certs/sp.key
Normal file
28
app/Libraries/Saml/certs/sp.key
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCSAvTGBbCNuE9f
|
||||
+ABaPjjPQbh3nPdreWFY9evnCkODaM6uynHJwyYPEvbIVO+fmgq+F+sJnVaunCXw
|
||||
QFGgYdUHSBTyWSMIwlBi56Tfi2F+rMxbd2I4kcmu2G91jkBrjDjeYuyH/0EeWgBw
|
||||
Pm0bFz7tlxgZy6GTnRYcUaIAsuW1wD7oOuvJiT3DIe/6SXZLgBYOynZ7KPD1LI5A
|
||||
Hw/Nxh7rV9FPvKNrYczPWqVr/P+3+1hsUfwjslrM2fvsSqcbe5OqHoZ6ByYTKpBI
|
||||
gwQgTXCnO3h58Ykf0qSDKm27gz6dHlKNaIU9e4SV3gKPXhY2x/XLLZr/gA7s5vWM
|
||||
h7X49aIXAgMBAAECggEAGy8ctk9x3vjNIl9wZVzHQ+MG/pIFSIepNaBXgsTY6/rT
|
||||
3BwJ0lgYWl8b/hE+KbdKv7iBRGF8NXcR4yh+af8846WqbLJmwOc4gymAezQeezCd
|
||||
vXu9GC4gYAKgwcCxwrQxFEpTokBGNenowf0FYDFUQHTMgmT0mKB68NvL7xhfhxZT
|
||||
P+hdQiCb8cZvGrdmcfUC67hlXSH9ePKQkb8H5e2HULpOiShoZEeoZk+bRq22YewP
|
||||
bZ+D0f8Eez5WUSKLRqeasGQvhsAwX491TsP0QeBWILp0KTiKCufuDW9bE1Vvb8h2
|
||||
xFepUhKvCLT0ZboKhl20K4P6NPOFYaO9Tcr5inTmRQKBgQDEfeSDe5N9563UcLy2
|
||||
X325E84xguyEDhyBmrJ3JcmiPnLorSCLaX9CZUi4+BMLN+5bJG7iz4jgfJp/y9ns
|
||||
VPPxZyXAJ2RicJkhiLmARWHAo317booyqJDH+/I9gxY5l0fCDMoMLbeEOBsJIlAk
|
||||
zeTEIIJkr5o2xz+7dLeCdujGDQKBgQC+O05g82tLTLDRliYYvNLlbPf8zgkn8yLx
|
||||
RRkkKO/svXokKLttiwbACuED2eR07seA3AZb1qAdj7V7HBb20axamdtZvKCfHMEw
|
||||
LdZwW1qimIxhkkwGgiBC3bHYvG2BytohU/kQx+0vWFaYOkK8ihsrrrcNILpKjaH2
|
||||
PV0M4iUDswKBgBY4MkYYDFa5gzO5x+1LoRjzv2Zj6sEII3sYdkP49vMs4qujIEID
|
||||
nQtyDqY0D1s+aOrPlOZ7F3xjOslm0O7jsG5E/sTa74QePYLIRknWDrbNBhyWJHSU
|
||||
EUM8H2mLUFEU5V1xOsvjw5PlEFGZGrz+t3biQjyGiwbUw0U8bqAHOE1lAoGAYoZm
|
||||
3tHUFUjgH3zruE470HWyru2rUlScGWfXUKIfOXcdRpMOF/s0gMxhpFP6/hEZpQTQ
|
||||
CkrL3OOsc9mljyojYT1knUKT0jTbXe+vq7u04petxW83DvvgZ6FY1k8pTFraxP4v
|
||||
9mAF2UqgdvFd1TaWQfaYeiUkNy7J3rYDdoO99f8CgYB0GYjD5XzDG4YoftVv4Za5
|
||||
WPl5MJhiVLuZfvg0aPUkL/J38nQJ66x287wW7aFMr5E86d6QWuVDeE9n3aAnIE8J
|
||||
NNPpJtFpe3dl/I/XJId0xWiFGyyTz5D7XP9pljlfz6ZV17cZlqvQf6gXg2/ELgtu
|
||||
A8li+XiGe3QPsoZWYZMd4Q==
|
||||
-----END PRIVATE KEY-----
|
||||
45
app/Libraries/Saml/settings.php
Normal file
45
app/Libraries/Saml/settings.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SAML SP settings and IdP placeholders.
|
||||
* IdP values are replaced at runtime in SamlController::getSamlAuth() from `saml_client`.
|
||||
*/
|
||||
$app = config('App');
|
||||
$base = rtrim((string) base_url(), '/');
|
||||
|
||||
$certDir = APPPATH . 'Libraries/Saml/certs/';
|
||||
$spCert = is_file($certDir . 'sp.crt') ? file_get_contents($certDir . 'sp.crt') : '';
|
||||
$spKey = is_file($certDir . 'sp.key') ? file_get_contents($certDir . 'sp.key') : '';
|
||||
|
||||
return [
|
||||
'strict' => true,
|
||||
'debug' => false,
|
||||
'baseurl' => $base . '/',
|
||||
|
||||
'sp' => [
|
||||
'entityId' => $base . '/saml/metadata',
|
||||
'assertionConsumerService' => [
|
||||
'url' => $base . '/saml/acs',
|
||||
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
|
||||
],
|
||||
'singleLogoutService' => [
|
||||
'url' => $base . '/saml/slo',
|
||||
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||
],
|
||||
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||
'x509cert' => $spCert,
|
||||
'privateKey' => $spKey,
|
||||
],
|
||||
|
||||
// Placeholder IdP (must pass Settings validation; replaced before Auth by DB row)
|
||||
'idp' => [
|
||||
'entityId' => $base . '/saml/placeholder-idp',
|
||||
'singleSignOnService' => [
|
||||
'url' => $base . '/saml/placeholder-sso',
|
||||
],
|
||||
'singleLogoutService' => [
|
||||
'url' => '',
|
||||
],
|
||||
'x509cert' => $spCert,
|
||||
],
|
||||
];
|
||||
31
app/Models/SamlClientModel.php
Normal file
31
app/Models/SamlClientModel.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class SamlClientModel extends Model
|
||||
{
|
||||
protected $table = 'saml_client';
|
||||
protected $primaryKey = 'id';
|
||||
protected $returnType = 'array';
|
||||
protected $allowedFields = [
|
||||
'client_id',
|
||||
'email_domain',
|
||||
'saml_entity_id',
|
||||
'saml_sso_url',
|
||||
'saml_slo_url',
|
||||
'saml_x509_cert',
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
protected $dateFormat = 'datetime';
|
||||
|
||||
public function getByDomain(string $domain): ?array
|
||||
{
|
||||
$domain = strtolower(trim($domain));
|
||||
|
||||
return $this->where('LOWER(email_domain)', $domain)->first();
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,7 @@
|
||||
"google/apiclient": "^2.18",
|
||||
"kreait/firebase-php": "^7.0",
|
||||
"laminas/laminas-escaper": "^2.9",
|
||||
"onelogin/php-saml": "^4.3",
|
||||
"php-amqplib/php-amqplib": "^2.8",
|
||||
"phpmailer/phpmailer": "^6.9",
|
||||
"phpoffice/phpspreadsheet": "^2.1",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user