diff --git a/README.md b/README.md
index 3a6ed3f..cf34c59 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/authconfig.php b/authconfig.php
index 3a38d04..80a7a1d 100644
--- a/authconfig.php
+++ b/authconfig.php
@@ -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) {
diff --git a/hgauth.php b/hgauth.php
index 82a77c7..c63ecd2 100644
--- a/hgauth.php
+++ b/hgauth.php
@@ -1,91 +1,90 @@
m_isAuthorized = $isAuthorized;
- $this->m_message = $message;
- }
-
- public function toXML() {
- return '' .
- '' .
- ''. $this->m_isAuthorized .'' .
- 'm_message .']]>';
- }
-}
$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 '' .
+ '' .
+ 'true' .
+ '';
+ 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 '' .
+ '' .
+ 'true' .
+ '';
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 '' .
+ '' .
+ 'false' .
+ '';
mysqli_close($dbc);
exit();
}
if($nametest != "@") {
- $authResp = new AuthorizationResponse("true","Authorized");
- echo $authResp->toXML();
+ echo '' .
+ '' .
+ 'true' .
+ '';
mysqli_close($dbc);
exit();
}
-if($nametest != "" ) {
- $authResp = new AuthorizationResponse("false",$failmsg);
- echo $authResp->toXML();
+if($nametest != "" ) {
+ echo '' .
+ '' .
+ 'false' .
+ '';
mysqli_close($dbc);
exit();
}
diff --git a/hgauth.sql b/hgauth.sql
index ca3eec5..1b9b102 100644
--- a/hgauth.sql
+++ b/hgauth.sql
@@ -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;
diff --git a/index.php b/index.php
index 21d2ee9..1a0c717 100644
--- a/index.php
+++ b/index.php
@@ -1,4 +1,5 @@
Welcome
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 = "1"; } // If query successful report success
- else { $xml = "0malformed query error"; } // If malformed query, report it as a problem
- if(mysqli_affected_rows($dbc)==0) { $xml = "0no rows were updated"; } // But if no rows were updated, report it as a problem
- header('Content-type: text/xml');
- echo "\n$xml\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 = "1"; } // If query successful report success
+ else { $xml = "0malformed query error"; } // If malformed query, report it as a problem
+ if(mysqli_affected_rows($dbc)==0) { $xml = "0no rows were updated"; } // But if no rows were updated, report it as a problem
+ } else {
+ $xml = "1";
+ }
+
+ header('Content-type: text/xml');
+ echo "\n$xml\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
?>
-
+
+
Authorize HG Avatar
@@ -102,60 +115,53 @@ $_SESSION['id'] = session_id();
Authorize HG Avatar
-
+
+if(! $skip) { ?>
-
-
-Our grid requires all people entering the grid to agree to the
Terms of Service and to be at least 18 years of age.
+First paragrapgh explains why this web form is being presented.
-If you are a member of the European Union,
GDP Regulations
-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.
-
-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.
-
-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.
-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.)
+4th paragraph explains call-to-action options, clicking Yes or No buttons below. Example:
+
+
+Clicking the Yes button below indicates acceptance of the Terms of Service and, if applicable, the GDPR.
+
-
-
+
+