Add openid security
[contractdashboard.git] / lib / openid.php
blob:a/lib/openid.php -> blob:b/lib/openid.php
<?php <?php
/** /**
* This class provides a simple interface for OpenID (1.1 and 2.0) authentication. * This class provides a simple interface for OpenID (1.1 and 2.0) authentication.
* Supports Yadis discovery. * Supports Yadis discovery.
* The authentication process is stateless/dumb. * The authentication process is stateless/dumb.
* *
* Usage: * Usage:
* Sign-on with OpenID is a two step process: * Sign-on with OpenID is a two step process:
* Step one is authentication with the provider: * Step one is authentication with the provider:
* <code> * <code>
* $openid = new LightOpenID('my-host.example.org'); * $openid = new LightOpenID('my-host.example.org');
* $openid->identity = 'ID supplied by user'; * $openid->identity = 'ID supplied by user';
* header('Location: ' . $openid->authUrl()); * header('Location: ' . $openid->authUrl());
* </code> * </code>
* The provider then sends various parameters via GET, one of them is openid_mode. * The provider then sends various parameters via GET, one of them is openid_mode.
* Step two is verification: * Step two is verification:
* <code> * <code>
* if ($this->data['openid_mode']) { * if ($this->data['openid_mode']) {
* $openid = new LightOpenID('my-host.example.org'); * $openid = new LightOpenID('my-host.example.org');
* echo $openid->validate() ? 'Logged in.' : 'Failed'; * echo $openid->validate() ? 'Logged in.' : 'Failed';
* } * }
* </code> * </code>
* *
* Change the 'my-host.example.org' to your domain name. Do NOT use $_SERVER['HTTP_HOST'] * Change the 'my-host.example.org' to your domain name. Do NOT use $_SERVER['HTTP_HOST']
* for that, unless you know what you are doing. * for that, unless you know what you are doing.
* *
* Optionally, you can set $returnUrl and $realm (or $trustRoot, which is an alias). * Optionally, you can set $returnUrl and $realm (or $trustRoot, which is an alias).
* The default values for those are: * The default values for those are:
* $openid->realm = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST']; * $openid->realm = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
* $openid->returnUrl = $openid->realm . $_SERVER['REQUEST_URI']; * $openid->returnUrl = $openid->realm . $_SERVER['REQUEST_URI'];
* If you don't know their meaning, refer to any openid tutorial, or specification. Or just guess. * If you don't know their meaning, refer to any openid tutorial, or specification. Or just guess.
* *
* AX and SREG extensions are supported. * AX and SREG extensions are supported.
* To use them, specify $openid->required and/or $openid->optional before calling $openid->authUrl(). * To use them, specify $openid->required and/or $openid->optional before calling $openid->authUrl().
* These are arrays, with values being AX schema paths (the 'path' part of the URL). * These are arrays, with values being AX schema paths (the 'path' part of the URL).
* For example: * For example:
* $openid->required = array('namePerson/friendly', 'contact/email'); * $openid->required = array('namePerson/friendly', 'contact/email');
* $openid->optional = array('namePerson/first'); * $openid->optional = array('namePerson/first');
* If the server supports only SREG or OpenID 1.1, these are automaticaly * If the server supports only SREG or OpenID 1.1, these are automaticaly
* mapped to SREG names, so that user doesn't have to know anything about the server. * mapped to SREG names, so that user doesn't have to know anything about the server.
* *
* To get the values, use $openid->getAttributes(). * To get the values, use $openid->getAttributes().
* *
* *
* The library requires PHP >= 5.1.2 with curl or http/https stream wrappers enabled. * The library requires PHP >= 5.1.2 with curl or http/https stream wrappers enabled.
* @author Mewp * @author Mewp
* @copyright Copyright (c) 2010, Mewp * @copyright Copyright (c) 2010, Mewp
* @license http://www.opensource.org/licenses/mit-license.php MIT * @license http://www.opensource.org/licenses/mit-license.php MIT
*/ */
class LightOpenID class LightOpenID
{ {
public $returnUrl public $returnUrl
, $required = array() , $required = array()
, $optional = array() , $optional = array()
, $verify_peer = null , $verify_peer = null
, $capath = null , $capath = null
, $cainfo = null , $cainfo = null
, $data; , $data;
private $identity, $claimed_id; private $identity, $claimed_id;
protected $server, $version, $trustRoot, $aliases, $identifier_select = false protected $server, $version, $trustRoot, $aliases, $identifier_select = false
, $ax = false, $sreg = false, $setup_url = null; , $ax = false, $sreg = false, $setup_url = null;
static protected $ax_to_sreg = array( static protected $ax_to_sreg = array(
'namePerson/friendly' => 'nickname', 'namePerson/friendly' => 'nickname',
'contact/email' => 'email', 'contact/email' => 'email',
'namePerson' => 'fullname', 'namePerson' => 'fullname',
'birthDate' => 'dob', 'birthDate' => 'dob',
'person/gender' => 'gender', 'person/gender' => 'gender',
'contact/postalCode/home' => 'postcode', 'contact/postalCode/home' => 'postcode',
'contact/country/home' => 'country', 'contact/country/home' => 'country',
'pref/language' => 'language', 'pref/language' => 'language',
'pref/timezone' => 'timezone', 'pref/timezone' => 'timezone',
); );
   
function __construct($host) function __construct($host)
{ {
$this->trustRoot = (strpos($host, '://') ? $host : 'http://' . $host); $this->trustRoot = (strpos($host, '://') ? $host : 'http://' . $host);
if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
&& $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
) { ) {
$this->trustRoot = (strpos($host, '://') ? $host : 'https://' . $host); $this->trustRoot = (strpos($host, '://') ? $host : 'https://' . $host);
} }
   
if(($host_end = strpos($this->trustRoot, '/', 8)) !== false) { if(($host_end = @strpos($this->trustRoot, '/', 8)) !== false) {
$this->trustRoot = substr($this->trustRoot, 0, $host_end); $this->trustRoot = substr($this->trustRoot, 0, $host_end);
} }
   
$uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?'); $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?');
$this->returnUrl = $this->trustRoot . $uri; $this->returnUrl = $this->trustRoot . $uri;
   
$this->data = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET; $this->data = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET;
   
if(!function_exists('curl_init') && !in_array('https', stream_get_wrappers())) { if(!function_exists('curl_init') && !in_array('https', stream_get_wrappers())) {
throw new ErrorException('You must have either https wrappers or curl enabled.'); throw new ErrorException('You must have either https wrappers or curl enabled.');
} }
} }
   
