Dev SSO: Tables, SAML
This commit is contained in:
13
Core/API/Parameter/UuidType.class.php
Normal file
13
Core/API/Parameter/UuidType.class.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API\Parameter;
|
||||
|
||||
class UuidType extends RegexType {
|
||||
|
||||
const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
||||
|
||||
public function __construct(string $name, bool $optional = FALSE, ?string $defaultValue = NULL) {
|
||||
parent::__construct($name, self::UUID_PATTERN, $optional, $defaultValue);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\TwoFactorToken;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
use Core\Objects\RateLimiting;
|
||||
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
@@ -144,6 +145,22 @@ abstract class Request {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function createSession(User $user, bool $stayLoggedIn = true): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
if (!($session = $this->context->createSession($user, $stayLoggedIn))) {
|
||||
return $this->createError("Error creating Session: " . $sql->getLastError());
|
||||
} else {
|
||||
$tfaToken = $user->getTwoFactorToken();
|
||||
$this->result["loggedIn"] = true;
|
||||
$this->result["user"] = $user->jsonSerialize();
|
||||
$this->result["session"] = $session->jsonSerialize(["expires", "csrfToken"]);
|
||||
$this->result["logoutIn"] = $session->getExpiresSeconds();
|
||||
$this->check2FA($tfaToken);
|
||||
$this->success = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected function check2FA(?TwoFactorToken $tfaToken = null): bool {
|
||||
|
||||
// do not require 2FA for verifying endpoints
|
||||
|
||||
242
Core/API/SsoAPI.class.php
Normal file
242
Core/API/SsoAPI.class.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
|
||||
abstract class SsoAPI extends Request {
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = []) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
}
|
||||
|
||||
protected function processLogin(User $user, ?string $redirectUrl): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
if ($user->getId() === null) {
|
||||
// user didn't exit yet. try to insert into database
|
||||
if (!$user->save($sql)) {
|
||||
return $this->createError("Could not create user: " . $sql->getLastError());
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->createSession($user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($redirectUrl)) {
|
||||
$this->context->router->redirect(302, $redirectUrl);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function validateRedirectURL(string $url): bool {
|
||||
// allow only relative paths
|
||||
return empty($url) || startsWith($url, "/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\Sso {
|
||||
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\Parameter\UuidType;
|
||||
use Core\Objects\Context;
|
||||
use Core\API\SsoAPI;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use Core\Objects\DatabaseEntity\SsoProvider;
|
||||
|
||||
class GetProviders extends SsoAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, []);
|
||||
// TODO: auto-generated method stub
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
// TODO: auto-generated method stub
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDescription(): string {
|
||||
// TODO: auto generated endpoint description
|
||||
return "Short description, what users are able to do with this endpoint.";
|
||||
}
|
||||
}
|
||||
|
||||
class AddProvider extends SsoAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, []);
|
||||
// TODO: auto-generated method stub
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
// TODO: auto-generated method stub
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDescription(): string {
|
||||
// TODO: auto generated endpoint description
|
||||
return "Short description, what users are able to do with this endpoint.";
|
||||
}
|
||||
|
||||
public static function getDefaultPermittedGroups(): array {
|
||||
return [Group::ADMIN];
|
||||
}
|
||||
}
|
||||
|
||||
class EditProvider extends SsoAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, []);
|
||||
// TODO: auto-generated method stub
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
// TODO: auto-generated method stub
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDescription(): string {
|
||||
// TODO: auto generated endpoint description
|
||||
return "Short description, what users are able to do with this endpoint.";
|
||||
}
|
||||
|
||||
public static function getDefaultPermittedGroups(): array {
|
||||
return [Group::ADMIN];
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveProvider extends SsoAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, []);
|
||||
// TODO: auto-generated method stub
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
// TODO: auto-generated method stub
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDescription(): string {
|
||||
// TODO: auto generated endpoint description
|
||||
return "Short description, what users are able to do with this endpoint.";
|
||||
}
|
||||
|
||||
public static function getDefaultPermittedGroups(): array {
|
||||
return [Group::ADMIN];
|
||||
}
|
||||
}
|
||||
|
||||
class Authenticate extends SsoAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"provider" => new UuidType("provider"),
|
||||
"redirect" => new StringType("redirect", StringType::UNLIMITED, true, null)
|
||||
]);
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
|
||||
if ($this->context->getUser()) {
|
||||
return $this->createError("You are already logged in.");
|
||||
}
|
||||
|
||||
$redirectUrl = $this->getParam("redirect");
|
||||
if (!$this->validateRedirectURL($redirectUrl)) {
|
||||
return $this->createError("Invalid redirect URL");
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$ssoProviderIdentifier = $this->getParam("provider");
|
||||
$ssoProvider = SsoProvider::findBy(SsoProvider::createBuilder($sql, true)
|
||||
->whereEq("identifier", $ssoProviderIdentifier)
|
||||
->whereTrue("active")
|
||||
);
|
||||
if ($ssoProvider === false) {
|
||||
return $this->createError("Error fetching SSO Provider: " . $sql->getLastError());
|
||||
} else if ($ssoProvider === null) {
|
||||
return $this->createError("SSO Provider not found");
|
||||
}
|
||||
|
||||
try {
|
||||
$ssoProvider->login($this->context, $redirectUrl);
|
||||
} catch (\Exception $ex) {
|
||||
return $this->createError("There was an error with the SSO provider: " . $ex->getMessage());
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDescription(): string {
|
||||
return "Allows users to authenticate with a configured SSO provider.";
|
||||
}
|
||||
|
||||
public static function hasConfigurablePermissions(): bool {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SAML extends SsoAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"SAMLResponse" => new StringType("SAMLResponse"),
|
||||
"provider" => new UuidType("provider"),
|
||||
"redirect" => new StringType("redirect", StringType::UNLIMITED, true, null)
|
||||
]);
|
||||
|
||||
$this->csrfTokenRequired = false;
|
||||
$this->forbidMethod("GET");
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
|
||||
if ($this->context->getUser()) {
|
||||
return $this->createError("You are already logged in.");
|
||||
}
|
||||
|
||||
$redirectUrl = $this->getParam("redirect");
|
||||
if (!$this->validateRedirectURL($redirectUrl)) {
|
||||
return $this->createError("Invalid redirect URL");
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$ssoProviderIdentifier = $this->getParam("provider");
|
||||
$ssoProvider = SsoProvider::findBy(SsoProvider::createBuilder($sql, true)
|
||||
->whereEq("identifier", $ssoProviderIdentifier)
|
||||
->whereTrue("active")
|
||||
);
|
||||
if ($ssoProvider === false) {
|
||||
return $this->createError("Error fetching SSO Provider: " . $sql->getLastError());
|
||||
} else if ($ssoProvider === null) {
|
||||
return $this->createError("SSO Provider not found");
|
||||
}
|
||||
|
||||
$samlResponseEncoded = $this->getParam("SAMLResponse");
|
||||
if (($samlResponse = @gzinflate(base64_decode($samlResponseEncoded))) === false) {
|
||||
$samlResponse = base64_decode($samlResponseEncoded);
|
||||
}
|
||||
|
||||
$parsedUser = $ssoProvider->parseResponse($this->context, $samlResponse);
|
||||
if ($parsedUser === null) {
|
||||
return $this->createError("Invalid SAMLResponse");
|
||||
} else {
|
||||
return $this->processLogin($parsedUser, $redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getDescription(): string {
|
||||
return "Return endpoint for SAML SSO authentication.";
|
||||
}
|
||||
|
||||
public static function hasConfigurablePermissions(): bool {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,8 @@ namespace Core\API\TFA {
|
||||
$twoFactorToken = $currentUser->getTwoFactorToken();
|
||||
if ($twoFactorToken && $twoFactorToken->isConfirmed()) {
|
||||
return $this->createError("You already added a two factor token");
|
||||
} else if (!$currentUser->isNativeAccount()) {
|
||||
return $this->createError("Cannot add a 2FA token: Your account is managed by an external identity provider (SSO)");
|
||||
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
|
||||
$sql = $this->context->getSQL();
|
||||
$twoFactorToken = new TimeBasedTwoFactorToken(generateRandomString(32, "base32"));
|
||||
@@ -259,6 +261,10 @@ namespace Core\API\TFA {
|
||||
public function _execute(): bool {
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
if (!$currentUser->isNativeAccount()) {
|
||||
return $this->createError("Cannot add a 2FA token: Your account is managed by an external identity provider (SSO)");
|
||||
}
|
||||
|
||||
$clientDataJSON = json_decode($this->getParam("clientDataJSON"), true);
|
||||
$attestationObjectRaw = base64_decode($this->getParam("attestationObject"));
|
||||
$twoFactorToken = $currentUser->getTwoFactorToken();
|
||||
|
||||
@@ -670,6 +670,7 @@ namespace Core\API\User {
|
||||
$sql = $this->context->getSQL();
|
||||
$user = User::findBy(User::createBuilder($sql, true)
|
||||
->where(new Compare("User.name", $username), new Compare("User.email", $username))
|
||||
->whereEq("User.sso_provider", NULL)
|
||||
->fetchEntities());
|
||||
|
||||
if ($user !== false) {
|
||||
@@ -681,17 +682,8 @@ namespace Core\API\User {
|
||||
if (!$user->confirmed) {
|
||||
$this->result["emailConfirmed"] = false;
|
||||
return $this->createError("Your email address has not been confirmed yet.");
|
||||
} else if (!($session = $this->context->createSession($user, $stayLoggedIn))) {
|
||||
return $this->createError("Error creating Session: " . $sql->getLastError());
|
||||
} else {
|
||||
$tfaToken = $user->getTwoFactorToken();
|
||||
|
||||
$this->result["loggedIn"] = true;
|
||||
$this->result["user"] = $user->jsonSerialize();
|
||||
$this->result["session"] = $session->jsonSerialize(["expires", "csrfToken"]);
|
||||
$this->result["logoutIn"] = $session->getExpiresSeconds();
|
||||
$this->check2FA($tfaToken);
|
||||
$this->success = true;
|
||||
return $this->createSession($user, $stayLoggedIn);
|
||||
}
|
||||
} else {
|
||||
return $this->createError(L('Wrong username or password'));
|
||||
@@ -1068,6 +1060,9 @@ namespace Core\API\User {
|
||||
} else if ($user !== null) {
|
||||
if (!$user->isActive()) {
|
||||
return $this->createError("This user is currently disabled. Contact the server administrator, if you believe this is a mistake.");
|
||||
} else if (!$user->isNativeAccount()) {
|
||||
// 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)");
|
||||
} else {
|
||||
$validHours = 1;
|
||||
$token = generateRandomString(36);
|
||||
@@ -1234,7 +1229,9 @@ namespace Core\API\User {
|
||||
}
|
||||
|
||||
$user = $token->getUser();
|
||||
if (!$this->checkPasswordRequirements($password, $confirmPassword)) {
|
||||
if (!$user->isNativeAccount()) {
|
||||
return $this->createError("Cannot reset password: Your account is managed by an external identity provider (SSO)");
|
||||
} else if (!$this->checkPasswordRequirements($password, $confirmPassword)) {
|
||||
return false;
|
||||
} else {
|
||||
$user->password = $this->hashPassword($password);
|
||||
@@ -1301,7 +1298,9 @@ namespace Core\API\User {
|
||||
}
|
||||
|
||||
if ($newPassword !== null || $newPasswordConfirm !== null) {
|
||||
if (!$this->checkPasswordRequirements($newPassword, $newPasswordConfirm)) {
|
||||
if (!$currentUser->isNativeAccount()) {
|
||||
return $this->createError("Cannot change password: Your account is managed by an external identity provider (SSO)");
|
||||
} else if (!$this->checkPasswordRequirements($newPassword, $newPasswordConfirm)) {
|
||||
return false;
|
||||
} else {
|
||||
if (!password_verify($oldPassword, $currentUser->password)) {
|
||||
|
||||
Reference in New Issue
Block a user