v 1.0.4 - removed name@grid dependence

This commit is contained in:
Cuga Rajal
2024-12-17 17:15:13 -08:00
parent 1204851fb8
commit a76cd69945
5 changed files with 134 additions and 116 deletions
+29 -16
View File
@@ -3,32 +3,35 @@ HGAuth
An Opensim authentication module that can enforce a Web Form submission before allowing
inbound HG teleport
Version 1.0.3, December 7, 2022
Version 1.0.4, December 17, 2024
-----
**Summary**
This is a re-write of Project Sasha which has not been developed since 2018.
It is a set of PHP scripts that provide a way to enforce inbound HG teleporting
This is a set of PHP scripts that provide a way to enforce inbound HG teleporting
avatars (from other grids) to agree to terms presented on a web page, before they
are allowed to enter.
are allowed to enter.
December 2024: I have re-written this to be compatible with the latest viewers and
OS server versions. It now uses UUIDs for authentication.
Although I am not aware of any issues, please use it at your own risk.
-----
**How it works**
Avatars attempting to HG teleport to a grid with this package installed, will
receive a rejection dialog in the Viewer with a customizable message and an
Avatars attempting to HG teleport to a grid for the first time with this package installed, will
receive a rejection dialog in the Viewer that contains a customizable message and an
external link. Clicking that link will take them to an external web page with an
on-page message and a form. The web form pre-fills their avatar address so they
can not enter it manually. They are asked to confirm or reject the agreement.
on-page message and a form.
Clicking the Confirm/Yes button on the form will authorize them for future
inbound HG teleports.
The package prevents the avatar name from being altered and prevents submitting
avatar names other than the one actually used in the viewer.
The current implementation authenticates based on avatar's UUID in the viewer
and prevents this from being altered. The web form has security features to prevent
a variety of misuses.
The verbiage on the web page can be changed or adapted to suit your needs.
Project Sasha was originally developed to enforce legal requirements of GDPR for
@@ -37,11 +40,20 @@ residents of the EU. However the form can be used to enforce TOS or other needs.
-----
**Recent changes**
Version 1.0.3 adds a workaround for a bug introduced in late 2022.
The Opensim dev team is investigating this bug.
Please note that as part of the workaround,
avatars who authenticate with the web form must restart their viewers
after authenticating.
Version 1.0.4 removes user@grid data in the authentication process due to
unresolved issues in the HTTP requests' sequence of XML payloads. This version fixes
viewer instability due to these bugs; However, user@grid information is no longer
availabe for display on web forms or authentication. Authentication is now based on UUID.
This change allows bug-free HG-TPs after signing the form.
The hgauth database table in Version 1.0.4 has an change since 1.0.3. Table data
from previous versions is compatible, however, existing tables migrating to 1.0.4 MUST
remove/delete the UNIQUE key for 'avatarname' before using version 1.0.4. You can optionally
add an INDEX key for 'avatarname' to speed up queries.
Version 1.0.3 added a workaround to keep user@grid data available for display and
authentication. However, newer viewer releases developed an incompatibility to
this with the symptom of requiring a viewer restart before a successful 2nd teleport.
Version 1.0.2 removed development code and added minor UI improvements.
@@ -101,8 +113,9 @@ renamed .htaccess and placed in the directory containing the PHP scripts. Apache
may need to be configured to read the .htaccess file.
- authconfig.php - should not be accessed directly, it is meant to be an include file only
- hgauth.php - access should be restricted to the IP of the Opensim server's inbound HTTP connection
- authconfig.php - should not be accessed directly, it is meant to be an include file only -
recommend placing this outside the document root or using .htaccess to prevent access
- hgauth.php - access should be restricted to the IP of the hosting Opensim server's inbound HTTP connection
- index.php - unrestricted web page
Though not critical, make sure your date.timezone is set in your php.ini.
+1 -1
View File
@@ -7,7 +7,7 @@ $db_user = 'dbuser';
$db_pass = 'dbpass';
$tablename = "hgauth"; //this is the table name that will store your authorizations.
$authlink = "http://mydomain.com/path/to/index.php"; //name of the page your users will use to submit consent
$authlink = "http://mydomain.com/path/to/index.php"; // URL your users will use to submit consent
function base64_url_encode($input) {
+48 -49
View File
@@ -1,91 +1,90 @@
<?php
// HGAuth version 1.0.4
// Github: https://github.com/cuga-rajal/hgauth
//
// This file is expected to be used to receive HG teleport authorization requests from an
// Opensim Authorization Service and to send responses back to that service.
include 'authconfig.php';
include '/full/path/to/authconfig.php'; // suggest placing this file outside the document root
$dbc = mysqli_connect($db_server, $db_user, $db_pass, $db_name);
$msgformat = "%s, please visit the following link to accept Terms of Service and GDPR: %s";
$newmsg = "Please visit the following link to accept Terms of Service and GDPR: ";
$failmsg = "Authentication has failed. Please try again or from another region. If the error persists, please contact the grid admin.";
class AuthorizationResponse {
private $m_isAuthorized;
private $m_message;
public function AuthorizationResponse($isAuthorized,$message) {
$this->m_isAuthorized = $isAuthorized;
$this->m_message = $message;
}
public function toXML() {
return '<?xml version="1.0" encoding="utf-8"?>' .
'<AuthorizationResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' .
'<IsAuthorized>'. $this->m_isAuthorized .'</IsAuthorized>' .
'<Message><![CDATA['. $this->m_message .']]></Message></AuthorizationResponse>';
}
}
$request = @file_get_contents('php://input');
$xml3 = simplexml_load_string($request);
$uuid = $xml3->ID;
$firstname = $xml3->FirstName;
$lastname = $xml3->SurName;
$nametest = substr($lastname,0,1);
$avatarname = $firstname.$lastname;
if($avatarname=='') {
if($xml3) {
$uuid = $xml3->ID;
$firstname = $xml3->FirstName;
$lastname = $xml3->SurName;
$nametest = substr($lastname,0,1);
$avatarname = $firstname.$lastname;
} else {
header('Status: 204');
exit();
}
$query = "SELECT * FROM $tablename WHERE avatarname LIKE '" . $avatarname . "'";
$query = "SELECT * FROM $tablename WHERE uuid='$uuid' AND confirmtime IS NOT NULL";
$data = mysqli_query($dbc, $query);
if(($nametest == "@") && (mysqli_num_rows($data)>0)) {
$row = mysqli_fetch_array($data);
if($row['uuid']=='') {
$query2 = "UPDATE $tablename SET uuid='$uuid' WHERE id=" . $row['id'];
$data2 = mysqli_query($dbc, $query2);
if(mysqli_num_rows($data)>0) {
if($avatarname=='') {
echo '<?xml version="1.0" encoding="utf-8"?>' .
'<AuthorizationResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' .
'<IsAuthorized>true</IsAuthorized>' .
'<Message><![CDATA[Authorized]]></Message></AuthorizationResponse>';
mysqli_close($dbc);
exit();
} else if($nametest == "@") {
$row = mysqli_fetch_array($data);
if($row['avatarname']=='') {
$query2 = "UPDATE $tablename SET avatarname='$avatarname' WHERE id=" . $row['id'];
$data2 = mysqli_query($dbc, $query2);
}
}
$authResp = new AuthorizationResponse("true","Authorized");
echo $authResp->toXML();
echo '<?xml version="1.0" encoding="utf-8"?>' .
'<AuthorizationResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' .
'<IsAuthorized>true</IsAuthorized>' .
'<Message><![CDATA[Authorized]]></Message></AuthorizationResponse>';
mysqli_close($dbc);
exit();
}
} else { // uuid not found
if(($nametest == "@") && (mysqli_num_rows($data)==0)) {
$token = substr(hash("sha1", $avatarname, false),0,7);
$token = substr(hash("sha1", $uuid, false),0,7);
$query2 = "SELECT * FROM $tablename WHERE token='$token' AND avatarname=''";
$query2 = "SELECT * FROM $tablename WHERE uuid='$uuid' LIMIT 1";
$data2 = mysqli_query($dbc, $query2);
if(mysqli_num_rows($data2)==0) {
$query3 = "INSERT INTO $tablename (`token`) VALUES ('" . $token . "')";
$query3 = "INSERT INTO $tablename (token,uuid) VALUES ('" . $token . "','" . $uuid . "')";
$data3 = mysqli_query($dbc, $query3);
}
$getstring = base64_url_encode("fn=" . $firstname . "&ln=" . $lastname);
$getstring = base64_url_encode("t=" . $token);
$authlink2 = $authlink . "?token=" . $getstring;
$newmsg = sprintf($msgformat,$firstname,$authlink2);
$authResp = new AuthorizationResponse("false",$newmsg);
echo $authResp->toXML();
echo '<?xml version="1.0" encoding="utf-8"?>' .
'<AuthorizationResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' .
'<IsAuthorized>false</IsAuthorized>' .
'<Message><![CDATA[' . $newmsg . $authlink2 . ']]></Message></AuthorizationResponse>';
mysqli_close($dbc);
exit();
}
if($nametest != "@") {
$authResp = new AuthorizationResponse("true","Authorized");
echo $authResp->toXML();
echo '<?xml version="1.0" encoding="utf-8"?>' .
'<AuthorizationResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' .
'<IsAuthorized>true</IsAuthorized>' .
'<Message><![CDATA[Authorized]]></Message></AuthorizationResponse>';
mysqli_close($dbc);
exit();
}
if($nametest != "" ) {
$authResp = new AuthorizationResponse("false",$failmsg);
echo $authResp->toXML();
if($nametest != "" ) {
echo '<?xml version="1.0" encoding="utf-8"?>' .
'<AuthorizationResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' .
'<IsAuthorized>false</IsAuthorized>' .
'<Message><![CDATA[' . $failmsg . ']]></Message></AuthorizationResponse>';
mysqli_close($dbc);
exit();
}
+1 -1
View File
@@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS `hgauth` (
`confirmtime` datetime,
PRIMARY KEY (`id`),
UNIQUE KEY uuid (`uuid`),
UNIQUE KEY avatarname (`avatarname`)
KEY avatarname (`avatarname`)
) DEFAULT CHARSET=utf8;
COMMIT;
+55 -49
View File
@@ -1,4 +1,5 @@
<?php
// HGAuth version 1.0.4
// Github: https://github.com/cuga-rajal/hgauth
//
// This file is expected to be used to present an authorization from on a web browser
@@ -8,7 +9,7 @@
$tokenerror = "You have accessed this page differently than intended. Please use the link provided from your Opensim Viewer.";
$alreadyreg = "It appears that your avatar is already authorized with the system. You may now HG teleport.";
$regmsg = "To accept the Terms of Service and GDPR at our grid, please complete the following form.";
$regmsg = "<strong>Welcome</strong><br /><br />To accept the Terms of Service and GDPR at our grid, please complete the following form.";
$confirmyes = "Thank you for authorizing your avatar. You may now teleport to reach the target region.";
$confirmno = "We are sorry to see you go.";
$ajaxerror = "An error occurred. Please make sure nothing has been altered from the original link.";
@@ -19,19 +20,31 @@ include 'authconfig.php';
$dbc = mysqli_connect($db_server, $db_user, $db_pass, $db_name);
// Handle AJAX connection
if((isset($_REQUEST["CONFIRMAUTH"])) && (isset($_REQUEST["avatarname"]))) {
if((isset($_REQUEST["CONFIRMAUTH"])) && (isset($_REQUEST["token"]))) {
if((! isset($_SESSION['id'])) || ($_SESSION['id'] != session_id())) { // If they are not sending the session cookie
header('Status: 204'); // terminate silently
} else if( $_REQUEST["CONFIRMAUTH"] == "YES") {
$avatarname = mysqli_real_escape_string($dbc,$_REQUEST['avatarname']);
$expected = substr(hash("sha1", $avatarname, false),0,7);
$confirmtime = date("Y/m/d H:i:s",time());
$query = "UPDATE $tablename SET avatarname='$avatarname', confirmtime='$confirmtime' WHERE token='$expected'";
if($data = mysqli_query($dbc, $query)) { $xml = "<status>1</status>"; } // If query successful report success
else { $xml = "<status>0</status><msg>malformed query error</msg>"; } // If malformed query, report it as a problem
if(mysqli_affected_rows($dbc)==0) { $xml = "<status>0</status><msg>no rows were updated</msg>"; } // But if no rows were updated, report it as a problem
header('Content-type: text/xml');
echo "<data>\n$xml</data>\n";
$token = mysqli_real_escape_string($dbc,$_REQUEST['token']);
$query = "SELECT * FROM $tablename WHERE token='$token'";
$data = mysqli_query($dbc, $query);
if(mysqli_num_rows($data)==0) {
header('Status: 204');
exit();
} else {
$row = mysqli_fetch_array($data);
if($row['confirmtime'] == NULL) {
$confirmtime = date("Y/m/d H:i:s",time());
$query = "UPDATE $tablename SET confirmtime='$confirmtime' WHERE token='$token'";
if($data = mysqli_query($dbc, $query)) { $xml = "<status>1</status>"; } // If query successful report success
else { $xml = "<status>0</status><msg>malformed query error</msg>"; } // If malformed query, report it as a problem
if(mysqli_affected_rows($dbc)==0) { $xml = "<status>0</status><msg>no rows were updated</msg>"; } // But if no rows were updated, report it as a problem
} else {
$xml = "<status>1</status>";
}
header('Content-type: text/xml');
echo "<data>\n$xml</data>\n";
}
}
exit(0);
}
@@ -50,36 +63,35 @@ $t = base64_url_decode($_REQUEST['token']);
// $t is the decoded query string with a list of name/value pairs
// Check that $t has the expected name/value pairs
if((strpos($t, "@")===false) || (strpos($t, "fn=")===false) || (strpos($t, "ln=")===false)) {
if((strpos($t, "t=")===false) || (strlen($t)!=9)) {
$message = $tokenerror;
$skip = TRUE;
goto postcheck;
}
// extract and sanitize the name/value pairs
list($sf, $sl) = explode("&", base64_url_decode($_REQUEST['token']));
$firstname = mysqli_real_escape_string($dbc,explode("=", $sf)[1]);
$lastname = mysqli_real_escape_string($dbc,explode("=", $sl)[1]);
$avatarname = $firstname."".$lastname;
$nametest = substr($lastname,0,1);
$expected = substr(hash("sha1", $avatarname, false),0,7);
$token = mysqli_real_escape_string($dbc, substr(base64_url_decode($_REQUEST['token']), 2));
// It's possible for someone to alter the query string and still have readable name/value pairs.
// Check that avatar name and stored hash match to make sure there they didn't alter the query string
$query = "SELECT * FROM $tablename WHERE token='$expected'";
// It's possible for someone to alter the query string. Check that the token exists in the database
$query = "SELECT * FROM $tablename WHERE token='$token'";
$data = mysqli_query($dbc, $query);
if(mysqli_num_rows($data)==0) {
$message = $tokenerror;
$skip = TRUE;
goto postcheck;
} else {
$row = mysqli_fetch_array($data);
$uuid = $row['uuid'];
}
// Now that we know the sent avatar name hasn't been altered, check if it's already registered
$query = "SELECT * FROM $tablename WHERE avatarname LIKE '$avatarname'";
$query = "SELECT * FROM $tablename WHERE token='$token' AND confirmtime IS NOT NULL";
$data = mysqli_query($dbc, $query);
if(mysqli_num_rows($data)>0) { // Avatar name exists, they already registered
$message = $avatarname . ", " . $alreadyreg;
if(mysqli_num_rows($data)>0) { // already registered
$row = mysqli_fetch_array($data);
if($row['avatarname'] != '') { $message = $row['avatarname'] . ", " . $alreadyreg; }
else { $message = $alreadyreg; }
$skip = TRUE;
} else { // Avatar name doesn't exist, proceed with registration
$message = $regmsg;
@@ -92,7 +104,8 @@ $_SESSION['id'] = session_id();
// Adjust the following HTML text as needed
?>
<html>
<!doctype html>
<html lang="en">
<head>
<title>Authorize HG Avatar</title>
@@ -102,60 +115,53 @@ $_SESSION['id'] = session_id();
<div id="content">
<h2>Authorize HG Avatar</h2>
<hr/>
<div id="main">
<?php echo $message;
//if(! $skip) { ?>
if(! $skip) { ?>
<br /><br />
<strong><?php echo $avatarname; ?></strong>
<br /><br />
Our grid requires all people entering the grid to agree to the <a href="http://mydomain/TOS.html" target="_blank">Terms of Service</a> and to be at least 18 years of age.
First paragrapgh explains why this web form is being presented.
<br />
<br />
If you are a member of the European Union, <a href="https://gdpr-info.eu/" taget="_blank">GDP Regulations</a>
require you to give us permission to store and use your data, including:
avatar first and last name, avatar UUID, and your IP address, for you and the avatars you may interact with.
<br >
Other activity in Opensim, such as Friendships,
Friendship Requests, Instant Messages, Profiles, and inventory exchanges, may also expose this information,
regardless if you actually travel to a foreign grid.
<br >
Your data will only be used for the purpose of your visits here and your interactions with other users. We will not share it with any 3rd party
unless required to do so by law.
Second paragraph describes GDPR-related data being collected.
<br />
<br />
Accepting the agreement below indicates acceptance of the Terms of Service and, if applicable, the GDPR.
Third paragraph describes usage of collected data (or if none, state so.)
<br />
<br />
4th paragraph explains call-to-action options, clicking Yes or No buttons below. Example:
<br />
<br />
Clicking the Yes button below indicates acceptance of the Terms of Service and, if applicable, the GDPR.
<br />
<div id="formdata">
<form action="" method="post">
<label>Avatar Name : </label><?php echo $row['avatarname']; ?>
<input type="hidden" name="token" id="token" value="<?php echo $arr['token']; ?>">
<input type="hidden" name="token" id="token" value="<?php echo $token; ?>">
<ul>
<li>I agree to the Terms of Service</li>
<li>I confirm I am 18 years of age or older</li>
<li>If I live in the EU, I give you my permission to collect and use my data as indicated above</li>
</ul>
<br />
<input id="b1" type="button" value="YES" name="CONFIRMAUTH" style="font-size:140%" onclick="sendAjax()" />&nbsp;&nbsp;&nbsp;&nbsp;
<input id="b2" type="button" value="NO" name="CONFIRMAUTH" style="font-size:140%" onclick="rejected()" />
</form>
</div>
<?php //<?php } ?>
<?php } ?>
</div>
</div> <!-- the last </div> on the page -->
<script type="text/javascript">
function sendAjax() {
$('#b1, #b2').attr('disabled', true);
req = new XMLHttpRequest();
req.open("GET", "<?php echo $_SERVER['PHP_SELF']; ?>?avatarname=<?php echo $avatarname; ?>&CONFIRMAUTH=YES", true);
req.open("GET", "<?php echo $_SERVER['PHP_SELF']; ?>?token=<?php echo $token; ?>&CONFIRMAUTH=YES", true);
req.onreadystatechange = function() {
if ((req.readyState == 4) && (req.status == 200)) {
xml = req.responseText;
if($(xml).find("status").text()=='1') {
$('#formdata').html("<strong><?php echo $avatarname; ?></strong><br /><?php echo $confirmyes; ?>");
$('#main').html("<strong><?php echo $confirmyes; ?></strong>");
} else if($(xml).find("status").text()=='0') {
$('#formdata').html("<?php echo $ajaxerror; ?>");
}
@@ -165,7 +171,7 @@ function sendAjax() {
}
function rejected() {
$('#formdata').html("<strong><?php echo $avatarname; ?></strong><br /><?php echo $confirmno; ?>");
$('#formdata').html("<strong><?php echo $confirmno; ?></strong>");
}
</script>