function __set($name, $value) function __set($name, $value)
{ {
switch ($name) { switch ($name) {
case 'identity': case 'identity':
if (strlen($value = trim((String) $value))) { if (strlen($value = trim((String) $value))) {
if (preg_match('#^xri:/*#i', $value, $m)) { if (preg_match('#^xri:/*#i', $value, $m)) {
$value = substr($value, strlen($m[0])); $value = substr($value, strlen($m[0]));
} elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) { } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) {
$value = "http://$value"; $value = "http://$value";
} }
if (preg_match('#^https?://[^/]+$#i', $value, $m)) { if (preg_match('#^https?://[^/]+$#i', $value, $m)) {
$value .= '/'; $value .= '/';
} }
} }
$this->$name = $this->claimed_id = $value; $this->$name = $this->claimed_id = $value;
break; break;
case 'trustRoot': case 'trustRoot':
case 'realm': case 'realm':
$this->trustRoot = trim($value); $this->trustRoot = trim($value);
} }
} }
   
function __get($name) function __get($name)
{ {
switch ($name) { switch ($name) {
case 'identity': case 'identity':
# We return claimed_id instead of identity, # We return claimed_id instead of identity,
# because the developer should see the claimed identifier, # because the developer should see the claimed identifier,
# i.e. what he set as identity, not the op-local identifier (which is what we verify) # i.e. what he set as identity, not the op-local identifier (which is what we verify)
return $this->claimed_id; return $this->claimed_id;
case 'trustRoot': case 'trustRoot':
case 'realm': case 'realm':
return $this->trustRoot; return $this->trustRoot;
case 'mode': case 'mode':
return empty($this->data['openid_mode']) ? null : $this->data['openid_mode']; return empty($this->data['openid_mode']) ? null : $this->data['openid_mode'];
} }
} }
   
/** /**
* Checks if the server specified in the url exists. * Checks if the server specified in the url exists.
* *
* @param $url url to check * @param $url url to check
* @return true, if the server exists; false otherwise * @return true, if the server exists; false otherwise
*/ */
function hostExists($url) function hostExists($url)
{ {
if (strpos($url, '/') === false) { if (strpos($url, '/') === false) {
$server = $url; $server = $url;
} else { } else {
$server = @parse_url($url, PHP_URL_HOST); $server = @parse_url($url, PHP_URL_HOST);
} }
   
if (!$server) { if (!$server) {
return false; return false;
} }
   
return !!gethostbynamel($server); return !!gethostbynamel($server);
} }
   
