Add API, auth logging, AD support

TODO: Test changing AD passwords
V2_Rewrite
Skylar Ittner il y a 7 ans
Parent 8b091c59f6
révision 5929d13147

@ -3,10 +3,14 @@
/**
* Make things happen when buttons are pressed and forms submitted.
*/
use LdapTools\LdapManager;
use LdapTools\Object\LdapObjectType;
require_once __DIR__ . "/required.php";
dieifnotloggedin();
require_once __DIR__ . "/lib/login.php";
require_once __DIR__ . "/lib/worst_passwords.php";
function returnToSender($msg, $arg = "") {
@ -21,12 +25,12 @@ function returnToSender($msg, $arg = "") {
switch ($VARS['action']) {
case "signout":
insertAuthLog(11, $_SESSION['uid']);
session_destroy();
header('Location: index.php');
die("Logged out.");
case "chpasswd":
$oldmatch = comparePassword($VARS['oldpass'], $database->select('accounts', 'password', ['uid' => $_SESSION['uid']])[0]);
if ($oldmatch) {
if ($_SESSION['password'] == $VARS['oldpass']) {
if ($VARS['newpass'] == $VARS['conpass']) {
$passrank = checkWorst500List($VARS['newpass']);
if ($passrank !== FALSE) {
@ -35,8 +39,29 @@ switch ($VARS['action']) {
if (strlen($VARS['newpass']) < MIN_PASSWORD_LENGTH) {
returnToSender("weak_password");
}
$database->update('accounts', ['password' => encryptPassword($VARS['newpass'])], ['uid' => $_SESSION['uid']]);
returnToSender("password_updated");
$acctloc = account_location($_SESSION['username'], $_SESSION['password']);
if ($acctloc == "LOCAL") {
$database->update('accounts', ['password' => encryptPassword($VARS['newpass'])], ['uid' => $_SESSION['uid']]);
$_SESSION['password'] = $VARS['newpass'];
insertAuthLog(3, $_SESSION['uid']);
returnToSender("password_updated");
} else if ($acctloc == "LDAP") {
$ldapManager = new LdapManager($ldap_config);
$repository = $ldapManager->getRepository(LdapObjectType::USER);
$user = $repository->findOneByUsername($_SESSION['username']);
$user->setPassword($VARS['newpass']);
try {
$ldapManager->persist($user);
insertAuthLog(3, $_SESSION['uid']);
returnToSender("password_updated");
} catch (\Exception $e) {
returnToSender("ldap_error", $e->getMessage());
}
} else {
returnToSender("account_state_error");
}
} else {
returnToSender("new_password_mismatch");
}
@ -49,9 +74,11 @@ switch ($VARS['action']) {
returnToSender("invalid_parameters");
}
$database->update('accounts', ['authsecret' => $VARS['secret']], ['uid' => $_SESSION['uid']]);
insertAuthLog(9, $_SESSION['uid']);
returnToSender("2fa_enabled");
case "rm2fa":
$database->update('accounts', ['authsecret' => ""], ['uid' => $_SESSION['uid']]);
insertAuthLog(10, $_SESSION['uid']);
returnToSender("2fa_removed");
break;
}

@ -0,0 +1,119 @@
<?php
/**
* Simple JSON API to allow other apps to access accounts in this system.
*
* Requests can be sent via either GET or POST requests. POST is recommended
* as it has a lower chance of being logged on the server, exposing unencrypted
* user passwords.
*/
require __DIR__ . '/required.php';
require_once __DIR__ . '/lib/login.php';
header("Content-Type: application/json");
//try {
$key = $VARS['key'];
if ($database->has('apikeys', ['key' => $key]) !== TRUE) {
header("HTTP/1.1 403 Unauthorized");
die("\"403 Unauthorized\"");
}
switch ($VARS['action']) {
case "ping":
exit(json_encode(["status" => "OK"]));
break;
case "auth":
if (authenticate_user($VARS['username'], $VARS['password'])) {
insertAuthLog(12);
exit(json_encode(["status" => "OK", "msg" => lang("login successful", false)]));
} else {
insertAuthLog(13);
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
}
break;
case "userinfo":
if (user_exists($VARS['username'])) {
$data = $database->select("accounts", ["uid", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"]], ["username" => $VARS['username']])[0];
exit(json_encode(["status" => "OK", "data" => $data]));
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
}
break;
case "userexists":
if (user_exists($VARS['username'])) {
exit(json_encode(["status" => "OK", "exists" => true]));
} else {
exit(json_encode(["status" => "OK", "exists" => false]));
}
break;
case "hastotp":
if (userHasTOTP($VARS['username'])) {
exit(json_encode(["status" => "OK", "otp" => true]));
} else {
exit(json_encode(["status" => "OK", "otp" => false]));
}
break;
case "verifytotp":
if (verifyTOTP($VARS['username'], $VARS['code'])) {
exit(json_encode(["status" => "OK", "valid" => true]));
} else {
insertAuthLog(7);
exit(json_encode(["status" => "ERROR", "msg" => lang("2fa incorrect", false), "valid" => false]));
}
break;
case "acctstatus":
exit(json_encode(["status" => "OK", "account" => get_account_status($VARS['username'])]));
case "login":
// simulate a login, checking account status and alerts
if (authenticate_user($VARS['username'], $VARS['password'])) {
switch (get_account_status($VARS['username'])) {
case "LOCKED_OR_DISABLED":
insertAuthLog(5);
exit(json_encode(["status" => "ERROR", "msg" => lang("account locked", false)]));
case "TERMINATED":
insertAuthLog(5);
exit(json_encode(["status" => "ERROR", "msg" => lang("account terminated", false)]));
case "CHANGE_PASSWORD":
insertAuthLog(5);
exit(json_encode(["status" => "ERROR", "msg" => lang("password expired", false)]));
case "NORMAL":
insertAuthLog(4);
exit(json_encode(["status" => "OK"]));
case "ALERT_ON_ACCESS":
sendLoginAlertEmail($VARS['username']);
insertAuthLog(4);
exit(json_encode(["status" => "OK", "alert" => true]));
default:
insertAuthLog(5);
exit(json_encode(["status" => "ERROR", "msg" => lang("account state error", false)]));
}
} else {
insertAuthLog(5);
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
}
break;
case "ismanagerof":
if (user_exists($VARS['manager'])) {
if (user_exists($VARS['employee'])) {
$managerid = $database->select('accounts', 'uid', ['username' => $VARS['manager']]);
$employeeid = $database->select('accounts', 'uid', ['username' => $VARS['employee']]);
if ($database->has('managers', ['AND' => ['managerid' => $managerid, 'employeeid' => $employeeid]])) {
exit(json_encode(["status" => "OK", "managerof" => true]));
} else {
exit(json_encode(["status" => "OK", "managerof" => false]));
}
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("user does not exist", false), "user" => $VARS['employee']]));
}
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("user does not exist", false), "user" => $VARS['manager']]));
}
break;
default:
header("HTTP/1.1 400 Bad Request");
die("\"400 Bad Request\"");
}
/* } catch (Exception $e) {
header("HTTP/1.1 500 Internal Server Error");
die("\"500 Internal Server Error\"");
} */

@ -5,7 +5,9 @@
"require": {
"catfan/medoo": "^1.2",
"spomky-labs/otphp": "^8.3",
"endroid/qrcode": "^1.9"
"endroid/qrcode": "^1.9",
"ldaptools/ldaptools": "^0.24.0",
"guzzlehttp/guzzle": "^6.2"
},
"authors": [
{

487
composer.lock generated

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "3d5a548f8a7cbbd0c911987b1fab33a5",
"content-hash": "4965262916e04d361db07e7f14ed06d6",
"packages": [
{
"name": "beberlei/assert",
@ -230,6 +230,244 @@
],
"time": "2017-04-08T09:13:59+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.2.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/8d6c6cc55186db87b7dc5009827429ba4e9dc006",
"reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006",
"shasum": ""
},
"require": {
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.4",
"php": ">=5.5"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^4.0",
"psr/log": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.2-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle is a PHP HTTP client library",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"rest",
"web service"
],
"time": "2017-02-28T22:50:30+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "v1.3.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"shasum": ""
},
"require": {
"php": ">=5.5.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"time": "2016-12-20T10:07:11+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.4.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
"reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Schultze",
"homepage": "https://github.com/Tobion"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"request",
"response",
"stream",
"uri",
"url"
],
"time": "2017-03-20T17:10:46+00:00"
},
{
"name": "ldaptools/ldaptools",
"version": "v0.24.0",
"source": {
"type": "git",
"url": "https://github.com/ldaptools/ldaptools.git",
"reference": "31e05ae6082fc7e61afc666e2c773ee8cb0e47b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ldaptools/ldaptools/zipball/31e05ae6082fc7e61afc666e2c773ee8cb0e47b5",
"reference": "31e05ae6082fc7e61afc666e2c773ee8cb0e47b5",
"shasum": ""
},
"require": {
"ext-ldap": "*",
"php": ">=5.6",
"ramsey/uuid": ">=3.0",
"symfony/event-dispatcher": ">=2.0",
"symfony/yaml": ">=2.0"
},
"require-dev": {
"doctrine/cache": "~1.0",
"friendsofphp/php-cs-fixer": "~1.0",
"phpspec/phpspec": "~3.0",
"tedivm/stash": ">=0.14.1"
},
"suggest": {
"doctrine/cache": "Provides the cache_type 'doctrine' to help increase performance.",
"ext-intl": "Better UTF-8 handling.",
"ext-mbstring": "Better UTF-8 handling.",
"tedivm/stash": "Provides the cache_type 'stash' to help increase performance."
},
"type": "library",
"autoload": {
"psr-4": {
"LdapTools\\": "src/LdapTools"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chad Sikorra",
"email": "Chad.Sikorra@gmail.com",
"homepage": "http://www.chadsikorra.com"
}
],
"description": "LdapTools is a feature-rich LDAP library for PHP 5.6+.",
"homepage": "http://www.phpldaptools.com",
"keywords": [
"Microsoft Exchange",
"active directory",
"ldap",
"openldap"
],
"time": "2017-04-09T23:39:51+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v2.0.10",
@ -278,6 +516,138 @@
],
"time": "2017-03-13T16:27:32+00:00"
},
{
"name": "psr/http-message",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"time": "2016-08-06T14:39:51+00:00"
},
{
"name": "ramsey/uuid",
"version": "3.6.1",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/4ae32dd9ab8860a4bbd750ad269cba7f06f7934e",
"reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e",
"shasum": ""
},
"require": {
"paragonie/random_compat": "^1.0|^2.0",
"php": "^5.4 || ^7.0"
},
"replace": {
"rhumsaa/uuid": "self.version"
},
"require-dev": {
"apigen/apigen": "^4.1",
"codeception/aspect-mock": "^1.0 | ^2.0",
"doctrine/annotations": "~1.2.0",
"goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1",
"ircmaxell/random-lib": "^1.1",
"jakub-onderka/php-parallel-lint": "^0.9.0",
"mockery/mockery": "^0.9.4",
"moontoast/math": "^1.1",
"php-mock/php-mock-phpunit": "^0.3|^1.1",
"phpunit/phpunit": "^4.7|>=5.0 <5.4",
"satooshi/php-coveralls": "^0.6.1",
"squizlabs/php_codesniffer": "^2.3"
},
"suggest": {
"ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator",
"ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator",
"ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
"moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).",
"ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid",
"ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Ramsey\\Uuid\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marijn Huizendveld",
"email": "marijn.huizendveld@gmail.com"
},
{
"name": "Thibaud Fabre",
"email": "thibaud@aztech.io"
},
{
"name": "Ben Ramsey",
"email": "ben@benramsey.com",
"homepage": "https://benramsey.com"
}
],
"description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).",
"homepage": "https://github.com/ramsey/uuid",
"keywords": [
"guid",
"identifier",
"uuid"
],
"time": "2017-03-26T20:37:53+00:00"
},
{
"name": "spomky-labs/otphp",
"version": "v8.3.0",
@ -342,6 +712,66 @@
],
"time": "2016-12-08T10:46:02+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v3.2.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "154bb1ef7b0e42ccc792bd53edbce18ed73440ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/154bb1ef7b0e42ccc792bd53edbce18ed73440ca",
"reference": "154bb1ef7b0e42ccc792bd53edbce18ed73440ca",
"shasum": ""
},
"require": {
"php": ">=5.5.9"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/config": "~2.8|~3.0",
"symfony/dependency-injection": "~2.8|~3.0",
"symfony/expression-language": "~2.8|~3.0",
"symfony/stopwatch": "~2.8|~3.0"
},
"suggest": {
"symfony/dependency-injection": "",
"symfony/http-kernel": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.2-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\EventDispatcher\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
"time": "2017-04-04T07:26:27+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v3.2.7",
@ -562,6 +992,61 @@
"shim"
],
"time": "2016-11-14T01:06:16+00:00"
},
{
"name": "symfony/yaml",
"version": "v3.2.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "62b4cdb99d52cb1ff253c465eb1532a80cebb621"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/62b4cdb99d52cb1ff253c465eb1532a80cebb621",
"reference": "62b4cdb99d52cb1ff253c465eb1532a80cebb621",
"shasum": ""
},
"require": {
"php": ">=5.5.9"
},
"require-dev": {
"symfony/console": "~2.8|~3.0"
},
"suggest": {
"symfony/console": "For validating YAML files using the lint command"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.2-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2017-03-20T09:45:15+00:00"
}
],
"packages-dev": [],

