Skip to content

Commit

Permalink
more craft v4
Browse files Browse the repository at this point in the history
  • Loading branch information
mattcdavis1 committed Apr 18, 2024
1 parent a0f124d commit fa0c5d1
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 51 deletions.
9 changes: 5 additions & 4 deletions src/OneLogin.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace unionco\onelogin;

use Craft;
use craft\base\Model;
use yii\base\Event;
use craft\base\Plugin;
use craft\web\UrlManager;
Expand All @@ -20,9 +21,9 @@ class OneLogin extends Plugin
public static $plugin = null;

/** @var string */
public $schemaVersion = '0.0.1';
public string $schemaVersion = '0.0.1';

public $hasCpSettings = true;
public bool $hasCpSettings = true;

/**
* @inheritdoc.
Expand Down Expand Up @@ -66,7 +67,7 @@ public function init()
/**
* @inheritdoc
*/
protected function createSettingsModel()
protected function createSettingsModel(): ?Model
{
return new Settings();
}
Expand All @@ -84,7 +85,7 @@ protected function createSettingsModel()
// ]
// );
// }
public function getSettingsResponse()
public function getSettingsResponse(): mixed
{
return Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('onelogin/settings/saml'));
}
Expand Down
97 changes: 81 additions & 16 deletions src/controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace unionco\onelogin\controllers;