protected function request_curl($url, $method='GET', $params=array()) protected function request_curl($url, $method='GET', $params=array())
{ {
$params = http_build_query($params, '', '&'); $params = http_build_query($params, '', '&');
$curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : '')); $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : ''));
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*')); curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*'));
   
if($this->verify_peer !== null) { if($this->verify_peer !== null) {
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer);
if($this->capath) { if($this->capath) {
curl_setopt($curl, CURLOPT_CAPATH, $this->capath); curl_setopt($curl, CURLOPT_CAPATH, $this->capath);
} }
   
if($this->cainfo) { if($this->cainfo) {
curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo); curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo);
} }
} }
   
if ($method == 'POST') { if ($method == 'POST') {
curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $params); curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
} elseif ($method == 'HEAD') { } elseif ($method == 'HEAD') {
curl_setopt($curl, CURLOPT_HEADER, true); curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_NOBODY, true); curl_setopt($curl, CURLOPT_NOBODY, true);
} else { } else {
curl_setopt($curl, CURLOPT_HTTPGET, true); curl_setopt($curl, CURLOPT_HTTPGET, true);
} }
$response = curl_exec($curl); $response = curl_exec($curl);
   
if($method == 'HEAD') { if($method == 'HEAD') {
$headers = array(); $headers = array();
foreach(explode("\n", $response) as $header) { foreach(explode("\n", $response) as $header) {
$pos = strpos($header,':'); $pos = strpos($header,':');
$name = strtolower(trim(substr($header, 0, $pos))); $name = strtolower(trim(substr($header, 0, $pos)));
$headers[$name] = trim(substr($header, $pos+1)); $headers[$name] = trim(substr($header, $pos+1));
} }
   
# Updating claimed_id in case of redirections. # Updating claimed_id in case of redirections.
$effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
if($effective_url != $url) { if($effective_url != $url) {
$this->identity = $this->claimed_id = $effective_url; $this->identity = $this->claimed_id = $effective_url;
} }
   
return $headers; return $headers;
} }
   
if (curl_errno($curl)) { if (curl_errno($curl)) {
throw new ErrorException(curl_error($curl), curl_errno($curl)); throw new ErrorException(curl_error($curl), curl_errno($curl));
} }
   
return $response; return $response;
} }
   
