Here are the key pieces from what I used to implement this. These are copy / paste snippets from our authentication module. Note this does not CREATE the user id Moodle, it logs in a user created earlier when they enrolled. The key part of the whole thing is the PK parameter sent by the sending site. It's HMAC of the entire query string plus a pre-shared key. That validates that the query string came from your partner site. Note also that the hashed parameters include a timestamp.
function user_login($username, $password){
$extusername= core_text::convert($username, 'utf-8', $this->config->extencoding);
$extpassword= core_text::convert($password, 'utf-8', $this->config->extencoding);
$teexsso= Teexsso::getInstance();
return$teexsso->checkhmac();
}
function loginpage_hook(){
$teexsso= Teexsso::getInstance();
return$teexsso->loginpage_hook();
}
class Teexsso {
protectedstatic$instance;
public$isvalid=false;
publicstaticfunction getInstance(){
if(!isset(self::$instance)){
self::$instance=new Teexsso;
}
returnself::$instance;
}
/**
* Protected constructor to prevent creating a new instance of the
* *Singleton* via the `new` operator from outside of this class.
*/
protectedfunction__construct(){
}
function loginpage_hook(){
global$DB;
global$SESSION;
global$CFG;
global$frm;
if(empty($_GET['PK'])){
returnfalse;
}
$this->username =$this->checkhmac();
if($this->username){
if(empty($frm)||($frm)){
$frm=newstdClass;
}
$frm->username =$this->username;
$frm->password = 'x';
$this->isvalid =true;
if($_GET['C']){
$courseid=$DB->get_field('course', 'id', array('idnumber' =>$_GET['C']));
$SESSION->wantsurl =$CFG->wwwroot . '/course/view.php?id=' .$courseid;
}
}
}
function checkhmac(){
global$DB;
if($this->isvalid &&!empty($this->username)){
return$this->username;
}
if(empty($_GET['PK'])){
returnfalse;
}
// This must use $_GET rather than _param to ensure variables are from the query string, and therefore in the hmac.
// We wouldn't want a different username set via POST.
$hmactry=$_GET['PK'];
$this->username =$_GET['username'];
$time=$_GET['T'];
if((!$hmactry)||(abs($time-time())>600)){
returnfalse;
}
$key= 'RANDOM_STRING_YOU_MUST_CHANGE_THIS';
$paramstr=preg_replace('/&*PK=[A-Za-z0-9]+/', '', $_SERVER['QUERY_STRING']);
$paramstr=preg_replace('/^\?/', '', $paramstr);
$hmaccorrect=strtoupper(hash_hmac('sha256','?' .$paramstr, $key));
if($hmactry!==$hmaccorrect){
$this->isvalid =false;
returnfalse;
}
$this->isvalid =true;
if(!empty($_GET['txid'])){
$txiduser=$DB->get_field('user', 'username', array('idnumber' =>$_GET['txid']));
if($txiduser){
$this->username =$txiduser;
}
}
global$user;
$user= get_complete_user_data('username', $this->username);
return$this->username;
}
}
Be very, very careful - it's easy to get authentication wrong, and it is a target for attack. The above code, while written by a security professional, has not yet passed my own final security analysis, much less third-party review.