use Craft;
use craft\elements\User;
use Monolog\Logger;
use SAML2\Response;
use craft\web\Request;
Expand All @@ -20,18 +21,28 @@
*/
class AuthController extends Controller
{
protected $allowAnonymous = true;
protected array|bool|int $allowAnonymous = self::ALLOW_ANONYMOUS_LIVE;

public function actionSsoRedirect()
{
$request = Craft::$app->getRequest();
$context = $request->getQueryParam('context');
$auth = OneLogin::$plugin->sp->getAuth();
$response = $auth->login($context);

return $response;
}

public function actionLogin()
{
/** @var Request */
$request = Craft::$app->getRequest();

/** @var ApiModule */
$module = ApiModule::getInstance();
// /** @var ApiModule */
// $module = ApiModule::getInstance();

/** @var LogService */
$log = $module->log;
// /** @var LogService */
// $log = $module->log;

/** @var Auth */
$auth = OneLogin::$plugin->sp->getAuth();
Expand All @@ -40,38 +51,92 @@ public function actionLogin()

if (!$auth->isAuthenticated()) {
$lastErrorReason = $auth->getLastErrorReason() ?? 'Unknown';
LogService::log($auth->getSettings(), 'one-login.log');
LogService::log($auth->getErrors(), 'one-login.log');
LogService::log($lastErrorReason, 'one-login.log');

$log->log($auth->getSettings(), 'SAML Auth Config');
$log->log($auth->getErrors(), 'SAML Auth - Errors', Logger::ERROR);
$log->log($lastErrorReason, 'SAML Auth - Last Error Reason', Logger::ERROR);
// $log->log($auth->getSettings(), 'SAML Auth Config');
// $log->log($auth->getErrors(), 'SAML Auth - Errors', Logger::ERROR);
// $log->log($lastErrorReason, 'SAML Auth - Last Error Reason', Logger::ERROR);
throw new UnauthorizedHttpException($lastErrorReason);
}

// Get the custom attributes, in this case we are only interested in the loginName
// to find the Craft User
$attributes = $auth->getAttributes();
if (!$loginName = $attributes['loginName'][0] ?? false) {
$log->log($attributes, 'loginName attribute not provided', Logger::ERROR);
$loginName = $auth->getAttribute('loginName')[0] ?? $auth->getAttribute('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress')[0] ?? null;
if (!$loginName) {
// $log->log($attributes, 'loginName attribute not provided', Logger::ERROR);
throw new BadRequestHttpException('LoginName is not provided');
}

$user = Craft::$app->getUsers()->getUserByUsernameOrEmail($loginName);
if (!$user) {
$log->log($loginName, 'Craft User lookup failed', Logger::ERROR);
// $log->log($loginName, 'Craft User lookup failed', Logger::ERROR);
throw new ServerErrorHttpException('Could not find user with login: ' . $loginName);
}

// Get the session duration
$generalConfig = Craft::$app->getConfig()->getGeneral();
$duration = $generalConfig->userSessionDuration;

if (!Craft::$app->getUser()->login($user, $duration)) {
$log->log($loginName, 'Failed to login SSO user', Logger::ERROR);
$userService = Craft::$app->getUser();
if (!$userService->login($user, $duration)) {
// $log->log($loginName, 'Failed to login SSO user', Logger::ERROR);
throw new UnauthorizedHttpException('Could not log in user');
}

$log->log(['loginName' => $loginName, 'craftUserId' => $user->id], 'Successfully logged in SSO user');
return $this->redirect('/');
$relayState = $request->getParam('RelayState');
if ($relayState == 'cms') {
return $this->redirect('https://' . $request->getHostName() . '/admin');
} else {
$token = ApiModule::$instance->getJWT()->buildToken($user);

$authJson = json_encode([
'success' => true,
'token' => (string) $token,
'user' => $user->transform()
]);
$ssoEncoded = base64_encode($authJson);

if (env('SSO_DEBUG')) {
LogService::log('[SSO Login Success]: ' . env('SSO_RETURN_URL', '/'), 'one-login.log');
LogService::log($authJson, 'one-login.log');
}

Craft::$app->response->getHeaders()->add('acbj-sso', $ssoEncoded);

return $this->redirect(env('SSO_RETURN_URL', '/') . '?sso=' . $ssoEncoded);
}

}

// https://dev.swiftpitch-cms.bizjournals.com/actions/onelogin/auth/login
public function actionLoginTest()
{
if (env('ENVIRONMENT') !== 'dev') {
return;
}

$_POST['SAMLResponse'] = file_get_contents(storage_path('onelogin/sample-response.http'));
$ssoSettings = [
'SSO_RETURN_URL' => env('SSO_RETURN_URL_DEBUG'),
'SSO_SAML_SP_ENTITY_ID' => env('SSO_SAML_SP_ENTITY_ID_DEBUG'),
'SSO_SAML_SP_CONSUMER_URL' => env('SSO_SAML_SP_CONSUMER_URL_DEBUG'),
'SSO_SAML_SP_LOGOUT_URL' => env('SSO_SAML_SP_LOGOUT_URL_DEBUG'),
'SSO_SAML_IDP_ENTITY_ID' => env('SSO_SAML_IDP_ENTITY_ID_DEBUG'),
'SSO_SAML_IDP_SSO_URL' => env('SSO_SAML_IDP_SSO_URL_DEBUG'),
'SSO_SAML_IDP_LOGOUT_URL' => env('SSO_SAML_IDP_LOGOUT_URL_DEBUG'),
'SSO_SAML_IDP_X509=' => env('SSO_SAML_IDP_X509_DEBUG'),
'SSO_PORTAL_URL' => env('SSO_PORTAL_URL_DEBUG'),
];
foreach($ssoSettings as $ssoSetting => $ssoValue) {
putenv($ssoSetting . '=' . $ssoValue);
$_SERVER[$ssoSetting] = $ssoValue;
$_ENV[$ssoSetting] = $ssoValue;
}


return $this->actionLogin();
}

private function handleSuccessfulLogin(bool $setNotice): \yii\web\Response
Expand Down
83 changes: 64 additions & 19 deletions src/models/SAMLSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

namespace unionco\onelogin\models;

use craft\base\Model;
use Craft;
use yii\base\InvalidConfigException;
use craft\base\Model;
use craft\helpers\App;
// use OneLogin\Saml2\Constants as OneLoginConstants;
// use yii\base\InvalidConfigException;

class SAMLSettings extends Model
{
/** @var bool Debug Mode */
public $debug = false;
public $debug = true;

/** @var string Local Onelogin base URL */
public $baseUrl;
Expand Down Expand Up @@ -38,14 +40,14 @@ class SAMLSettings extends Model
/** @var string Identity Provider | X.509 Certificate filepath or cert contents*/
public $idpX509Cert;

public function rules()
public function rules(): array
{
return [
[[
'baseUrl',
'spEntityId', 'spAcsUrl', 'spSloUrl',
'idpEntityId', 'idpSsoUrl', 'idpSloUrl', 'idpX509Cert',
], 'required'],
// [[
// 'baseUrl',
// 'spEntityId', 'spAcsUrl', 'spSloUrl',
// 'idpEntityId', 'idpSsoUrl', 'idpSloUrl', 'idpX509Cert',
// ], 'required'],
['spEntityId', 'string'],
['baseUrl', 'string'],
['spAcsUrl', 'string'],
Expand All @@ -60,15 +62,16 @@ public function getDebug()

public function getBaseUrl()
{
return Craft::parseEnv($this->baseUrl);
return App::parseEnv($this->baseUrl);
}

public function getSpAttributes()
{
return [
[
'name' => 'loginName',
'isRequired' => true,
// 'isRequired' => true,
'isRequired' => false,
'nameFormat' => 'string',
'friendlyName' => 'loginName',
'attributeValue' => '',
Expand All @@ -78,42 +81,42 @@ public function getSpAttributes()

public function getSpEntityId()
{
return Craft::parseEnv($this->spEntityId);
return App::parseEnv($this->spEntityId);
}

public function getSpAcsUrl()
{
return Craft::parseEnv($this->spAcsUrl);
return App::parseEnv($this->spAcsUrl);
}

// public function getIdpSsoUrl()
// {
// return Craft::parseEnv($this->idpSsoUrl);
// return App::parseEnv($this->idpSsoUrl);
// }

public function getSpSloUrl()
{
return Craft::parseEnv($this->spSloUrl);
return App::parseEnv($this->spSloUrl);
}

public function getIdpEntityId()
{
return Craft::parseEnv($this->idpEntityId);
return App::parseEnv($this->idpEntityId);
}

public function getIdpSsoUrl()
{
return Craft::parseEnv($this->idpSsoUrl);
return App::parseEnv($this->idpSsoUrl);
}

public function getIdpSloUrl()
{
return Craft::parseEnv($this->idpSloUrl);
return App::parseEnv($this->idpSloUrl);
}

public function getIdpX509Cert()
{
$certFilePath = Craft::parseEnv($this->idpX509Cert);
$certFilePath = App::parseEnv($this->idpX509Cert);

$cert = null;
if ($this->idpX509Cert && file_exists($certFilePath)) {
Expand Down Expand Up @@ -170,6 +173,10 @@ public function getIdpX509Cert()

public function getSettingsArray(): array
{
// https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
// https://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf
// https://learn.microsoft.com/en-us/azure/active-directory/develop/single-sign-on-saml-protocol

return [
// If 'strict' is True, then the PHP Toolkit will reject unsigned
// or unencrypted messages if it expects them signed or encrypted
Expand All @@ -187,6 +194,43 @@ public function getSettingsArray(): array
'baseurl' => $this->getBaseUrl(),

// Service Provider Data that we are deploying
'security' => [
// exact (Default) Must be the exact match of at least one of the authentication contexts specified.
// minimum Must be at least as strong (as deemed by the responder) as one of the authentication contexts specified.
// maximum Must be as strong as possible (as deemed by the responder) without exceeding the strength of at least one of the authentication contexts specified.
// better Must be stronger (as deemed by the responder) than any one of the authentication contexts specified.
// Not Specified Uses the default value EXACT.
// 'requestedAuthnContextComparison' => 'minimum',

'requestedAuthnContext' => [
'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos',
// 'urn:federation:authentication:windows',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorUnregistered',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecureRemotePassword',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:MobileOneFactorContract',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:MobileOneFactorUnregistered',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:PGP',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:SPKI',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:XMLDSig',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:SoftwarePKI',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:Telephony',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:NomadTelephony',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:PersonalTelephony',
// 'urn:oasis:names:tc:SAML:2.0:ac:classes:AuthenticatedTelephony',
],
],
'sp' => [
// Identifier of the SP entity (must be a URI)
'entityId' => $this->getSpEntityId(),
Expand Down Expand Up @@ -222,6 +266,7 @@ public function getSettingsArray(): array
// represent the requested subject.
// Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
// 'requestedAuthnContext' => false,

// Usually x509cert and privateKey of the SP are provided by files placed at
// the certs folder. But we can also provide them with the following parameters
Expand Down
Loading

0 comments on commit fa0c5d1

Please sign in to comment.