protected function request_streams($url, $method='GET', $params=array()) protected function request_streams($url, $method='GET', $params=array())
{ {
if(!$this->hostExists($url)) { if(!$this->hostExists($url)) {
throw new ErrorException("Could not connect to $url.", 404); throw new ErrorException("Could not connect to $url.", 404);
} }
   
$params = http_build_query($params, '', '&'); $params = http_build_query($params, '', '&');
switch($method) { switch($method) {
case 'GET': case 'GET':
$opts = array( $opts = array(
'http' => array( 'http' => array(
'method' => 'GET', 'method' => 'GET',
'header' => 'Accept: application/xrds+xml, */*', 'header' => 'Accept: application/xrds+xml, */*',
'ignore_errors' => true, 'ignore_errors' => true,
), 'ssl' => array( ), 'ssl' => array(
'CN_match' => parse_url($url, PHP_URL_HOST), 'CN_match' => parse_url($url, PHP_URL_HOST),
), ),
); );
$url = $url . ($params ? '?' . $params : ''); $url = $url . ($params ? '?' . $params : '');
break; break;
case 'POST': case 'POST':
$opts = array( $opts = array(
'http' => array( 'http' => array(
'method' => 'POST', 'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded', 'header' => 'Content-type: application/x-www-form-urlencoded',
'content' => $params, 'content' => $params,
'ignore_errors' => true, 'ignore_errors' => true,
), 'ssl' => array( ), 'ssl' => array(
'CN_match' => parse_url($url, PHP_URL_HOST), 'CN_match' => parse_url($url, PHP_URL_HOST),
), ),
); );
break; break;
case 'HEAD': case 'HEAD':
# We want to send a HEAD request, # We want to send a HEAD request,
# but since get_headers doesn't accept $context parameter, # but since get_headers doesn't accept $context parameter,
# we have to change the defaults. # we have to change the defaults.
$default = stream_context_get_options(stream_context_get_default()); $default = stream_context_get_options(stream_context_get_default());
stream_context_get_default( stream_context_get_default(
array( array(
'http' => array( 'http' => array(
'method' => 'HEAD', 'method' => 'HEAD',
'header' => 'Accept: application/xrds+xml, */*', 'header' => 'Accept: application/xrds+xml, */*',
'ignore_errors' => true, 'ignore_errors' => true,
), 'ssl' => array( ), 'ssl' => array(
'CN_match' => parse_url($url, PHP_URL_HOST), 'CN_match' => parse_url($url, PHP_URL_HOST),
), ),
) )
); );
   
$url = $url . ($params ? '?' . $params : ''); $url = $url . ($params ? '?' . $params : '');
$headers_tmp = get_headers ($url); $headers_tmp = get_headers ($url);
if(!$headers_tmp) { if(!$headers_tmp) {
return array(); return array();
} }
   
# Parsing headers. # Parsing headers.
$headers = array(); $headers = array();
foreach($headers_tmp as $header) { foreach($headers_tmp as $header) {
$pos = strpos($header,':'); $pos = strpos($header,':');
$name = strtolower(trim(substr($header, 0, $pos))); $name = strtolower(trim(substr($header, 0, $pos)));
$headers[$name] = trim(substr($header, $pos+1)); $headers[$name] = trim(substr($header, $pos+1));
   
# Following possible redirections. The point is just to have # Following possible redirections. The point is just to have
# claimed_id change with them, because get_headers() will # claimed_id change with them, because get_headers() will
# follow redirections automatically. # follow redirections automatically.
# We ignore redirections with relative paths. # We ignore redirections with relative paths.
# If any known provider uses them, file a bug report. # If any known provider uses them, file a bug report.
if($name == 'location') { if($name == 'location') {
if(strpos($headers[$name], 'http') === 0) { if(strpos($headers[$name], 'http') === 0) {
$this->identity = $this->claimed_id = $headers[$name]; $this->identity = $this->claimed_id = $headers[$name];
} elseif($headers[$name][0] == '/') { } elseif($headers[$name][0] == '/') {
$parsed_url = parse_url($this->claimed_id); $parsed_url = parse_url($this->claimed_id);
$this->identity = $this->identity =
$this->claimed_id = $parsed_url['scheme'] . '://' $this->claimed_id = $parsed_url['scheme'] . '://'
. $parsed_url['host'] . $parsed_url['host']
. $headers[$name]; . $headers[$name];
} }
} }
} }
   
# And restore them. # And restore them.
stream_context_get_default($default); stream_context_get_default($default);
return $headers; return $headers;
} }
   
if($this->verify_peer) { if($this->verify_peer) {
$opts['ssl'] += array( $opts['ssl'] += array(
'verify_peer' => true, 'verify_peer' => true,
'capath' => $this->capath, 'capath' => $this->capath,
'cafile' => $this->cainfo, 'cafile' => $this->cainfo,
); );
} }
   
$context = stream_context_create ($opts); $context = stream_context_create ($opts);
   
return file_get_contents($url, false, $context); return file_get_contents($url, false, $context);
} }
   
protected function request($url, $method='GET', $params=array()) protected function request($url, $method='GET', $params=array())
{ {
if (function_exists('curl_init') if (function_exists('curl_init')
&& (!in_array('https', stream_get_wrappers()) || !ini_get('safe_mode') && !ini_get('open_basedir')) && (!in_array('https', stream_get_wrappers()) || !ini_get('safe_mode') && !ini_get('open_basedir'))
) { ) {
return $this->request_curl($url, $method, $params); return $this->request_curl($url, $method, $params);
} }
return $this->request_streams($url, $method, $params); return $this->request_streams($url, $method, $params);
} }
   
protected function build_url($url, $parts) protected function build_url($url, $parts)
{ {
if (isset($url['query'], $parts['query'])) { if (isset($url['query'], $parts['query'])) {
$parts['query'] = $url['query'] . '&' . $parts['query']; $parts['query'] = $url['query'] . '&' . $parts['query'];
} }
   
$url = $parts + $url; $url = $parts + $url;
$url = $url['scheme'] . '://' $url = $url['scheme'] . '://'
. (empty($url['username'])?'' . (empty($url['username'])?''
:(empty($url['password'])? "{$url['username']}@" :(empty($url['password'])? "{$url['username']}@"