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. +
- - +
  • I agree to the Terms of Service
  • I confirm I am 18 years of age or older
  • If I live in the EU, I give you my permission to collect and use my data as indicated above
+
    
- - + +