GWM : saml integration

This commit is contained in:
Gowtham M 2026-04-03 15:17:00 +05:30
parent 76a41264e8
commit 3865a9b0e3
8 changed files with 417 additions and 0 deletions

View File

@ -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');

View 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);
}
}

View 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.
];

View 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-----

View 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-----

View 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,
],
];

View 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();
}
}

View File

@ -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",