Fichier binaire non affiché.

@ -34,7 +34,7 @@ if (!is_empty($_GET['page'])) {
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<img class="img-responsive banner-image" src="static/img/banner.png" />
<img class="img-responsive banner-image" src="static/img/logo.svg" />
</div>
</div>
<nav class="navbar navbar-inverse">
@ -130,17 +130,18 @@ END;
if ($appcount == 1) {
?>
<div class="hidden-xs col-sm-3 col-md-4 col-lg-4">
<!-- Placeholder column for nice center-align -->
<!-- Empty placeholder column for nice center-align -->
</div>
<?php
} else if ($appcount == 2) {
?>
<div class="hidden-xs hidden-sm col-md-2 col-lg-2">
<!-- Placeholder column for nice center-align -->
<!-- Empty placeholder column for nice center-align -->
</div>
<?php
}
// Load app widgets
foreach (APPS[$pageid] as $app) {
if (file_exists(__DIR__ . "/apps/" . $app . ".php")) {
include_once __DIR__ . "/apps/" . $app . ".php";

@ -7,43 +7,52 @@ require_once __DIR__ . "/lib/login.php";
$userpass_ok = false;
$multiauth = false;
if ($VARS['progress'] == "1") {
if (authenticate_user($VARS['username'], $VARS['password'])) {
switch (get_account_status($VARS['username'])) {
case "LOCKED_OR_DISABLED":
$alert = lang("account locked", false);
break;
case "TERMINATED":
$alert = lang("account terminated", false);
break;
case "CHANGE_PASSWORD":
$alert = lang("password expired", false);
case "NORMAL":
$userpass_ok = true;
break;
case "ALERT_ON_ACCESS":
sendAlertEmail($VARS['username']);
$userpass_ok = true;
break;
}
if ($userpass_ok) {
if (userHasTOTP($VARS['username'])) {
$multiauth = true;
} else {
doLoginUser($VARS['username']);
header('Location: home.php');
die("Logged in, go to home.php");
if (!RECAPTCHA_ENABLED || (RECAPTCHA_ENABLED && verifyReCaptcha($VARS['g-recaptcha-response']))) {
if (authenticate_user($VARS['username'], $VARS['password'])) {
switch (get_account_status($VARS['username'])) {
case "LOCKED_OR_DISABLED":
$alert = lang("account locked", false);
break;
case "TERMINATED":
$alert = lang("account terminated", false);
break;
case "CHANGE_PASSWORD":
$alert = lang("password expired", false);
case "NORMAL":
$userpass_ok = true;
break;
case "ALERT_ON_ACCESS":
sendLoginAlertEmail($VARS['username']);
$userpass_ok = true;
break;
}
if ($userpass_ok) {
if (userHasTOTP($VARS['username'])) {
$multiauth = true;
} else {
doLoginUser($VARS['username'], $VARS['password']);
insertAuthLog(1, $_SESSION['uid']);
header('Location: home.php');
die("Logged in, go to home.php");
}
}
} else {
$alert = lang("login incorrect", false);
insertAuthLog(2);
}
} else {
$alert = lang("login incorrect", false);
$alert = lang("captcha error", false);
insertAuthLog(8);
}
} else if ($VARS['progress'] == "2") {
if (verifyTOTP($VARS['username'], $VARS['authcode'])) {
doLoginUser($VARS['username']);
doLoginUser($VARS['username'], $VARS['password']);
insertAuthLog(1, $_SESSION['uid']);
header('Location: home.php');
die("Logged in, go to home.php");
} else {
$alert = lang("2fa incorrect", false);
insertAuthLog(6);
}
}
?>
@ -58,13 +67,16 @@ if ($VARS['progress'] == "1") {
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
<?php if (RECAPTCHA_ENABLED) { ?>
<script src='https://www.google.com/recaptcha/api.js'></script>
<?php } ?>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<div>
<img class="img-responsive banner-image" src="static/img/banner.png" />
<img class="img-responsive banner-image" src="static/img/logo.svg" />
</div>
<div class="panel panel-primary">
<div class="panel-heading">
@ -85,6 +97,10 @@ if ($VARS['progress'] == "1") {
?>
<input type="text" class="form-control" name="username" placeholder="<?php lang("username"); ?>" required="required" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php lang("password"); ?>" required="required" /><br />
<?php if (RECAPTCHA_ENABLED) { ?>
<div class="g-recaptcha" data-sitekey="<?php echo RECAPTCHA_SITE_KEY; ?>"></div>
<br />
<?php } ?>
<input type="hidden" name="progress" value="1" />
<?php
} else if ($multiauth) {

@ -9,9 +9,11 @@ define("STRINGS", [
"2fa prompt" => "Enter the six-digit code from your mobile authenticator app.",
"2fa incorrect" => "Authentication code incorrect.",
"login incorrect" => "Login incorrect.",
"login successful" => "Login successful.",
"account locked" => "This account has been disabled. Contact technical support.",
"password expired" => "You must change your password before continuing.",
"account terminated" => "Account terminated. Access denied.",
"account state error" => "Your account state is not stable. Log out, restart your browser, and try again.",
"password on 500 list" => "The given password is ranked number {arg} out of the 500 most common passwords. Try a different one.",
"welcome user" => "Welcome, {user}!",
"change password" => "Change password",
@ -36,5 +38,8 @@ define("STRINGS", [
"scan 2fa qrcode" => "Scan the QR Code with the authenticator app, or enter the secret key manually.",
"confirm 2fa" => "Finish setup",
"invalid parameters" => "Invalid request parameters.",
"ldap server error" => "The LDAP server returned an error: {arg}",
"user does not exist" => "User does not exist.",
"captcha error" => "There was a problem with the CAPTCHA (robot test). Try again.",
"home" => "Home",
]);

@ -32,5 +32,13 @@ define("MESSAGES", [
"password_500" => [
"string" => "password on 500 list",
"type" => "danger"
],
"account_state_error" => [
"string" => "account state error",
"type" => "danger"
],
"ldap_error" => [
"string" => "ldap server error",
"type" => "danger"
]
]);

@ -1,32 +1,317 @@
<?php
/**
* Authentication and account functions
*/
use Base32\Base32;
use OTPHP\TOTP;
use LdapTools\LdapManager;
use LdapTools\Connection\ADResponseCodes;
////////////////////////////////////////////////////////////////////////////////
// Account handling //
////////////////////////////////////////////////////////////////////////////////
/**
* Send an alert email to the system admin
*
* Used when an account with the status ALERT_ON_ACCESS logs in
* @param String $username the account username
* Add a user to the system. /!\ Assumes input is OK /!\
* @param string $username Username, saved in lowercase.
* @param string $password Password, will be hashed before saving.
* @param string $realname User's real legal name
* @param string $email User's email address.
* @param string $phone1 Phone number #1
* @param string $phone2 Phone number #2
* @param string $type Account type
* @return int The new user's ID number in the database.
*/
function sendAlertEmail($username) {
// TODO: add email code
function adduser($username, $password, $realname, $email = null, $phone1 = "", $phone2 = "", $type) {
global $database;
$database->insert('accounts', [
'username' => strtolower($username),
'password' => (is_null($password) ? null : encryptPassword($password)),
'realname' => $realname,
'email' => $email,
'phone1' => $phone1,
'phone2' => $phone2,
'acctstatus' => 1,
'accttype' => $type
]);
var_dump($database->error());
return $database->id();
}
/**
* Get where a user's account actually is.
* @param string $username
* @return string "LDAP", "LOCAL", "LDAP_ONLY", or "NONE".
*/
function account_location($username, $password) {
global $database;
$username = strtolower($username);
$user_exists = user_exists($username);
if (!$user_exists && !LDAP_ENABLED) {
return false;
}
if ($user_exists) {
$userinfo = $database->select('accounts', ['password'], ['username' => $username])[0];
// if password empty, it's an LDAP user
if (is_empty($userinfo['password']) && LDAP_ENABLED) {
return "LDAP";
} else if (is_empty($userinfo['password']) && !LDAP_ENABLED) {
return "NONE";
} else {
return "LOCAL";
}
} else {
if (user_exists_ldap($username, $password)) {
return "LDAP_ONLY";
} else {
return "NONE";
}
}
}
/**
* Checks the given credentials against the database.
* @param string $username
* @param string $password
* @return boolean True if OK, else false
*/
function authenticate_user($username, $password) {
global $database;
global $ldap_config;
$username = strtolower($username);
if (is_empty($username) || is_empty($password)) {
return false;
}
$loc = account_location($username, $password);
if ($loc == "NONE") {
return false;
} else if ($loc == "LOCAL") {
$hash = $database->select('accounts', ['password'], ['username' => $username, "LIMIT" => 1])[0]['password'];
return (comparePassword($password, $hash));
} else if ($loc == "LDAP") {
return authenticate_user_ldap($username, $password) === TRUE;
} else if ($loc == "LDAP_ONLY") {
try {
if (authenticate_user_ldap($username, $password) === TRUE) {
$user = (new LdapManager($ldap_config))->getRepository('user')->findOneByUsername($username);
//var_dump($user);
adduser($user->getUsername(), null, $user->getName(), ($user->hasEmailAddress() ? $user->getEmailAddress() : null), "", "", 2);
return true;
} else {
return false;
}
} catch (Exception $e) {
sendError("LDAP error: " . $e->getMessage());
}
} else {
return false;
}
}
/**
* Check if a username exists in the local database.
* @param String $username
*/
function user_exists($username) {
global $database;
$username = strtolower($username);
return $database->has('accounts', ['username' => $username, "LIMIT" => QUERY_LIMIT]);
}
/**
* Get the account status: NORMAL, TERMINATED, LOCKED_OR_DISABLED,
* CHANGE_PASSWORD, or ALERT_ON_ACCESS
* @global $database $database
* @param string $username
* @return string
*/
function get_account_status($username) {
global $database;
$username = strtolower($username);
$loc = account_location($username);
if ($loc == "LOCAL") {
$statuscode = $database->select('accounts', [
'[>]acctstatus' => [
'acctstatus' => 'statusid'
]
], [
'accounts.acctstatus',
'acctstatus.statuscode'
], [
'username' => $username,
"LIMIT" => 1
]
)[0]['statuscode'];
return $statuscode;
} else if ($loc == "LDAP") {
// TODO: Read actual account status from AD servers
return "NORMAL";
} else {
// account isn't setup properly
return "LOCKED_OR_DISABLED";
}
}
////////////////////////////////////////////////////////////////////////////////
// Login handling //
////////////////////////////////////////////////////////////////////////////////
/**
* Setup $_SESSION values to log in a user
* @param string $username
*/
function doLoginUser($username) {
function doLoginUser($username, $password) {
global $database;
$username = strtolower($username);
$userinfo = $database->select('accounts', ['email', 'uid', 'realname'], ['username' => $username])[0];
$_SESSION['username'] = $username;
$_SESSION['uid'] = $userinfo['uid'];
$_SESSION['email'] = $userinfo['email'];
$_SESSION['realname'] = $userinfo['realname'];
$_SESSION['password'] = $password; // needed for things like EWS
$_SESSION['loggedin'] = true;
}
/**
* Send an alert email to the system admin
*
* Used when an account with the status ALERT_ON_ACCESS logs in
* @param String $username the account username
*/
function sendLoginAlertEmail($username) {
// TODO: add email code
}
function insertAuthLog($type, $uid = null) {
global $database;
$ip = "";
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
$ip = $_SERVER["HTTP_CF_CONNECTING_IP"];
} else if (isset($_SERVER["HTTP_CLIENT_IP"])) {
$ip = $_SERVER["HTTP_CLIENT_IP"];
} else if (isset($_SERVER["HTTP_X_FORWARDED_FOR"])) {
$ip = $_SERVER["HTTP_X_FORWARDED_FOR"];
} else if (isset($_SERVER["HTTP_X_FORWARDED"])) {
$ip = $_SERVER["HTTP_X_FORWARDED"];
} else if (isset($_SERVER["HTTP_FORWARDED_FOR"])) {
$ip = $_SERVER["HTTP_FORWARDED_FOR"];
} else if (isset($_SERVER["HTTP_FORWARDED"])) {
$ip = $_SERVER["HTTP_FORWARDED"];
} else if (isset($_SERVER["REMOTE_ADDR"])) {
$ip = $_SERVER["REMOTE_ADDR"];
} else {
$ip = "NOT FOUND";
}
$database->insert("authlog", ['#logtime' => 'NOW()', 'logtype' => $type, 'uid' => $uid, 'ip' => $ip]);
}
function verifyReCaptcha($response) {
try {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', "https://www.google.com/recaptcha/api/siteverify", [
'form_params' => [
'secret' => RECAPTCHA_SECRET_KEY,
'response' => $response
]
]);
if ($response->getStatusCode() != 200) {
return false;
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['success'] === true) {
return true;
} else {
return false;
}
} catch (Exception $e) {
return false;
}
}
////////////////////////////////////////////////////////////////////////////////
// LDAP handling //
////////////////////////////////////////////////////////////////////////////////
/**
* Checks the given credentials against the LDAP server.
* @param string $username
* @param string $password
* @return mixed True if OK, else false or the error code from the server
*/
function authenticate_user_ldap($username, $password) {
global $ldap_config;
if (is_empty($username) || is_empty($password)) {
return false;
}
$username = strtolower($username);
try {
$ldapManager = new LdapManager($ldap_config);
$msg = "";
$code = 0;
if ($ldapManager->authenticate($username, $password, $msg, $code) === TRUE) {
return true;
} else {
return $code;
}
} catch (Exception $e) {
sendError("LDAP error: " . $e->getMessage());
}
}
/**
* Check if a username exists on the LDAP server.
* @global type $ldap_config
* @param type $username
* @return boolean true if yes, else false
*/
function user_exists_ldap($username, $password) {
global $ldap_config;
try {
$ldap = new LdapManager($ldap_config);
$username = strtolower($username);
if (!$ldap->authenticate($username, $password, $message, $code)) {
switch ($code) {
case ADResponseCodes::ACCOUNT_INVALID:
return false;
case ADResponseCodes::ACCOUNT_CREDENTIALS_INVALID:
return true;
case ADResponseCodes::ACCOUNT_RESTRICTIONS:
return true;
case ADResponseCodes::ACCOUNT_RESTRICTIONS_TIME:
return true;
case ADResponseCodes::ACCOUNT_RESTRICTIONS_DEVICE:
return true;
case ADResponseCodes::ACCOUNT_PASSWORD_EXPIRED:
return true;
case ADResponseCodes::ACCOUNT_DISABLED:
return true;
case ADResponseCodes::ACCOUNT_CONTEXT_IDS:
return true;
case ADResponseCodes::ACCOUNT_EXPIRED:
return false;
case ADResponseCodes::ACCOUNT_PASSWORD_MUST_CHANGE:
return true;
case ADResponseCodes::ACCOUNT_LOCKED:
return true;
default:
return false;
}
}
return true;
} catch (Exception $e) {
sendError("LDAP error: " . $e->getMessage());
}
}
////////////////////////////////////////////////////////////////////////////////
// 2-factor authentication //
////////////////////////////////////////////////////////////////////////////////
/**
* Check if a user has TOTP setup
* @global $database $database
@ -35,6 +320,7 @@ function doLoginUser($username) {
*/
function userHasTOTP($username) {
global $database;
$username = strtolower($username);
$secret = $database->select('accounts', 'authsecret', ['username' => $username])[0];
if (is_empty($secret)) {
return false;
@ -49,10 +335,11 @@ function userHasTOTP($username) {
*/
function newTOTP($username) {
global $database;
$username = strtolower($username);
$secret = random_bytes(20);
$encoded_secret = Base32::encode($secret);
$userdata = $database->select('accounts', ['email', 'authsecret'], ['username' => $username])[0];
$totp = new TOTP($userdata['email'], $encoded_secret);
$userdata = $database->select('accounts', ['email', 'authsecret', 'realname'], ['username' => $username])[0];
$totp = new TOTP((is_null($userdata['email']) ? $userdata['realname'] : $userdata['email']), $encoded_secret);
$totp->setIssuer(SYSTEM_NAME);
return $totp->getProvisioningUri();
}
@ -65,6 +352,7 @@ function newTOTP($username) {
*/
function saveTOTP($username, $secret) {
global $database;
$username = strtolower($username);
$database->update('accounts', ['authsecret' => $secret], ['username' => $username]);
}
@ -77,6 +365,7 @@ function saveTOTP($username, $secret) {
*/
function verifyTOTP($username, $code) {
global $database;
$username = strtolower($username);
$userdata = $database->select('accounts', ['email', 'authsecret'], ['username' => $username])[0];
if (is_empty($userdata['authsecret'])) {
return false;

@ -3,7 +3,7 @@
<type>org.netbeans.modules.php.project</type>
<configuration>
<data xmlns="http://www.netbeans.org/ns/php-project/1">
<name>NetsymsBusinessSSO</name>
<name>BusinessPortal</name>
</data>
</configuration>
</project>

@ -16,9 +16,10 @@ $session_length = 60 * 60; // 1 hour
session_set_cookie_params($session_length, "/", null, false, true);
session_start(); // stick some cookies in it
//
// Composer
require __DIR__ . '/vendor/autoload.php';
// Settings file
require __DIR__ . '/settings.php';
// List of alert messages
@ -123,27 +124,6 @@ function lang2($key, $replace, $echo = true) {
}
}
/**
* Add a user to the system. /!\ Assumes input is OK /!\
* @param string $username Username, saved in lowercase.
* @param string $password Password, will be hashed before saving.
* @param string $realname User's real legal name
* @param string $email User's email address.
* @return int The new user's ID number in the database.
*/
function adduser($username, $password, $realname, $email = "NOEMAIL@EXAMPLE.COM", $phone1 = "", $phone2 = "") {
global $database;
$database->insert('accounts', [
'username' => strtolower($username),
'password' => encryptPassword($password),
'realname' => $realname,
'email' => $email,
'phone1' => $phone1,
'phone2' => $phone2
]);
return $database->id();
}
/**
* Checks if an email address is valid.
* @param string $email Email to check
@ -153,87 +133,6 @@ function isValidEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
* Check if an email exists in the database.
* @param String $email
*/
function email_exists($email) {
global $database;
return $database->has('accounts', ['email' => $email, "LIMIT" => QUERY_LIMIT]);
}
/**
* Check if a username exists in the database.
* @param String $username
*/
function user_exists($username) {
global $database;
return $database->has('accounts', ['username' => $username, "LIMIT" => QUERY_LIMIT]);
}
/**
* Checks the given credentials against the database.
* @param string $username
* @param string $password
* @return boolean True if OK, else false
*/
function authenticate_user($username, $password) {
global $database;
if (is_empty($username) || is_empty($password)) {
return false;
}
if (!user_exists($username)) {
return false;
}
$hash = $database->select('accounts', ['password'], ['username' => $username, "LIMIT" => 1])[0]['password'];
return (comparePassword($password, $hash));
}
function get_account_status($username) {
global $database;
$statuscode = $database->select('accounts', [
'[>]acctstatus' => [
'acctstatus' => 'statusid'
]
], [
'accounts.acctstatus',
'acctstatus.statuscode'
], [
'username' => $username,
"LIMIT" => 1
]
)[0]['statuscode'];
return $statuscode;
}
/**
* Checks the given credentials to see if they're legit.
* @param string $username
* @param string $password
* @return boolean True if OK, else false
*/
function authenticate_user_ldap($username, $password) {
$ds = ldap_connect(LDAP_SERVER);
if ($ds) {
$sr = ldap_search($ds, LDAP_BASEDN, "(|(uid=" . $username . ")(mail=" . $username . "))", ['cn', 'uid', 'mail']);
if (ldap_count_entries($ds, $sr) == 1) {
$info = ldap_get_entries($ds, $sr);
$name = $info[0]["cn"][0];
$uid = $info[0]["uid"][0];
$mail = $info[0]["mail"][0];
$_SESSION['uid'] = $uid;
$_SESSION['name'] = $name;
$_SESSION['mail'] = $mail;
return true;
} else if (ldap_count_entries($ds, $sr) > 1) {
sendError("Multiple users matched search criteria. Unsure which one you are.");
} else {
return false;
}
} else {
sendError("Login server offline.");
}
}
/**
* Hashes the given plaintext password

@ -13,8 +13,28 @@ define("DB_USER", "sso");
define("DB_PASS", "");
define("DB_CHARSET", "utf8");
define("LDAP_SERVER", "example.com");
define("LDAP_BASEDN", "ou=users,dc=example,dc=com");
define("LDAP_ENABLED", TRUE);
// See https://github.com/ldaptools/ldaptools/blob/master/docs/en/reference/Main-Configuration.md
// for info on the LDAP config
/*
* Begin LDAP Configuration
*/
use LdapTools\Configuration;
use LdapTools\DomainConfiguration;
$ldap_config = new Configuration();
$ldap_config_domain = (new DomainConfiguration('example'))
->setDomainName("example.com")
->setServers(['192.168.25.131'])
->setLazyBind(TRUE)
->setUsername("readonly-bind")
->setPassword("password")
->setUseTls(TRUE);
$ldap_config->addDomain($ldap_config_domain);
/*
* End LDAP Configuration
*/
define("SITE_TITLE", "Netsyms Business Apps :: Single Sign On");
@ -27,6 +47,12 @@ define("TIMEZONE", "America/Denver");
// Base URL for site links.
define('URL', 'http://localhost:8000/');
// Use reCAPTCHA on login screen
// https://www.google.com/recaptcha/
define("RECAPTCHA_ENABLED", FALSE);
define('RECAPTCHA_SITE_KEY', '');
define('RECAPTCHA_SECRET_KEY', '');
// See lang folder for language options
define('LANGUAGE', "en_us");
@ -46,9 +72,10 @@ define("QUERY_LIMIT", 1000);
///////////////////////////////////////////////////////////////////////////////////////////////
// /!\ Warning: Changing these values may violate the terms of your license agreement! /!\ //
///////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////
// /!\ Warning: Changing these values may /!\ //
// /!\ violate the terms of your license agreement! /!\ //
//////////////////////////////////////////////////////////////
define("LICENSE_TEXT", "<b>Unlicensed Demo: For Trial Use Only</b>");
define("COPYRIGHT_NAME", "Netsyms Technologies");
/////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="512"
height="512"
viewBox="0 0 512.00001 512.00001"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="logo.svg"
inkscape:export-filename="/home/skylar/Documents/Projects/Assets/BusinessPortal/logo_512.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.49497475"
inkscape:cx="-135.9681"
inkscape:cy="352.66131"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-540.36216)">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.74509804"
id="rect4726"
width="512"
height="512"
x="0"
y="540.36218"
rx="50"
ry="50" />
<ellipse
style="opacity:1;fill:none;fill-opacity:1;stroke:#2196f3;stroke-width:50;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4155"
cx="901.47205"
cy="-256"
rx="68.690376"
ry="193.9493"
transform="matrix(0,1,-1,0,0,0)" />
<path
inkscape:connector-curvature="0"
d="m 257.45991,599.83707 c 9.94158,-3.2506 20.98401,-3.01096 30.77413,0.67362 8.13772,3.03356 15.37803,8.41343 20.65625,15.31019 5.7326,7.42341 9.11199,16.6258 9.55504,25.99551 0.63974,11.77034 -3.4201,23.71233 -11.14414,32.62095 6.11458,6.07391 10.75535,13.62837 13.37297,21.84299 3.28899,10.26035 3.40883,21.51078 0.35265,31.84111 -2.44585,8.32302 -6.96227,16.01318 -12.98644,22.24763 -5.15388,5.31434 -11.38153,9.60704 -18.23981,12.40548 -0.0129,14.28395 -0.007,28.56791 -0.004,42.85187 -0.50863,0 -1.01271,-0.0143 -1.51227,-0.0143 0.0129,30.01462 -10e-4,60.02924 0.009,90.04386 0.0543,0.47919 -0.14918,0.95395 -0.54929,1.23189 -4.56391,3.42459 -9.12781,6.85389 -13.69623,10.2762 -0.5787,0.4362 -1.43991,0.42506 -2.02089,0.0143 -5.08607,-3.42245 -10.18796,-6.8199 -15.26953,-10.24677 -0.44305,-0.26224 -0.65099,-0.76871 -0.58547,-1.26589 0.0159,-30.01462 -0.0129,-60.02696 0.0159,-90.04172 -2.60182,0 -5.20363,0 -7.80317,0 -0.0114,-4.06875 0.0339,-8.13992 -0.0181,-12.20652 -0.53574,1.44442 -1.03529,2.90698 -1.60493,4.3424 -0.43399,-0.18082 -0.86577,-0.34593 -1.29978,-0.50632 -9.53697,26.00443 -19.14397,51.98187 -28.70129,77.97974 -0.13112,0.4272 -0.38428,0.85668 -0.859,0.94937 -5.01825,1.51913 -10.04102,3.01768 -15.06381,4.52094 -0.74369,0.26452 -1.57784,-0.1014 -1.97343,-0.75727 -3.15789,-4.37183 -6.3248,-8.7368 -9.50077,-13.09491 -0.37071,-0.47476 -0.80472,-1.06922 -0.486,-1.6908 7.00975,-19.06727 14.04209,-38.12755 21.05862,-57.19253 2.5566,-6.99387 5.19005,-13.96288 7.69466,-20.97717 -2.25143,-0.78213 -4.48025,-1.62982 -6.71587,-2.45493 4.56389,-12.36478 9.10972,-24.73398 13.66911,-37.10119 -6.0965,-5.61043 -10.75082,-12.76712 -13.42272,-20.60638 -3.3568,-9.71789 -3.59191,-20.48227 -0.62388,-30.32899 3.38618,-11.61193 11.17807,-21.8587 21.46999,-28.21756 -5.26461,-8.55355 -7.54771,-18.87946 -6.50336,-28.85942 0.91548,-9.41942 4.851,-18.50206 11.03791,-25.65645 5.5314,-6.43108 12.84403,-11.32052 20.91847,-13.92679 m -13.13113,14.70443 c -6.2276,5.76422 -10.57903,13.53348 -12.16813,21.8746 -1.94174,9.85347 -0.052,20.40536 5.25335,28.94094 4.48253,-2.07972 9.27248,-3.49243 14.16418,-4.18643 0.74819,-0.10855 1.51225,-0.12855 2.24914,-0.3278 7.64496,-2.25365 15.79171,-2.74188 23.66271,-1.53254 9.18205,1.406 17.95044,5.28493 25.23598,11.04019 2.56112,-3.05396 4.71761,-6.46055 6.29543,-10.12481 4.19092,-9.57528 4.426,-20.79176 0.65329,-30.53671 -2.92507,-7.68337 -8.28467,-14.40606 -15.09097,-19.01291 -7.28325,-4.97078 -16.23022,-7.42788 -25.03026,-6.89669 -9.32448,0.49954 -18.40936,4.3808 -25.22472,10.76216 m 20.4596,59.08886 c -3.87446,1.01265 -7.05947,4.14802 -8.22589,7.9681 7.28328,2.99525 15.45491,3.74567 23.17444,2.22669 -0.36167,-3.1128 -1.94175,-6.08077 -4.4102,-8.02923 -2.90697,-2.33281 -6.94871,-3.20306 -10.53835,-2.16556 m -7.45051,14.82199 c 1.12344,2.37566 2.91375,4.45081 5.19004,5.78911 3.65069,2.2017 8.43386,2.41865 12.2337,0.46562 1.66825,-1.08506 2.90924,-2.7103 3.84961,-4.44181 -7.10695,1.04193 -14.43996,0.39321 -21.27335,-1.81292 z"
id="path3"
style="fill:#ff9100;fill-opacity:1" />
</g>
</svg>

Après

Largeur:  |  Hauteur:  |  Taille: 5.2 KiB

Fichier binaire non affiché.

Après

Largeur:  |  Hauteur:  |  Taille: 19 KiB

Chargement…
Annuler
Enregistrer