Request: loginRequired => loginRequirements, SsoRequest, SAML
This commit is contained in:
parent
50cc0fc5be
commit
2861eaa9a9
@ -9,7 +9,7 @@ namespace Core\API {
|
|||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||||
parent::__construct($context, $externalCall, $params);
|
parent::__construct($context, $externalCall, $params);
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function fetchAPIKey(int $apiKeyId): ApiKey|bool {
|
protected function fetchAPIKey(int $apiKeyId): ApiKey|bool {
|
||||||
|
@ -7,7 +7,7 @@ namespace Core\API {
|
|||||||
abstract class GpgKeyAPI extends \Core\API\Request {
|
abstract class GpgKeyAPI extends \Core\API\Request {
|
||||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||||
parent::__construct($context, $externalCall, $params);
|
parent::__construct($context, $externalCall, $params);
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,7 +34,6 @@ namespace Core\API\GpgKey {
|
|||||||
parent::__construct($context, $externalCall, [
|
parent::__construct($context, $externalCall, [
|
||||||
"publicKey" => new StringType("publicKey")
|
"publicKey" => new StringType("publicKey")
|
||||||
]);
|
]);
|
||||||
$this->loginRequired = true;
|
|
||||||
$this->forbidMethod("GET");
|
$this->forbidMethod("GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +124,6 @@ namespace Core\API\GpgKey {
|
|||||||
parent::__construct($context, $externalCall, array(
|
parent::__construct($context, $externalCall, array(
|
||||||
"password" => new StringType("password")
|
"password" => new StringType("password")
|
||||||
));
|
));
|
||||||
$this->loginRequired = true;
|
|
||||||
$this->forbidMethod("GET");
|
$this->forbidMethod("GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +157,6 @@ namespace Core\API\GpgKey {
|
|||||||
parent::__construct($context, $externalCall, [
|
parent::__construct($context, $externalCall, [
|
||||||
"token" => new StringType("token", 36)
|
"token" => new StringType("token", 36)
|
||||||
]);
|
]);
|
||||||
$this->loginRequired = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -209,7 +206,6 @@ namespace Core\API\GpgKey {
|
|||||||
"id" => new Parameter("id", Parameter::TYPE_INT, true, null),
|
"id" => new Parameter("id", Parameter::TYPE_INT, true, null),
|
||||||
"format" => new StringType("format", 16, true, "ascii")
|
"format" => new StringType("format", 16, true, "ascii")
|
||||||
));
|
));
|
||||||
$this->loginRequired = true;
|
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ namespace Core\API\Language {
|
|||||||
use Core\API\Parameter\Parameter;
|
use Core\API\Parameter\Parameter;
|
||||||
use Core\API\Parameter\RegexType;
|
use Core\API\Parameter\RegexType;
|
||||||
use Core\API\Parameter\StringType;
|
use Core\API\Parameter\StringType;
|
||||||
|
use Core\API\Request;
|
||||||
use Core\Driver\SQL\Condition\Compare;
|
use Core\Driver\SQL\Condition\Compare;
|
||||||
use Core\Driver\SQL\Condition\CondOr;
|
use Core\Driver\SQL\Condition\CondOr;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
@ -126,7 +127,7 @@ namespace Core\API\Language {
|
|||||||
"modules" => new ArrayType("modules", Parameter::TYPE_STRING, true, false),
|
"modules" => new ArrayType("modules", Parameter::TYPE_STRING, true, false),
|
||||||
"compression" => new StringType("compression", -1, true, NULL, ["gzip", "zlib"])
|
"compression" => new StringType("compression", -1, true, NULL, ["gzip", "zlib"])
|
||||||
]);
|
]);
|
||||||
$this->loginRequired = false;
|
$this->loginRequirements = Request::NONE;
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,11 @@ use PhpMqtt\Client\MqttClient;
|
|||||||
// TODO: many things are only checked for external calls, e.g. loginRequired. If we call the API internally, we might get null-pointers for $context->user
|
// TODO: many things are only checked for external calls, e.g. loginRequired. If we call the API internally, we might get null-pointers for $context->user
|
||||||
abstract class Request {
|
abstract class Request {
|
||||||
|
|
||||||
|
// Login Requirements
|
||||||
|
const NONE = 0;
|
||||||
|
const LOGGED_IN = 1;
|
||||||
|
const NOT_LOGGED_IN = 2;
|
||||||
|
|
||||||
protected Context $context;
|
protected Context $context;
|
||||||
protected Logger $logger;
|
protected Logger $logger;
|
||||||
protected array $params;
|
protected array $params;
|
||||||
@ -21,7 +26,7 @@ abstract class Request {
|
|||||||
protected array $result;
|
protected array $result;
|
||||||
protected bool $success;
|
protected bool $success;
|
||||||
protected bool $isPublic;
|
protected bool $isPublic;
|
||||||
protected bool $loginRequired;
|
protected int $loginRequirements;
|
||||||
protected bool $variableParamCount;
|
protected bool $variableParamCount;
|
||||||
protected bool $isDisabled;
|
protected bool $isDisabled;
|
||||||
protected bool $apiKeyAllowed;
|
protected bool $apiKeyAllowed;
|
||||||
@ -47,9 +52,9 @@ abstract class Request {
|
|||||||
// restrictions
|
// restrictions
|
||||||
$this->isPublic = true;
|
$this->isPublic = true;
|
||||||
$this->isDisabled = false;
|
$this->isDisabled = false;
|
||||||
$this->loginRequired = false;
|
$this->loginRequirements = self::NONE;
|
||||||
$this->apiKeyAllowed = true;
|
$this->apiKeyAllowed = true;
|
||||||
$this->allowedMethods = array("GET", "POST");
|
$this->allowedMethods = ["GET", "POST"];
|
||||||
$this->csrfTokenRequired = true;
|
$this->csrfTokenRequired = true;
|
||||||
$this->rateLimiting = null;
|
$this->rateLimiting = null;
|
||||||
}
|
}
|
||||||
@ -270,8 +275,8 @@ abstract class Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->loginRequirements === self::LOGGED_IN) {
|
||||||
// Logged in or api key authorized?
|
// Logged in or api key authorized?
|
||||||
if ($this->loginRequired) {
|
|
||||||
if (!$session && !$apiKeyAuthorized) {
|
if (!$session && !$apiKeyAuthorized) {
|
||||||
$this->lastError = 'You are not logged in.';
|
$this->lastError = 'You are not logged in.';
|
||||||
$this->result["loggedIn"] = false;
|
$this->result["loggedIn"] = false;
|
||||||
@ -281,6 +286,12 @@ abstract class Request {
|
|||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if ($this->loginRequirements === self::NOT_LOGGED_IN) {
|
||||||
|
// Request only for unauthenticated users, e.g. login endpoint
|
||||||
|
if ($session || $apiKeyAuthorized) {
|
||||||
|
$this->lastError = "You are already logged in.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSRF Token
|
// CSRF Token
|
||||||
@ -383,7 +394,7 @@ abstract class Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function loginRequired(): bool {
|
public function loginRequired(): bool {
|
||||||
return $this->loginRequired;
|
return $this->loginRequirements === self::LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isExternalCall(): bool {
|
public function isExternalCall(): bool {
|
||||||
|
@ -41,26 +41,39 @@ namespace Core\API\Sso {
|
|||||||
|
|
||||||
use Core\API\Parameter\StringType;
|
use Core\API\Parameter\StringType;
|
||||||
use Core\API\Parameter\UuidType;
|
use Core\API\Parameter\UuidType;
|
||||||
|
use Core\API\Request;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
use Core\API\SsoAPI;
|
use Core\API\SsoAPI;
|
||||||
use Core\Objects\DatabaseEntity\Group;
|
use Core\Objects\DatabaseEntity\Group;
|
||||||
use Core\Objects\DatabaseEntity\SsoProvider;
|
use Core\Objects\DatabaseEntity\SsoProvider;
|
||||||
|
use Core\Objects\RateLimiting;
|
||||||
|
use Core\Objects\RateLimitRule;
|
||||||
|
use Core\Objects\SSO\SAMLResponse;
|
||||||
|
|
||||||
class GetProviders extends SsoAPI {
|
class GetProviders extends SsoAPI {
|
||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, []);
|
parent::__construct($context, $externalCall, []);
|
||||||
// TODO: auto-generated method stub
|
$this->csrfTokenRequired = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
// TODO: auto-generated method stub
|
|
||||||
return $this->success;
|
$sql = $this->context->getSQL();
|
||||||
|
$query = SsoProvider::createBuilder($sql, false);
|
||||||
|
|
||||||
|
if (!$this->context->getUser()) {
|
||||||
|
$query->whereTrue("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
$providers = SsoProvider::findBy($query);
|
||||||
|
$this->result["providers"] = SsoProvider::toJsonArray($providers);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getDescription(): string {
|
public static function getDescription(): string {
|
||||||
// TODO: auto generated endpoint description
|
// TODO: auto generated endpoint description
|
||||||
return "Short description, what users are able to do with this endpoint.";
|
return "Allows users to get a list of SSO providers. Unauthenticated users will only see active providers.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,14 +151,13 @@ namespace Core\API\Sso {
|
|||||||
"redirect" => new StringType("redirect", StringType::UNLIMITED, true, null)
|
"redirect" => new StringType("redirect", StringType::UNLIMITED, true, null)
|
||||||
]);
|
]);
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
|
$this->loginRequirements = Request::NOT_LOGGED_IN;
|
||||||
|
$this->rateLimiting = new RateLimiting(
|
||||||
|
new RateLimitRule(5, 1, RateLimitRule::MINUTE)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
|
|
||||||
if ($this->context->getUser()) {
|
|
||||||
return $this->createError("You are already logged in.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$redirectUrl = $this->getParam("redirect");
|
$redirectUrl = $this->getParam("redirect");
|
||||||
if (!$this->validateRedirectURL($redirectUrl)) {
|
if (!$this->validateRedirectURL($redirectUrl)) {
|
||||||
return $this->createError("Invalid redirect URL");
|
return $this->createError("Invalid redirect URL");
|
||||||
@ -186,24 +198,30 @@ namespace Core\API\Sso {
|
|||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, [
|
parent::__construct($context, $externalCall, [
|
||||||
"SAMLResponse" => new StringType("SAMLResponse"),
|
"SAMLResponse" => new StringType("SAMLResponse")
|
||||||
"provider" => new UuidType("provider"),
|
|
||||||
"redirect" => new StringType("redirect", StringType::UNLIMITED, true, null)
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
|
$this->loginRequirements = Request::NOT_LOGGED_IN;
|
||||||
$this->forbidMethod("GET");
|
$this->forbidMethod("GET");
|
||||||
|
$this->rateLimiting = new RateLimiting(
|
||||||
|
new RateLimitRule(15, 1, RateLimitRule::MINUTE)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
|
|
||||||
if ($this->context->getUser()) {
|
|
||||||
return $this->createError("You are already logged in.");
|
$samlResponseEncoded = $this->getParam("SAMLResponse");
|
||||||
|
if (($samlResponse = @gzinflate(base64_decode($samlResponseEncoded))) === false) {
|
||||||
|
$samlResponse = base64_decode($samlResponseEncoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
$redirectUrl = $this->getParam("redirect");
|
$parsedResponse = SAMLResponse::parseResponse($this->context, $samlResponse);
|
||||||
if (!$this->validateRedirectURL($redirectUrl)) {
|
if (!$parsedResponse->wasSuccessful()) {
|
||||||
return $this->createError("Invalid redirect URL");
|
return $this->createError("Error parsing SAMLResponse: " . $parsedResponse->getError());
|
||||||
|
} else {
|
||||||
|
return $this->processLogin($parsedResponse->getUser(), $parsedResponse->getRedirectURL());
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
|
@ -12,7 +12,7 @@ namespace Core\API {
|
|||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||||
parent::__construct($context, $externalCall, $params);
|
parent::__construct($context, $externalCall, $params);
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
$this->apiKeyAllowed = false;
|
$this->apiKeyAllowed = false;
|
||||||
$this->userVerificationRequired = false;
|
$this->userVerificationRequired = false;
|
||||||
}
|
}
|
||||||
@ -148,7 +148,7 @@ namespace Core\API\TFA {
|
|||||||
$twoFactorToken = $currentUser->getTwoFactorToken();
|
$twoFactorToken = $currentUser->getTwoFactorToken();
|
||||||
if ($twoFactorToken && $twoFactorToken->isConfirmed()) {
|
if ($twoFactorToken && $twoFactorToken->isConfirmed()) {
|
||||||
return $this->createError("You already added a two factor token");
|
return $this->createError("You already added a two factor token");
|
||||||
} else if (!$currentUser->isNativeAccount()) {
|
} else if (!$currentUser->isLocalAccount()) {
|
||||||
return $this->createError("Cannot add a 2FA token: Your account is managed by an external identity provider (SSO)");
|
return $this->createError("Cannot add a 2FA token: Your account is managed by an external identity provider (SSO)");
|
||||||
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
|
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
@ -179,7 +179,6 @@ namespace Core\API\TFA {
|
|||||||
class ConfirmTotp extends VerifyTotp {
|
class ConfirmTotp extends VerifyTotp {
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall);
|
parent::__construct($context, $externalCall);
|
||||||
$this->loginRequired = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -216,7 +215,6 @@ namespace Core\API\TFA {
|
|||||||
parent::__construct($context, $externalCall, [
|
parent::__construct($context, $externalCall, [
|
||||||
"code" => new StringType("code", 6)
|
"code" => new StringType("code", 6)
|
||||||
]);
|
]);
|
||||||
$this->loginRequired = true;
|
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
$this->rateLimiting = new RateLimiting(
|
$this->rateLimiting = new RateLimiting(
|
||||||
null,
|
null,
|
||||||
@ -255,13 +253,12 @@ namespace Core\API\TFA {
|
|||||||
"clientDataJSON" => new StringType("clientDataJSON", 0, true, "{}"),
|
"clientDataJSON" => new StringType("clientDataJSON", 0, true, "{}"),
|
||||||
"attestationObject" => new StringType("attestationObject", 0, true, "")
|
"attestationObject" => new StringType("attestationObject", 0, true, "")
|
||||||
]);
|
]);
|
||||||
$this->loginRequired = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
|
|
||||||
$currentUser = $this->context->getUser();
|
$currentUser = $this->context->getUser();
|
||||||
if (!$currentUser->isNativeAccount()) {
|
if (!$currentUser->isLocalAccount()) {
|
||||||
return $this->createError("Cannot add a 2FA token: Your account is managed by an external identity provider (SSO)");
|
return $this->createError("Cannot add a 2FA token: Your account is managed by an external identity provider (SSO)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,7 +354,6 @@ namespace Core\API\TFA {
|
|||||||
"authData" => new StringType("authData"),
|
"authData" => new StringType("authData"),
|
||||||
"signature" => new StringType("signature"),
|
"signature" => new StringType("signature"),
|
||||||
]);
|
]);
|
||||||
$this->loginRequired = true;
|
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
$this->rateLimiting = new RateLimiting(
|
$this->rateLimiting = new RateLimiting(
|
||||||
null,
|
null,
|
||||||
|
@ -148,6 +148,7 @@ namespace Core\API\User {
|
|||||||
use Core\API\Parameter\IntegerType;
|
use Core\API\Parameter\IntegerType;
|
||||||
use Core\API\Parameter\Parameter;
|
use Core\API\Parameter\Parameter;
|
||||||
use Core\API\Parameter\StringType;
|
use Core\API\Parameter\StringType;
|
||||||
|
use Core\API\Request;
|
||||||
use Core\API\Template\Render;
|
use Core\API\Template\Render;
|
||||||
use Core\API\Traits\Captcha;
|
use Core\API\Traits\Captcha;
|
||||||
use Core\API\Traits\Pagination;
|
use Core\API\Traits\Pagination;
|
||||||
@ -184,7 +185,7 @@ namespace Core\API\User {
|
|||||||
'groups' => new ArrayType("groups", Parameter::TYPE_INT, true, true, [])
|
'groups' => new ArrayType("groups", Parameter::TYPE_INT, true, true, [])
|
||||||
));
|
));
|
||||||
|
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -304,7 +305,7 @@ namespace Core\API\User {
|
|||||||
parent::__construct($context, $externalCall, array(
|
parent::__construct($context, $externalCall, array(
|
||||||
'id' => new Parameter('id', Parameter::TYPE_INT)
|
'id' => new Parameter('id', Parameter::TYPE_INT)
|
||||||
));
|
));
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -447,7 +448,7 @@ namespace Core\API\User {
|
|||||||
'groups' => new ArrayType("groups", Parameter::TYPE_INT, true, true, [])
|
'groups' => new ArrayType("groups", Parameter::TYPE_INT, true, true, [])
|
||||||
));
|
));
|
||||||
|
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -545,14 +546,10 @@ namespace Core\API\User {
|
|||||||
'confirmPassword' => new StringType('confirmPassword'),
|
'confirmPassword' => new StringType('confirmPassword'),
|
||||||
));
|
));
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
|
$this->loginRequirements = Request::NOT_LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
|
|
||||||
if ($this->context->getUser()) {
|
|
||||||
return $this->createError("You are already logged in.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$token = $this->getParam("token");
|
$token = $this->getParam("token");
|
||||||
$password = $this->getParam("password");
|
$password = $this->getParam("password");
|
||||||
@ -593,17 +590,13 @@ namespace Core\API\User {
|
|||||||
'token' => new StringType('token', 36)
|
'token' => new StringType('token', 36)
|
||||||
));
|
));
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
|
$this->loginRequirements = Request::NOT_LOGGED_IN;
|
||||||
$this->rateLimiting = new RateLimiting(
|
$this->rateLimiting = new RateLimiting(
|
||||||
new RateLimitRule(5, 1, RateLimitRule::MINUTE)
|
new RateLimitRule(5, 1, RateLimitRule::MINUTE)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
|
|
||||||
if ($this->context->getUser()) {
|
|
||||||
return $this->createError("You are already logged in.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$token = $this->getParam("token");
|
$token = $this->getParam("token");
|
||||||
$userToken = $this->checkToken($token);
|
$userToken = $this->checkToken($token);
|
||||||
@ -708,7 +701,6 @@ namespace Core\API\User {
|
|||||||
|
|
||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall);
|
parent::__construct($context, $externalCall);
|
||||||
$this->loginRequired = false;
|
|
||||||
$this->apiKeyAllowed = false;
|
$this->apiKeyAllowed = false;
|
||||||
$this->forbidMethod("GET");
|
$this->forbidMethod("GET");
|
||||||
}
|
}
|
||||||
@ -750,14 +742,10 @@ namespace Core\API\User {
|
|||||||
$this->addCaptchaParameters($context, $parameters);
|
$this->addCaptchaParameters($context, $parameters);
|
||||||
parent::__construct($context, $externalCall, $parameters);
|
parent::__construct($context, $externalCall, $parameters);
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
|
$this->loginRequirements = Request::NOT_LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
|
|
||||||
if ($this->context->getUser()) {
|
|
||||||
return $this->createError(L('You are already logged in'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$settings = $this->context->getSettings();
|
$settings = $this->context->getSettings();
|
||||||
$registrationAllowed = $settings->isRegistrationAllowed();
|
$registrationAllowed = $settings->isRegistrationAllowed();
|
||||||
if (!$registrationAllowed) {
|
if (!$registrationAllowed) {
|
||||||
@ -848,7 +836,7 @@ namespace Core\API\User {
|
|||||||
class Edit extends UserAPI {
|
class Edit extends UserAPI {
|
||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, array(
|
parent::__construct($context, $externalCall, [
|
||||||
'id' => new Parameter('id', Parameter::TYPE_INT),
|
'id' => new Parameter('id', Parameter::TYPE_INT),
|
||||||
'username' => new StringType('username', 32, true, NULL),
|
'username' => new StringType('username', 32, true, NULL),
|
||||||
'fullName' => new StringType('fullName', 64, true, NULL),
|
'fullName' => new StringType('fullName', 64, true, NULL),
|
||||||
@ -857,10 +845,10 @@ namespace Core\API\User {
|
|||||||
'groups' => new ArrayType('groups', Parameter::TYPE_INT, true, true, NULL),
|
'groups' => new ArrayType('groups', Parameter::TYPE_INT, true, true, NULL),
|
||||||
'confirmed' => new Parameter('confirmed', Parameter::TYPE_BOOLEAN, true, NULL),
|
'confirmed' => new Parameter('confirmed', Parameter::TYPE_BOOLEAN, true, NULL),
|
||||||
'active' => new Parameter('active', Parameter::TYPE_BOOLEAN, true, NULL)
|
'active' => new Parameter('active', Parameter::TYPE_BOOLEAN, true, NULL)
|
||||||
));
|
]);
|
||||||
|
|
||||||
$this->loginRequired = true;
|
|
||||||
$this->forbidMethod("GET");
|
$this->forbidMethod("GET");
|
||||||
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -980,7 +968,7 @@ namespace Core\API\User {
|
|||||||
'id' => new Parameter('id', Parameter::TYPE_INT)
|
'id' => new Parameter('id', Parameter::TYPE_INT)
|
||||||
));
|
));
|
||||||
|
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -1060,7 +1048,7 @@ namespace Core\API\User {
|
|||||||
} else if ($user !== null) {
|
} else if ($user !== null) {
|
||||||
if (!$user->isActive()) {
|
if (!$user->isActive()) {
|
||||||
return $this->createError("This user is currently disabled. Contact the server administrator, if you believe this is a mistake.");
|
return $this->createError("This user is currently disabled. Contact the server administrator, if you believe this is a mistake.");
|
||||||
} else if (!$user->isNativeAccount()) {
|
} else if (!$user->isLocalAccount()) {
|
||||||
// TODO: this allows user enumeration for SSO accounts
|
// TODO: this allows user enumeration for SSO accounts
|
||||||
return $this->createError("Cannot request a password reset: Account is managed by an external identity provider (SSO)");
|
return $this->createError("Cannot request a password reset: Account is managed by an external identity provider (SSO)");
|
||||||
} else {
|
} else {
|
||||||
@ -1206,17 +1194,13 @@ namespace Core\API\User {
|
|||||||
$this->forbidMethod("GET");
|
$this->forbidMethod("GET");
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
$this->apiKeyAllowed = false;
|
$this->apiKeyAllowed = false;
|
||||||
|
$this->loginRequirements = Request::NOT_LOGGED_IN;
|
||||||
$this->rateLimiting = new RateLimiting(
|
$this->rateLimiting = new RateLimiting(
|
||||||
new RateLimitRule(5, 1, RateLimitRule::MINUTE)
|
new RateLimitRule(5, 1, RateLimitRule::MINUTE)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
|
|
||||||
if ($this->context->getUser()) {
|
|
||||||
return $this->createError("You are already logged in.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$token = $this->getParam("token");
|
$token = $this->getParam("token");
|
||||||
$password = $this->getParam("password");
|
$password = $this->getParam("password");
|
||||||
@ -1228,8 +1212,8 @@ namespace Core\API\User {
|
|||||||
return $this->createError("Invalid token type");
|
return $this->createError("Invalid token type");
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $token->getUser();
|
$user = $userToken->getUser();
|
||||||
if (!$user->isNativeAccount()) {
|
if (!$user->isLocalAccount()) {
|
||||||
return $this->createError("Cannot reset password: Your account is managed by an external identity provider (SSO)");
|
return $this->createError("Cannot reset password: Your account is managed by an external identity provider (SSO)");
|
||||||
} else if (!$this->checkPasswordRequirements($password, $confirmPassword)) {
|
} else if (!$this->checkPasswordRequirements($password, $confirmPassword)) {
|
||||||
return false;
|
return false;
|
||||||
@ -1261,7 +1245,7 @@ namespace Core\API\User {
|
|||||||
'confirmPassword' => new StringType('confirmPassword', -1, true, NULL),
|
'confirmPassword' => new StringType('confirmPassword', -1, true, NULL),
|
||||||
'oldPassword' => new StringType('oldPassword', -1, true, NULL),
|
'oldPassword' => new StringType('oldPassword', -1, true, NULL),
|
||||||
));
|
));
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
$this->csrfTokenRequired = true;
|
$this->csrfTokenRequired = true;
|
||||||
$this->apiKeyAllowed = false; // prevent account takeover when an API-key is stolen
|
$this->apiKeyAllowed = false; // prevent account takeover when an API-key is stolen
|
||||||
$this->forbidMethod("GET");
|
$this->forbidMethod("GET");
|
||||||
@ -1298,7 +1282,7 @@ namespace Core\API\User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($newPassword !== null || $newPasswordConfirm !== null) {
|
if ($newPassword !== null || $newPasswordConfirm !== null) {
|
||||||
if (!$currentUser->isNativeAccount()) {
|
if (!$currentUser->isLocalAccount()) {
|
||||||
return $this->createError("Cannot change password: Your account is managed by an external identity provider (SSO)");
|
return $this->createError("Cannot change password: Your account is managed by an external identity provider (SSO)");
|
||||||
} else if (!$this->checkPasswordRequirements($newPassword, $newPasswordConfirm)) {
|
} else if (!$this->checkPasswordRequirements($newPassword, $newPasswordConfirm)) {
|
||||||
return false;
|
return false;
|
||||||
@ -1338,7 +1322,7 @@ namespace Core\API\User {
|
|||||||
"y" => new FloatType("y", 0, PHP_FLOAT_MAX, true, NULL),
|
"y" => new FloatType("y", 0, PHP_FLOAT_MAX, true, NULL),
|
||||||
"size" => new FloatType("size", self::MIN_SIZE, self::MAX_SIZE, true, NULL),
|
"size" => new FloatType("size", self::MIN_SIZE, self::MAX_SIZE, true, NULL),
|
||||||
]);
|
]);
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
$this->forbidMethod("GET");
|
$this->forbidMethod("GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1415,7 +1399,7 @@ namespace Core\API\User {
|
|||||||
class RemovePicture extends UserAPI {
|
class RemovePicture extends UserAPI {
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, []);
|
parent::__construct($context, $externalCall, []);
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -1491,7 +1475,7 @@ namespace Core\API\User {
|
|||||||
parent::__construct($context, $externalCall, [
|
parent::__construct($context, $externalCall, [
|
||||||
"active" => new Parameter("active", Parameter::TYPE_BOOLEAN, true, true)
|
"active" => new Parameter("active", Parameter::TYPE_BOOLEAN, true, true)
|
||||||
]);
|
]);
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
@ -1535,7 +1519,7 @@ namespace Core\API\User {
|
|||||||
parent::__construct($context, $externalCall, [
|
parent::__construct($context, $externalCall, [
|
||||||
"id" => new Parameter("id", Parameter::TYPE_INT)
|
"id" => new Parameter("id", Parameter::TYPE_INT)
|
||||||
]);
|
]);
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
|
@ -5,18 +5,26 @@ use Core\Driver\SQL\Column\StringColumn;
|
|||||||
use Core\Driver\SQL\Constraint\ForeignKey;
|
use Core\Driver\SQL\Constraint\ForeignKey;
|
||||||
use Core\Driver\SQL\Strategy\CascadeStrategy;
|
use Core\Driver\SQL\Strategy\CascadeStrategy;
|
||||||
use Core\Objects\DatabaseEntity\SsoProvider;
|
use Core\Objects\DatabaseEntity\SsoProvider;
|
||||||
|
use Core\Objects\DatabaseEntity\SsoRequest;
|
||||||
use Core\Objects\DatabaseEntity\User;
|
use Core\Objects\DatabaseEntity\User;
|
||||||
|
|
||||||
$userHandler = User::getHandler($sql);
|
$userHandler = User::getHandler($sql);
|
||||||
$ssoProviderHandler = SsoProvider::getHandler($sql);
|
$ssoProviderHandler = SsoProvider::getHandler($sql);
|
||||||
|
$ssoRequestHandler = SsoRequest::getHandler($sql);
|
||||||
|
|
||||||
$userTable = $userHandler->getTableName();
|
$userTable = $userHandler->getTableName();
|
||||||
$ssoProviderTable = $ssoProviderHandler->getTableName();
|
$ssoProviderTable = $ssoProviderHandler->getTableName();
|
||||||
$ssoProviderColumn = $userHandler->getColumnName("ssoProvider", false);
|
$ssoProviderColumn = $userHandler->getColumnName("ssoProvider", false);
|
||||||
$passwordColumn = $userHandler->getColumnName("password");
|
$passwordColumn = $userHandler->getColumnName("password");
|
||||||
|
|
||||||
$queries = array_merge($queries, $ssoProviderHandler->getCreateQueries($sql));
|
// create new tables
|
||||||
|
$queries = array_merge(
|
||||||
|
$queries,
|
||||||
|
$ssoProviderHandler->getCreateQueries($sql),
|
||||||
|
$ssoRequestHandler->getCreateQueries($sql)
|
||||||
|
);
|
||||||
|
|
||||||
|
// add sso column to user table
|
||||||
$queries[] = $sql->alterTable($userTable)
|
$queries[] = $sql->alterTable($userTable)
|
||||||
->add(new IntColumn($ssoProviderColumn, true,null));
|
->add(new IntColumn($ssoProviderColumn, true,null));
|
||||||
|
|
||||||
@ -24,6 +32,7 @@ $queries[] = $sql->alterTable($userTable)
|
|||||||
$queries[] = $sql->alterTable($userTable)
|
$queries[] = $sql->alterTable($userTable)
|
||||||
->modify(new StringColumn($passwordColumn, 128,true));
|
->modify(new StringColumn($passwordColumn, 128,true));
|
||||||
|
|
||||||
|
// create foreign key constraint for sso column
|
||||||
$constraint = new ForeignKey($ssoProviderColumn, $ssoProviderTable, "id", new CascadeStrategy());
|
$constraint = new ForeignKey($ssoProviderColumn, $ssoProviderTable, "id", new CascadeStrategy());
|
||||||
$constraint->setName("${userTable}_ibfk_$ssoProviderColumn");
|
$constraint->setName("${userTable}_ibfk_$ssoProviderColumn");
|
||||||
$queries[] = $sql->alterTable($userTable)
|
$queries[] = $sql->alterTable($userTable)
|
||||||
|
@ -72,5 +72,4 @@ abstract class SsoProvider extends DatabaseEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract function login(Context $context, ?string $redirectUrl);
|
public abstract function login(Context $context, ?string $redirectUrl);
|
||||||
public abstract function parseResponse(Context $context, string $response) : ?User;
|
|
||||||
}
|
}
|
67
Core/Objects/DatabaseEntity/SsoRequest.class.php
Normal file
67
Core/Objects/DatabaseEntity/SsoRequest.class.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Objects\DatabaseEntity;
|
||||||
|
|
||||||
|
use Core\Driver\SQL\SQL;
|
||||||
|
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
|
||||||
|
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||||
|
use Core\Objects\DatabaseEntity\Attribute\Unique;
|
||||||
|
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
||||||
|
|
||||||
|
class SsoRequest extends DatabaseEntity {
|
||||||
|
|
||||||
|
const SSO_REQUEST_DURABILITY = 15; // in minutes
|
||||||
|
|
||||||
|
#[MaxLength(128)]
|
||||||
|
#[Unique]
|
||||||
|
private string $identifier;
|
||||||
|
|
||||||
|
private SsoProvider $ssoProvider;
|
||||||
|
|
||||||
|
private \DateTime $validUntil;
|
||||||
|
|
||||||
|
#[DefaultValue(false)]
|
||||||
|
private bool $used;
|
||||||
|
|
||||||
|
private ?string $redirectUrl;
|
||||||
|
|
||||||
|
public static function create(SQL $sql, SsoProvider $ssoProvider, ?string $redirectUrl): ?SsoRequest {
|
||||||
|
$request = new SsoRequest();
|
||||||
|
$request->identifier = uuidv4();
|
||||||
|
$request->ssoProvider = $ssoProvider;
|
||||||
|
$request->used = false;
|
||||||
|
$request->validUntil = (new \DateTime())->modify(sprintf('+%d minutes', self::SSO_REQUEST_DURABILITY));
|
||||||
|
$request->redirectUrl = $redirectUrl;
|
||||||
|
if ($request->save($sql)) {
|
||||||
|
return $request;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIdentifier() : string {
|
||||||
|
return $this->identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRedirectUrl() : ?string {
|
||||||
|
return $this->redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wasUsed() : bool {
|
||||||
|
return $this->used;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isValid() : bool {
|
||||||
|
return !isInPast($this->validUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProvider() : SsoProvider {
|
||||||
|
return $this->ssoProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invalidate(SQL $sql) : bool {
|
||||||
|
$this->used = true;
|
||||||
|
return $this->save($sql, ["used"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -173,7 +173,7 @@ class User extends DatabaseEntity {
|
|||||||
$alias);
|
$alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isNativeAccount(): bool {
|
public function isLocalAccount(): bool {
|
||||||
return $this->ssoProvider === null;
|
return $this->ssoProvider === null;
|
||||||
}
|
}
|
||||||
}
|
}
|
149
Core/Objects/SSO/SAMLResponse.class.php
Normal file
149
Core/Objects/SSO/SAMLResponse.class.php
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Objects\SSO;
|
||||||
|
|
||||||
|
use Core\Driver\SQL\Condition\Compare;
|
||||||
|
use Core\Objects\Context;
|
||||||
|
use Core\Objects\DatabaseEntity\SsoProvider;
|
||||||
|
use Core\Objects\DatabaseEntity\SsoRequest;
|
||||||
|
use Core\Objects\DatabaseEntity\User;
|
||||||
|
use DOMDocument;
|
||||||
|
|
||||||
|
class SAMLResponse {
|
||||||
|
|
||||||
|
private bool $success;
|
||||||
|
private string $error;
|
||||||
|
private ?User $user;
|
||||||
|
private ?SsoRequest $request;
|
||||||
|
|
||||||
|
private static function createSuccess(SsoRequest $request, User $user) : SAMLResponse {
|
||||||
|
$response = new SAMLResponse();
|
||||||
|
$response->user = $user;
|
||||||
|
$response->request = $request;
|
||||||
|
$response->success = true;
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function createError(?SsoRequest $request, string $error) : SAMLResponse {
|
||||||
|
$response = new SAMLResponse();
|
||||||
|
$response->error = $error;
|
||||||
|
$response->request = $request;
|
||||||
|
$response->success = false;
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function parseResponse(Context $context, string $response) : SAMLResponse {
|
||||||
|
$sql = $context->getSQL();
|
||||||
|
$xml = new DOMDocument();
|
||||||
|
$xml->loadXML($response);
|
||||||
|
|
||||||
|
if ($xml->documentElement->nodeName !== "samlp:Response") {
|
||||||
|
return self::createError(null, "Invalid root node");
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestId = $xml->documentElement->getAttribute("InResponseTo");
|
||||||
|
if (empty($requestId)) {
|
||||||
|
return self::createError(null, "Root node missing attribute 'InResponseTo'");
|
||||||
|
}
|
||||||
|
|
||||||
|
$ssoRequest = SsoRequest::findBy(SsoRequest::createBuilder($sql, true)
|
||||||
|
->whereEq("SsoRequest.identifier", $requestId)
|
||||||
|
->fetchEntities()
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($ssoRequest === false) {
|
||||||
|
return self::createError(null, "Error fetching SSO provider: " . $sql->getLastError());
|
||||||
|
} else if ($ssoRequest === null) {
|
||||||
|
return self::createError(null, "Request not found");
|
||||||
|
} else if ($ssoRequest->wasUsed()) {
|
||||||
|
return self::createError($ssoRequest, "SAMLResponse already processed");
|
||||||
|
} else if (!$ssoRequest->isValid()) {
|
||||||
|
return self::createError($ssoRequest, "Authentication request expired");
|
||||||
|
} else {
|
||||||
|
$ssoRequest->invalidate($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider = $ssoRequest->getProvider();
|
||||||
|
if (!($provider instanceof SSOProviderSAML)) {
|
||||||
|
return self::createError(null, "Authentication request was not a SAML request");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate XML and extract user info
|
||||||
|
if (!$xml->getElementsByTagName("Assertion")->length) {
|
||||||
|
return self::createError(null, "Assertion tag missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$assertion = $xml->getElementsByTagName('Assertion')->item(0);
|
||||||
|
if (!$assertion->getElementsByTagName("Signature")->length) {
|
||||||
|
return self::createError(null, "Signature tag missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature = $assertion->getElementsByTagName("Signature")->item(0);
|
||||||
|
// TODO: parse and validate signature
|
||||||
|
|
||||||
|
$statusCode = $xml->getElementsByTagName('StatusCode')->item(0);
|
||||||
|
if ($statusCode->getAttribute("Value") !== "urn:oasis:names:tc:SAML:2.0:status:Success") {
|
||||||
|
return self::createError(null, "StatusCode was not successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
$issuer = $xml->getElementsByTagName('Issuer')->item(0)->nodeValue;
|
||||||
|
// TODO: validate issuer
|
||||||
|
|
||||||
|
$username = $xml->getElementsByTagName('NameID')->item(0)->nodeValue;
|
||||||
|
$attributes = [];
|
||||||
|
foreach ($xml->getElementsByTagName('Attribute') as $attribute) {
|
||||||
|
$name = $attribute->getAttribute('Name');
|
||||||
|
$value = $attribute->getElementsByTagName('AttributeValue')->item(0)->nodeValue;
|
||||||
|
$attributes[$name] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $attributes["email"];
|
||||||
|
$fullName = [];
|
||||||
|
|
||||||
|
if (isset($attributes["firstName"])) {
|
||||||
|
$fullName[] = $attributes["firstName"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($attributes["lastName"])) {
|
||||||
|
$fullName[] = $attributes["lastName"];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullName = implode(" ", $fullName);
|
||||||
|
$user = User::findBy(User::createBuilder($context->getSQL(), true)
|
||||||
|
->where(new Compare("email", $email), new Compare("name", $username)));
|
||||||
|
|
||||||
|
if ($user === false) {
|
||||||
|
return self::createError($ssoRequest, "Error fetching user: " . $sql->getLastError());
|
||||||
|
} else if ($user === null) {
|
||||||
|
$user = new User();
|
||||||
|
$user->fullName = $fullName;
|
||||||
|
$user->email = $email;
|
||||||
|
$user->name = $username;
|
||||||
|
$user->password = null;
|
||||||
|
$user->ssoProvider = $ssoRequest->getProvider();
|
||||||
|
$user->confirmed = true;
|
||||||
|
$user->active = true;
|
||||||
|
$user->groups = []; // TODO: create a possibility to set auto-groups for SSO registered users
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::createSuccess($ssoRequest, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wasSuccessful() : bool {
|
||||||
|
return $this->success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getError() : string {
|
||||||
|
return $this->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser() : User {
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRedirectURL() : ?string {
|
||||||
|
return $this->request->getRedirectUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
namespace Core\Objects\SSO;
|
namespace Core\Objects\SSO;
|
||||||
|
|
||||||
use Core\Driver\SQL\Condition\Compare;
|
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
use Core\Objects\DatabaseEntity\SsoProvider;
|
use Core\Objects\DatabaseEntity\SsoProvider;
|
||||||
use Core\Objects\DatabaseEntity\User;
|
use Core\Objects\DatabaseEntity\SsoRequest;
|
||||||
use DOMDocument;
|
|
||||||
|
|
||||||
class SSOProviderSAML extends SSOProvider {
|
class SSOProviderSAML extends SSOProvider {
|
||||||
|
|
||||||
@ -18,19 +16,19 @@ class SSOProviderSAML extends SSOProvider {
|
|||||||
|
|
||||||
public function login(Context $context, ?string $redirectUrl) {
|
public function login(Context $context, ?string $redirectUrl) {
|
||||||
|
|
||||||
|
$sql = $context->getSQL();
|
||||||
$settings = $context->getSettings();
|
$settings = $context->getSettings();
|
||||||
$baseUrl = $settings->getBaseUrl();
|
$baseUrl = $settings->getBaseUrl();
|
||||||
$params = ["provider" => $this->getIdentifier()];
|
$ssoRequest = SsoRequest::create($sql, $this, $redirectUrl);
|
||||||
|
if (!$ssoRequest) {
|
||||||
if (!empty($redirectUrl)) {
|
throw new \Exception("Could not save SSO request: " . $sql->getLastError());
|
||||||
$params["redirect"] = $redirectUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$acsUrl = $baseUrl . "/api/sso/saml?" . http_build_query($params);
|
$acsUrl = $baseUrl . "/api/sso/saml";
|
||||||
$samlp = html_tag_ex("samlp:AuthnRequest", [
|
$samlp = html_tag_ex("samlp:AuthnRequest", [
|
||||||
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
||||||
"xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
|
"xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
|
||||||
"ID" => "_" . uniqid(),
|
"ID" => $ssoRequest->getIdentifier(),
|
||||||
"Version" => "2.0",
|
"Version" => "2.0",
|
||||||
"IssueInstant" => gmdate('Y-m-d\TH:i:s\Z'),
|
"IssueInstant" => gmdate('Y-m-d\TH:i:s\Z'),
|
||||||
"Destination" => $this->ssoUrl,
|
"Destination" => $this->ssoUrl,
|
||||||
@ -48,70 +46,4 @@ class SSOProviderSAML extends SSOProvider {
|
|||||||
$context->router->redirect(302, $samlUrl);
|
$context->router->redirect(302, $samlUrl);
|
||||||
die();
|
die();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parseResponse(Context $context, string $response): ?User {
|
|
||||||
$xml = new DOMDocument();
|
|
||||||
$xml->loadXML($response);
|
|
||||||
|
|
||||||
// Validate XML and extract user info
|
|
||||||
if (!$xml->getElementsByTagName("Assertion")->length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$assertion = $xml->getElementsByTagName('Assertion')->item(0);
|
|
||||||
if (!$assertion->getElementsByTagName("Signature")->length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$signature = $assertion->getElementsByTagName("Signature")->item(0);
|
|
||||||
// TODO: parse and validate signature
|
|
||||||
|
|
||||||
$statusCode = $xml->getElementsByTagName('StatusCode')->item(0);
|
|
||||||
if ($statusCode->getAttribute("Value") !== "urn:oasis:names:tc:SAML:2.0:status:Success") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$issuer = $xml->getElementsByTagName('Issuer')->item(0)->nodeValue;
|
|
||||||
// TODO: validate issuer
|
|
||||||
|
|
||||||
$username = $xml->getElementsByTagName('NameID')->item(0)->nodeValue;
|
|
||||||
$attributes = [];
|
|
||||||
foreach ($xml->getElementsByTagName('Attribute') as $attribute) {
|
|
||||||
$name = $attribute->getAttribute('Name');
|
|
||||||
$value = $attribute->getElementsByTagName('AttributeValue')->item(0)->nodeValue;
|
|
||||||
$attributes[$name] = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = $attributes["email"];
|
|
||||||
$fullName = [];
|
|
||||||
|
|
||||||
if (isset($attributes["firstName"])) {
|
|
||||||
$fullName[] = $attributes["firstName"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($attributes["lastName"])) {
|
|
||||||
$fullName[] = $attributes["lastName"];
|
|
||||||
}
|
|
||||||
|
|
||||||
$fullName = implode(" ", $fullName);
|
|
||||||
$user = User::findBy(User::createBuilder($context->getSQL(), true)
|
|
||||||
->where(new Compare("email", $email), new Compare("name", $username)));
|
|
||||||
|
|
||||||
if ($user === false) {
|
|
||||||
return null;
|
|
||||||
} else if ($user === null) {
|
|
||||||
$user = new User();
|
|
||||||
$user->fullName = $fullName;
|
|
||||||
$user->email = $email;
|
|
||||||
$user->name = $username;
|
|
||||||
$user->password = null;
|
|
||||||
$user->ssoProvider = $this;
|
|
||||||
$user->confirmed = true;
|
|
||||||
$user->active = true;
|
|
||||||
$user->groups = []; // TODO: create a possibility to set auto-groups for SSO registered users
|
|
||||||
}
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -112,7 +112,7 @@ php cli.php api add
|
|||||||
|
|
||||||
By default, and if not further specified or restricted, all endpoints have the following access rules:
|
By default, and if not further specified or restricted, all endpoints have the following access rules:
|
||||||
1. Allowed methods: GET and POST (`$this->allowedMethods`)
|
1. Allowed methods: GET and POST (`$this->allowedMethods`)
|
||||||
2. No login is required (`$this->loginRequired`)
|
2. No login is required (`$this->loginRequirements`)
|
||||||
3. CSRF-Token is required, if the user is logged in (`$this->csrfTokenRequired`)
|
3. CSRF-Token is required, if the user is logged in (`$this->csrfTokenRequired`)
|
||||||
4. The function can be called from outside (`$this->isPublic`)
|
4. The function can be called from outside (`$this->isPublic`)
|
||||||
5. An API-Key can be used to access this method (`$this->apiKeyAllowed`)
|
5. An API-Key can be used to access this method (`$this->apiKeyAllowed`)
|
||||||
|
@ -199,6 +199,6 @@ class RequestDisabled extends TestRequest {
|
|||||||
class RequestLoginRequired extends TestRequest {
|
class RequestLoginRequired extends TestRequest {
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, []);
|
parent::__construct($context, $externalCall, []);
|
||||||
$this->loginRequired = true;
|
$this->loginRequirements = Request::LOGGED_IN;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user