SAML role-group mapping, Signature validation, Bugfixes
This commit is contained in:
parent
20e464776c
commit
ee9ab8b7f6
@ -22,7 +22,10 @@ namespace Core\API {
|
|||||||
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->getSsoProvider()?->getIdentifier() !== $provider->getIdentifier()) {
|
} else if ($user->getSsoProvider()?->getIdentifier() !== $provider->getIdentifier()) {
|
||||||
return $this->createError("An existing user is not managed by the used identity provider");
|
return $this->createError("An existing user is not managed by the used identity provider");
|
||||||
} else if (!$this->createSession($user)) {
|
}
|
||||||
|
|
||||||
|
// Create the session and log them in
|
||||||
|
if (!$this->createSession($user)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +36,7 @@ namespace Core\API {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function validateRedirectURL(string $url): bool {
|
protected function validateRedirectURL(?string $url): bool {
|
||||||
// allow only relative paths
|
// allow only relative paths
|
||||||
return empty($url) || startsWith($url, "/");
|
return empty($url) || startsWith($url, "/");
|
||||||
}
|
}
|
||||||
@ -64,13 +67,27 @@ namespace Core\API\Sso {
|
|||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$query = SsoProvider::createBuilder($sql, false);
|
$query = SsoProvider::createBuilder($sql, false);
|
||||||
|
$user = $this->context->getUser();
|
||||||
|
$canEdit = false;
|
||||||
|
|
||||||
if (!$this->context->getUser()) {
|
if (!$user) {
|
||||||
|
// only show active providers, when not logged in
|
||||||
$query->whereTrue("active");
|
$query->whereTrue("active");
|
||||||
|
} else {
|
||||||
|
$req = new \Core\API\Permission\Check($this->context);
|
||||||
|
$canEdit = $req->execute(["method" => "sso/editProvider"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// show all properties, if a user is allowed to edit them
|
||||||
$providers = SsoProvider::findBy($query);
|
$providers = SsoProvider::findBy($query);
|
||||||
$this->result["providers"] = SsoProvider::toJsonArray($providers);
|
$properties = $canEdit ? null : [
|
||||||
|
"id",
|
||||||
|
"identifier",
|
||||||
|
"name",
|
||||||
|
"protocol"
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->result["providers"] = SsoProvider::toJsonArray($providers, $properties);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,12 +231,7 @@ namespace Core\API\Sso {
|
|||||||
|
|
||||||
protected function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
|
|
||||||
|
$samlResponse = base64_decode($this->getParam("SAMLResponse"));
|
||||||
$samlResponseEncoded = $this->getParam("SAMLResponse");
|
|
||||||
if (($samlResponse = @gzinflate(base64_decode($samlResponseEncoded))) === false) {
|
|
||||||
$samlResponse = base64_decode($samlResponseEncoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
$parsedResponse = SAMLResponse::parseResponse($this->context, $samlResponse);
|
$parsedResponse = SAMLResponse::parseResponse($this->context, $samlResponse);
|
||||||
if (!$parsedResponse->wasSuccessful()) {
|
if (!$parsedResponse->wasSuccessful()) {
|
||||||
return $this->createError("Error parsing SAMLResponse: " . $parsedResponse->getError());
|
return $this->createError("Error parsing SAMLResponse: " . $parsedResponse->getError());
|
||||||
|
@ -663,7 +663,7 @@ namespace Core\API\User {
|
|||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$user = User::findBy(User::createBuilder($sql, true)
|
$user = User::findBy(User::createBuilder($sql, true)
|
||||||
->where(new Compare("User.name", $username), new Compare("User.email", $username))
|
->where(new Compare("User.name", $username), new Compare("User.email", $username))
|
||||||
->whereEq("User.sso_provider", NULL)
|
->whereEq("User.sso_provider_id", NULL)
|
||||||
->fetchEntities());
|
->fetchEntities());
|
||||||
|
|
||||||
if ($user !== false) {
|
if ($user !== false) {
|
||||||
|
@ -168,7 +168,7 @@ abstract class DatabaseEntity implements ArrayAccess, JsonSerializable {
|
|||||||
return $dbQuery->execute();
|
return $dbQuery->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function findAll(SQL $sql, ?Condition $condition = null): ?array {
|
public static function findAll(SQL $sql, ?Condition $condition = null): array|bool|null {
|
||||||
|
|
||||||
$query = self::createBuilder($sql, false);
|
$query = self::createBuilder($sql, false);
|
||||||
if ($condition) {
|
if ($condition) {
|
||||||
|
@ -2,8 +2,13 @@
|
|||||||
|
|
||||||
namespace Core\Objects\DatabaseEntity;
|
namespace Core\Objects\DatabaseEntity;
|
||||||
|
|
||||||
|
use Core\Driver\Logger\Logger;
|
||||||
|
use Core\Driver\SQL\Column\Column;
|
||||||
|
use Core\Driver\SQL\Condition\CondIn;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
|
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
|
||||||
use Core\Objects\DatabaseEntity\Attribute\ExtendingEnum;
|
use Core\Objects\DatabaseEntity\Attribute\ExtendingEnum;
|
||||||
|
use Core\Objects\DatabaseEntity\Attribute\Json;
|
||||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||||
use Core\Objects\DatabaseEntity\Attribute\Unique;
|
use Core\Objects\DatabaseEntity\Attribute\Unique;
|
||||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
||||||
@ -31,8 +36,18 @@ abstract class SsoProvider extends DatabaseEntity {
|
|||||||
#[ExtendingEnum(self::PROTOCOLS)]
|
#[ExtendingEnum(self::PROTOCOLS)]
|
||||||
private string $protocol;
|
private string $protocol;
|
||||||
|
|
||||||
|
#[MaxLength(256)]
|
||||||
protected string $ssoUrl;
|
protected string $ssoUrl;
|
||||||
|
|
||||||
|
#[MaxLength(128)]
|
||||||
|
protected string $clientId;
|
||||||
|
|
||||||
|
#[Json]
|
||||||
|
#[DefaultValue('{}')]
|
||||||
|
protected array $groupMapping;
|
||||||
|
|
||||||
|
protected string $certificate;
|
||||||
|
|
||||||
public function __construct(string $protocol, ?int $id = null) {
|
public function __construct(string $protocol, ?int $id = null) {
|
||||||
parent::__construct($id);
|
parent::__construct($id);
|
||||||
$this->protocol = $protocol;
|
$this->protocol = $protocol;
|
||||||
@ -71,5 +86,42 @@ abstract class SsoProvider extends DatabaseEntity {
|
|||||||
return $this->identifier;
|
return $this->identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getGroupMapping(): array {
|
||||||
|
return $this->groupMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createUser(Context $context, string $email, array $groupNames) : User {
|
||||||
|
$sql = $context->getSQL();
|
||||||
|
$loggerName = "SSO-" . strtoupper($this->protocol);
|
||||||
|
$logger = new Logger($loggerName, $sql);
|
||||||
|
|
||||||
|
if (empty($groupNames)) {
|
||||||
|
$groups = [];
|
||||||
|
} else {
|
||||||
|
$groups = Group::findAll($sql, new CondIn(new Column("name"), $groupNames));
|
||||||
|
if ($groups === false) {
|
||||||
|
throw new \Exception("Error fetching available groups: " . $sql->getLastError());
|
||||||
|
} else if (count($groups) !== count($groupNames)) {
|
||||||
|
$availableGroups = array_map(function (Group $group) {
|
||||||
|
return $group->getName();
|
||||||
|
}, $groups);
|
||||||
|
$nonExistentGroups = array_diff($groupNames, $availableGroups);
|
||||||
|
$logger->warning("Could not resolve group names: " . implode(', ', $nonExistentGroups));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: create a possibility to map attribute values to user properties
|
||||||
|
$user = new User();
|
||||||
|
$user->email = $email;
|
||||||
|
$user->name = $email;
|
||||||
|
$user->password = null;
|
||||||
|
$user->fullName = "";
|
||||||
|
$user->ssoProvider = $this;
|
||||||
|
$user->confirmed = true;
|
||||||
|
$user->active = true;
|
||||||
|
$user->groups = $groups;
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract function login(Context $context, ?string $redirectUrl);
|
public abstract function login(Context $context, ?string $redirectUrl);
|
||||||
}
|
}
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Core\Objects\SSO;
|
namespace Core\Objects\SSO;
|
||||||
|
|
||||||
use Core\Driver\Logger\Logger;
|
|
||||||
use Core\Driver\SQL\Condition\Compare;
|
use Core\Driver\SQL\Condition\Compare;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
use Core\Objects\DatabaseEntity\SsoProvider;
|
use Core\Objects\DatabaseEntity\SsoProvider;
|
||||||
@ -36,14 +35,59 @@ class SAMLResponse {
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function findSignatureNode(\DOMNode $node) : ?\DOMNode {
|
||||||
|
foreach ($node->childNodes as $child) {
|
||||||
|
if ($child->nodeName === 'dsig:Signature') {
|
||||||
|
return $child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function parseSignatureAlgorithm($name) : ?int {
|
||||||
|
return match ($name) {
|
||||||
|
'http://www.w3.org/2000/09/xmldsig#sha1' => OPENSSL_ALGO_SHA1,
|
||||||
|
'http://www.w3.org/2001/04/xmlenc#sha256' => OPENSSL_ALGO_SHA256,
|
||||||
|
'http://www.w3.org/2001/04/xmldsig-more#sha384' => OPENSSL_ALGO_SHA384,
|
||||||
|
'http://www.w3.org/2001/04/xmlenc#sha512' => OPENSSL_ALGO_SHA512,
|
||||||
|
'http://www.w3.org/2001/04/xmlenc#ripemd160' => OPENSSL_ALGO_RMD160,
|
||||||
|
'http://www.w3.org/2001/04/xmldsig-more#md5' => OPENSSL_ALGO_MD5,
|
||||||
|
default => throw new \Exception("Unsupported digest algorithm: $name"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function verifyNodeSignature(SsoProvider $provider, \DOMNode $signatureNode) {
|
||||||
|
$signedInfoNode = $signatureNode->getElementsByTagName('SignedInfo')->item(0);
|
||||||
|
if (!$signedInfoNode) {
|
||||||
|
throw new \Exception("SignedInfo not found in the Signature element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$signedInfo = $signedInfoNode->C14N(true, false);
|
||||||
|
$signatureValueNode = $signatureNode->getElementsByTagName('SignatureValue')->item(0);
|
||||||
|
if (!$signatureValueNode) {
|
||||||
|
throw new \Exception("SignatureValue not found in the Signature element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$digestMethodNode = $signatureNode->getElementsByTagName('DigestMethod')->item(0);
|
||||||
|
if (!$digestMethodNode) {
|
||||||
|
throw new \Exception("DigestMethod not found in the Signature element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$algorithm = self::parseSignatureAlgorithm($digestMethodNode->getAttribute("Algorithm"));
|
||||||
|
$signatureValue = base64_decode($signatureValueNode->nodeValue);
|
||||||
|
if (!$provider->validateSignature($signedInfo, $signatureValue, $algorithm)) {
|
||||||
|
throw new \Exception("Invalid Signature.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function parseResponse(Context $context, string $response) : SAMLResponse {
|
public static function parseResponse(Context $context, string $response) : SAMLResponse {
|
||||||
$sql = $context->getSQL();
|
$sql = $context->getSQL();
|
||||||
$logger = new Logger("SAML", $sql);
|
|
||||||
$xml = new DOMDocument();
|
$xml = new DOMDocument();
|
||||||
$xml->loadXML($response);
|
$xml->loadXML($response);
|
||||||
|
|
||||||
if ($xml->documentElement->nodeName !== "samlp:Response") {
|
if ($xml->documentElement->nodeName !== "samlp:Response") {
|
||||||
return self::createError(null, "Invalid root node");
|
return self::createError(null, "Invalid root node, expected: 'samlp:Response'");
|
||||||
}
|
}
|
||||||
|
|
||||||
$requestId = $xml->documentElement->getAttribute("InResponseTo");
|
$requestId = $xml->documentElement->getAttribute("InResponseTo");
|
||||||
@ -65,76 +109,74 @@ class SAMLResponse {
|
|||||||
} else if (!$ssoRequest->isValid()) {
|
} else if (!$ssoRequest->isValid()) {
|
||||||
return self::createError($ssoRequest, "Authentication request expired");
|
return self::createError($ssoRequest, "Authentication request expired");
|
||||||
} else {
|
} else {
|
||||||
$ssoRequest->invalidate($sql);
|
// $ssoRequest->invalidate($sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
$provider = $ssoRequest->getProvider();
|
try {
|
||||||
if (!($provider instanceof SSOProviderSAML)) {
|
$provider = $ssoRequest->getProvider();
|
||||||
return self::createError(null, "Authentication request was not a SAML request");
|
if (!($provider instanceof SSOProviderSAML)) {
|
||||||
|
return self::createError($ssoRequest, "Authentication request was not a SAML request");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate XML and extract user info
|
||||||
|
if (!$xml->getElementsByTagName("Assertion")->length) {
|
||||||
|
return self::createError($ssoRequest, "Assertion tag missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
$assertion = $xml->getElementsByTagName('Assertion')->item(0);
|
||||||
|
|
||||||
|
//// <-- Signature Validation
|
||||||
|
$rootSignature = self::findSignatureNode($xml->documentElement);
|
||||||
|
$assertionSignature = self::findSignatureNode($assertion);
|
||||||
|
if ($rootSignature === null && $assertionSignature === null) {
|
||||||
|
return self::createError($ssoRequest, "Neither a document nor an assertion signature was present.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rootSignature !== null) {
|
||||||
|
self::verifyNodeSignature($provider, $rootSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($assertionSignature !== null) {
|
||||||
|
self::verifyNodeSignature($provider, $assertionSignature);
|
||||||
|
}
|
||||||
|
//// Signature Validation -->
|
||||||
|
|
||||||
|
// Check status code
|
||||||
|
$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");
|
||||||
|
}
|
||||||
|
|
||||||
|
$groupMapping = $provider->getGroupMapping();
|
||||||
|
$email = $xml->getElementsByTagName('NameID')->item(0)->nodeValue;
|
||||||
|
$attributes = [];
|
||||||
|
$groupNames = [];
|
||||||
|
foreach ($xml->getElementsByTagName('Attribute') as $attribute) {
|
||||||
|
$name = $attribute->getAttribute('Name');
|
||||||
|
$value = $attribute->getElementsByTagName('AttributeValue')->item(0)->nodeValue;
|
||||||
|
if ($name === "Role") {
|
||||||
|
if (isset($groupMapping[$value])) {
|
||||||
|
$groupNames[] = $groupMapping[$value];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$attributes[$name] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::findBy(User::createBuilder($context->getSQL(), true)
|
||||||
|
->where(new Compare("User.email", $email), new Compare("User.name", $email))
|
||||||
|
->fetchEntities());
|
||||||
|
|
||||||
|
if ($user === false) {
|
||||||
|
return self::createError($ssoRequest, "Error fetching user: " . $sql->getLastError());
|
||||||
|
} else if ($user === null) {
|
||||||
|
$user = $ssoRequest->getProvider()->createUser($context, $email, $groupNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::createSuccess($ssoRequest, $user);
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
return self::createError($ssoRequest, $ex->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// TODO: create a possibility to map attribute values to user properties
|
|
||||||
$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))
|
|
||||||
->fetchEntities());
|
|
||||||
|
|
||||||
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 {
|
public function wasSuccessful() : bool {
|
||||||
|
@ -34,16 +34,42 @@ class SSOProviderSAML extends SSOProvider {
|
|||||||
"Destination" => $this->ssoUrl,
|
"Destination" => $this->ssoUrl,
|
||||||
"ProtocolBinding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
"ProtocolBinding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
||||||
"AssertionConsumerServiceURL" => $acsUrl
|
"AssertionConsumerServiceURL" => $acsUrl
|
||||||
], html_tag("saml:Issuer", [], $baseUrl), false);
|
], html_tag("saml:Issuer", [], $this->clientId), false);
|
||||||
|
|
||||||
$samlRequest = base64_encode(gzdeflate($samlp));
|
$samlRequest = base64_encode($samlp);
|
||||||
$samlUrl = $this->buildUrl($this->ssoUrl, [ "SAMLRequest" => $samlRequest ]);
|
$req = new \Core\API\Template\Render($context);
|
||||||
|
$success = $req->execute([
|
||||||
|
"file" => "sso.twig",
|
||||||
|
"parameters" => [
|
||||||
|
"sso" => [
|
||||||
|
"url" => $this->ssoUrl,
|
||||||
|
"data" => [
|
||||||
|
"SAMLRequest" => $samlRequest
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
if ($samlUrl === null) {
|
if (!$success) {
|
||||||
throw new \Exception("SSO Provider has an invalid URL configured");
|
throw new \Exception("Could not redirect: " . $req->getLastError());
|
||||||
}
|
}
|
||||||
|
|
||||||
$context->router->redirect(302, $samlUrl);
|
die($req->getResult()["html"]);
|
||||||
die();
|
}
|
||||||
|
|
||||||
|
public function validateSignature(string $what, string $signature, int $algorithm) : bool {
|
||||||
|
$publicKey = openssl_pkey_get_public($this->certificate);
|
||||||
|
if (!$publicKey) {
|
||||||
|
throw new \Exception("Failed to load certificate: " . openssl_error_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = openssl_verify($what, $signature, $publicKey, $algorithm);
|
||||||
|
if ($result === 1) {
|
||||||
|
return true;
|
||||||
|
} else if ($result === 0) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw new \Exception("Failed to validate signature: " . openssl_error_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
19
Core/Templates/sso.twig
Normal file
19
Core/Templates/sso.twig
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ user.lang }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<script>
|
||||||
|
window.onload = () => {
|
||||||
|
document.forms["sso"].submit();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form method="POST" action="{{ sso.url }}" id="sso">
|
||||||
|
{% for key, value in sso.data %}
|
||||||
|
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||||
|
{% endfor %}
|
||||||
|
You will be automatically redirected. If that doesn't work, click <button type="submit">here</button>.
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user