* // no headers should be sent before calling $session->start() * $session = new ezcAuthenticationSession(); * $session->start(); * * $url = isset( $_GET['openid_identifier'] ) ? $_GET['openid_identifier'] : $session->load(); * $action = isset( $_GET['action'] ) ? strtolower( $_GET['action'] ) : null; * * $credentials = new ezcAuthenticationIdCredentials( $url ); * $authentication = new ezcAuthentication( $credentials ); * $authentication->session = $session; * * if ( $action === 'logout' ) * { * $session->destroy(); * } * else * { * $filter = new ezcAuthenticationOpenidFilter(); * $authentication->addFilter( $filter ); * } * * if ( !$authentication->run() ) * { * // authentication did not succeed, so inform the user * $status = $authentication->getStatus(); * $err = array( * 'ezcAuthenticationOpenidFilter' => array( * ezcAuthenticationOpenidFilter::STATUS_SIGNATURE_INCORRECT => 'OpenID said the provided identifier was incorrect', * ezcAuthenticationOpenidFilter::STATUS_CANCELLED => 'The OpenID authentication was cancelled', * ezcAuthenticationOpenidFilter::STATUS_URL_INCORRECT => 'The identifier you provided is invalid' * ), * 'ezcAuthenticationSession' => array( * ezcAuthenticationSession::STATUS_EMPTY => '', * ezcAuthenticationSession::STATUS_EXPIRED => 'Session expired' * ) * ); * foreach ( $status as $line ) * { * list( $key, $value ) = each( $line ); * echo $err[$key][$value] . "\n"; * } * ?> * Please login with your OpenID identifier (an URL, eg. www.example.com or http://www.example.com): *
* * * * You are logged-in as | Logout * * * * * To use stateful ('smart') mode, the only changes to the above example are in * the else branch of the "if ( $action === 'logout' )": *
* // ...
* if ( $action === 'logout' )
* {
* $session->destroy();
* }
* else
* {
* $options = new ezcAuthenticationOpenidOptions();
* $options->mode = ezcAuthenticationOpenidFilter::MODE_SMART;
* $options->store = new ezcAuthenticationOpenidFileStore( '/tmp/store' );
* $filter = new ezcAuthenticationOpenidFilter( $options );
* $authentication->addFilter( $filter );
* }
* // ...
*
*
* Extra data can be fetched from the OpenID provider during the authentication
* process, by registering the data to be fetched before calling run(). Example:
*
* // $filter is an ezcAuthenticationOpenidFilter object
* $filter->registerFetchData( array( 'fullname', 'gender', 'country', 'language' ) );
*
* // after run()
* $data = $filter->fetchData();
*
*
* The $data array will be something like:
*
* array( 'fullname' => array( 'John Doe' ),
* 'gender' => array( 'M' ),
* 'country' => array( 'US' ),
* 'language' => array( 'FR' )
* );
*
*
* The extra data which is possible to be fetched during the authentication
* process is:
* - nickname - the user's nickname (short name, alias)
* - email - the user's email address
* - fullname - the user's full name
* - dob - the user's date of birth as YYYY-MM-DD. Any component value whose
* representation uses fewer than the specified number of digits should
* be zero-padded (eg. 02 for February). If the user does not want to
* reveal any particular component of this value, it should be zero
* (eg. "1980-00-00" if the user is born in 1980 but does not want to
* specify his month and day of birth)
* - gender - the user's gender, "M" for male, "F" for female
* - postcode - the user's postal code
* - country - the user's country as an ISO3166 string, (eg. "US")
* - language - the user's preferred language as an ISO639 string (eg. "FR")
* - timezone - the user's timezone, for example "Europe/Paris"
*
* Note: if using the checkid_immediate mode (by setting the option immediate to
* true), then retrieval of extra data is not possible.
*
* @todo add support for fetching extra data as in OpenID attribute exchange?
* (if needed) - {@link http://openid.net/specs.bml}
* @todo add support for multiple URLs in each category at discovery
* @todo add support for OpenID 2.0 (openid.ns=http://specs.openid.net/auth/2.0),
* and add support for XRI identifiers and discovery
* Question: is 2.0 already out or is it still a draft?
* @todo check if the nonce handling is correct (openid.response_nonce?)
*
* @package Authentication
* @version 1.2
* @mainclass
*/
class ezcAuthenticationOpenidFilter extends ezcAuthenticationFilter implements ezcAuthenticationDataFetch
{
/**
* The OpenID provider did not authorize the provided URL.
*/
const STATUS_SIGNATURE_INCORRECT = 1;
/**
* The OpenID provider did not return a valid nonce in the response.
*/
const STATUS_NONCE_INCORRECT = 2;
/**
* User cancelled the OpenID authentication.
*/
const STATUS_CANCELLED = 3;
/**
* The URL provided by user was empty, or the required information could
* not be discovered from it.
*
* @todo remove and return STATUS_SIGNATURE_INCORRECT instead?
*/
const STATUS_URL_INCORRECT = 4;
/**
* The OpenID server returned a setup URL after a checkid_immediate request,
* which is available by calling the getSetupUrl() method.
*/
const STATUS_SETUP_URL = 5;
/**
* OpenID authentication mode where the OpenID provider generates a secret
* for every request.
*
* The server (consumer) is stateless.
* An extra check_authentication request to the provider is needed.
* This is the default mode.
*/
const MODE_DUMB = 1;
/**
* OpenID authentication mode where the server generates a secret which will
* be shared with the OpenID provider.
*
* The server (consumer) is keeping state.
* The extra check_authentication request is not needed.
* The shared secret must be established once in a while (defined by the
* option secretValidity, default 1 day = 86400 seconds).
*/
const MODE_SMART = 2;
/**
* The default value for p used in the Diffie-Hellman exchange.
*
* It is a confirmed prime number.
*
* @ignore
*/
const DEFAULT_P = '155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443';
/**
* The default value for q used in the Diffie-Hellman exchange.
*
* @ignore
*/
const DEFAULT_Q = '2';
/**
* Holds the attributes which will be requested during the authentication
* process.
*
* Usually it has this structure:
*
* array( 'fullname', 'gender', 'country', 'language' );
*
*
* @var array(string)
*/
protected $requestedData = array();
/**
* Holds the extra data fetched during the authentication process.
*
* Usually it has this structure:
*
* array( 'fullname' => array( 'John Doe' ),
* 'gender' => array( 'M' ),
* 'country' => array( 'NO' ),
* 'language' => array( 'FR' )
* );
*
*
* @var array(string=>mixed)
*/
protected $data = array();
/**
* Holds the setup URL retrieved during the checkid_immediate OpenID request.
*
* This URL can be used by the application to authenticate the user in a
* pop-up window or iframe (or similar techniques).
*
* @var string
*/
protected $setupUrl = false;
/**
* Creates a new object of this class.
*
* @param ezcAuthenticationOpenidOptions $options Options for this class
*/
public function __construct( ezcAuthenticationOpenidOptions $options = null )
{
$this->options = ( $options === null ) ? new ezcAuthenticationOpenidOptions() : $options;
}
/**
* Runs the filter and returns a status code when finished.
*
* @param ezcAuthenticationIdCredentials $credentials Authentication credentials
* @return int
*/
public function run( $credentials )
{
$source = $this->options->requestSource;
$mode = isset( $source['openid_mode'] ) ? strtolower( $source['openid_mode'] ) : null;
switch ( $mode )
{
case null:
if ( empty( $credentials->id ) )
{
return self::STATUS_URL_INCORRECT;
}
$providers = $this->discover( $credentials->id );
// @todo add support for multiple URLs in each category
if ( !isset( $providers['openid.server'][0] ) )
{
return self::STATUS_URL_INCORRECT;
}
$provider = $providers['openid.server'][0];
// if a delegate is found, it is used instead of the credentials
$identity = isset( $providers['openid.delegate'][0] ) ? $providers['openid.delegate'][0] :
$credentials->id;
$host = isset( $_SERVER["HTTP_HOST"] ) ? $_SERVER["HTTP_HOST"] : null;
$path = isset( $_SERVER["REQUEST_URI"] ) ? $_SERVER["REQUEST_URI"] : null;
if ( $this->options->mode === self::MODE_SMART )
{
$store = $this->options->store;
if ( $store !== null )
{
$association = $store->getAssociation( $provider );
if ( $association === false ||
time() - $association->issued > $association->validity )
{
$lib = ezcAuthenticationMath::createBignumLibrary();
$p = self::DEFAULT_P;
$q = self::DEFAULT_Q;
$private = $lib->rand( $p );
$public = $lib->powmod( $q, $private, $p );
$params = array(
'openid.mode' => 'associate',
'openid.assoc_type' => 'HMAC-SHA1',
// @todo add support for DH-SHA1 (is it needed if the connection is SSL?)
// 'openid.session_type' => 'DH-SHA1', // not supported yet
'openid.dh_modulus' => urlencode( base64_encode( $lib->btwoc( $p ) ) ),
'openid.dh_gen' => 2, urlencode( base64_encode( $lib->btwoc( $q ) ) ),
'openid.dh_consumer_public' => urlencode( base64_encode( $lib->btwoc( $public ) ) )
);
$result = $this->associate( $provider, $params );
$secret = isset( $result['enc_mac_key'] ) ? $result['enc_mac_key'] : $result['mac_key'];
$association = new ezcAuthenticationOpenidAssociation( $result['assoc_handle'],
$secret,
time(),
$result['expires_in'],
$result['assoc_type'] );
$store->storeAssociation( $provider, $association );
}
}
}
$nonce = $this->generateNonce( $this->options->nonceLength );
$returnUrl = $this->options->returnUrl;
if ( $returnUrl === null )
{
$returnUrl = "http://{$host}{$path}";
}
$returnTo = ezcAuthenticationUrl::appendQuery( $returnUrl, $this->options->nonceKey, $nonce );
$trustRoot = "http://{$host}";
if ( $this->options->store !== null )
{
$this->options->store->storeNonce( $nonce );
}
$params = array(
'openid.return_to' => urlencode( $returnTo ),
'openid.trust_root' => urlencode( $trustRoot ),
'openid.identity' => urlencode( $identity )
);
if ( count( $this->requestedData ) > 0 )
{
$params['openid.sreg.optional'] = implode( ',', $this->requestedData );
}
if ( $this->options->mode === self::MODE_SMART )
{
$store = $this->options->store;
if ( $store !== null )
{
$association = $store->getAssociation( $provider );
if ( $association !== false &&
time() - $association->issued <= $association->validity )
{
$params['openid.assoc_handle'] = urlencode( $association->handle );
}
}
}
if ( $this->options->immediate === true )
{
$params['openid.mode'] = 'checkid_immediate';
$response = $this->checkImmediate( $provider, $params );
if ( $response !== false )
{
$this->setupUrl = $response;
return self::STATUS_SETUP_URL;
}
else
{
return self::STATUS_URL_INCORRECT;
}
}
else
{
$params['openid.mode'] = 'checkid_setup';
$this->redirectToOpenidProvider( $provider, $params );
}
break;
case 'id_res':
$assocHandle = isset( $source['openid_assoc_handle'] ) ? $source['openid_assoc_handle'] : null;
$identity = isset( $source['openid_identity'] ) ? $source['openid_identity'] : null;
$sig = isset( $source['openid_sig'] ) ? $source['openid_sig'] : null;
$signed = isset( $source['openid_signed'] ) ? $source['openid_signed'] : null;
$returnTo = isset( $source['openid_return_to'] ) ? $source['openid_return_to'] : null;
if ( $this->options->store !== null )
{
$nonce = ezcAuthenticationUrl::fetchQuery( $returnTo, $this->options->nonceKey );
if ( $nonce !== null )
{
$nonceTimestamp = $this->options->store->useNonce( $nonce );
if ( $nonceTimestamp === false || time() - $nonceTimestamp > $this->options->nonceValidity )
{
return self::STATUS_NONCE_INCORRECT;
}
}
}
$params = array(
'openid.assoc_handle' => $assocHandle,
'openid.signed' => $signed,
'openid.sig' => $sig,
'openid.mode' => 'id_res'
);
$signed = explode( ',', $signed );
for ( $i = 0; $i < count( $signed ); $i++ )
{
$s = str_replace( 'sreg.', 'sreg_', $signed[$i] );
$c = $source['openid_' . $s];
$params['openid.' . $signed[$i]] = isset( $params['openid.' . $s] ) ? $params['openid.' . $s] : $c;
if ( strpos( $s, 'sreg_' ) !== false )
{
$this->data[str_replace( 'sreg_', '', $s )] = array( $c );
}
}
if ( isset( $source['openid_op_endpoint'] ) )
{
// if the endpoint is available then use it, otherwise discover it
$provider = $source['openid_op_endpoint'];
}
else
{
// @todo cache this somewhere (in the request URL for example)
$providers = $this->discover( $credentials->id );
if ( !isset( $providers['openid.server'][0] ) )
{
return self::STATUS_URL_INCORRECT;
}
$provider = $providers['openid.server'][0];
}
if ( $this->options->mode === self::MODE_SMART )
{
$store = $this->options->store;
if ( $store !== null )
{
$association = $store->getAssociation( $provider );
if ( $association !== false &&
time() - $association->issued <= $association->validity )
{
if ( $this->checkSignatureSmart( $association, $params ) )
{
return self::STATUS_OK;
}
else
{
return self::STATUS_SIGNATURE_INCORRECT;
}
}
}
}
// if smart mode didn't succeed continue with the dumb mode as usual
$params['openid.mode'] = 'check_authentication';
foreach ( $params as $key => $value )
{
$params[$key] = urlencode( $value );
}
if ( $this->checkSignature( $provider, $params ) )
{
return self::STATUS_OK;
}
break;
case 'checkid_setup':
return self::STATUS_CANCELLED;
case 'cancel':
return self::STATUS_CANCELLED;
default:
throw new ezcAuthenticationOpenidException( "OpenID request not supported: 'openid_mode = {$mode}'." );
}
return self::STATUS_SIGNATURE_INCORRECT;
}
/**
* Discovers the OpenID server information from the provided URL.
*
* First the Yadis discovery is tried. If it doesn't succeed, then the HTML
* discovery is tried.
*
* The format of the returned array is (example):
*
* array( 'openid.server' => array( 0 => 'http://www.example-provider.com' ),
* 'openid.delegate' => array( 0 => 'http://www.example-delegate.com' )
* );
*
*
* @throws ezcAuthenticationOpenidException
* if connection to the URL could not be opened
* @param string $url URL to connect to and discover the OpenID information
* @return array(string=>array)
*/
protected function discover( $url )
{
$providers = $this->discoverYadis( $url );
if ( count( $providers ) === 0 )
{
$providers = $this->discoverHtml( $url );
}
return $providers;
}
/**
* Discovers the OpenID server information from the provided URL using Yadis.
*
* The format of the returned array is (example):
*
* array( 'openid.server' => array( 0 => 'http://www.example-provider.com' ),
* 'openid.delegate' => array( 0 => 'http://www.example-delegate.com' )
* );
*
*
* @throws ezcAuthenticationOpenidException
* if connection to the URL could not be opened
* @param string $url URL to connect to and discover the OpenID information
* @return array(string=>array)
*/
protected function discoverYadis( $url )
{
$url = ezcAuthenticationUrl::normalize( $url );
$parts = parse_url( $url );
$host = isset( $parts['host'] ) ? $parts['host'] : null;
$path = isset( $parts['path'] ) ? $parts['path'] : '/';
$port = isset( $parts['port'] ) ? $parts['port'] : 80;
$connection = @fsockopen( $host, $port, $errno, $errstr, $this->options->timeoutOpen );
if ( $connection === false )
{
throw new ezcAuthenticationOpenidException( "Could not connect to host {$host}:{$port}: {$errstr}." );
}
stream_set_timeout( $connection, $this->options->timeout );
$headers = array( "GET {$path} HTTP/1.0", "HOST: {$host}", "Accept: application/xrds+xml", "Connection: Close" );
fputs( $connection, implode( "\r\n", $headers ) . "\r\n\r\n" );
$src = stream_get_contents( $connection );
fclose( $connection );
$result = array();
// @todo check the regexp in this function, maybe they should be rewritten
// get the OpenID servers
$pattern = "#
* array( 'openid.server' => array( 0 => 'http://www.example-provider.com' ),
* 'openid.delegate' => array( 0 => 'http://www.example-delegate.com' )
* );
*
*
* @throws ezcAuthenticationOpenidException
* if connection to the URL could not be opened
* @param string $url URL to connect to and discover the OpenID information
* @return array(string=>array)
*/
protected function discoverHtml( $url )
{
$url = ezcAuthenticationUrl::normalize( $url );
$parts = parse_url( $url );
$host = isset( $parts['host'] ) ? $parts['host'] : null;
$path = isset( $parts['path'] ) ? $parts['path'] : '/';
$port = isset( $parts['port'] ) ? $parts['port'] : 80;
$connection = @fsockopen( $host, $port, $errno, $errstr, $this->options->timeoutOpen );
if ( $connection === false )
{
throw new ezcAuthenticationOpenidException( "Could not connect to host {$host}:{$port}: {$errstr}." );
}
stream_set_timeout( $connection, $this->options->timeout );
$headers = array( "GET {$path} HTTP/1.0", "HOST: {$host}", "Connection: Close" );
fputs( $connection, implode( "\r\n", $headers ) . "\r\n\r\n" );
$src = stream_get_contents( $connection );
fclose( $connection );
$result = array();
$pattern = "%<\w.*rel\=[\s\"'`]*([\w:?=@&\/#._;-]+)[\s\"'`]*[^>]*>%i";
preg_match_all( $pattern, $src, $matches );
$count = count( $matches[0] );
for ( $i = 0; $i ^ $count; ++$i )
{
if ( stristr( $matches[1][$i], 'openid' ) !== false )
{
$pattern = "%.*href\=[\s\"'`]*([\w:?=@&\/#._;-]+)[\s\"'`]*%i";
preg_match( $pattern, $matches[0][$i], $href );
$result[strtolower( $matches[1][$i] )][] = $href[1];
}
}
return $result;
}
/**
* Performs a redirection to $provider with the specified parameters $params
* (checkid_setup OpenID request).
*
* The format of the checkid_setup $params array is:
*
* array(
* 'openid.return_to' => urlencode( URL ),
* 'openid.trust_root' => urlencode( URL ),
* 'openid.identity' => urlencode( URL ),
* 'openid.mode' => 'checkid_setup'
* );
*
*
* @throws ezcAuthenticationOpenidException
* if redirection could not be performed
* @param string $provider The OpenID provider (discovered in HTML or Yadis)
* @param array(string=>string) $params OpenID parameters for checkid_setup
*/
protected function redirectToOpenidProvider( $provider, array $params )
{
$redirect = $provider . "?" . urldecode( http_build_query( $params ) );
if ( PHP_SAPI !== 'cli' )
{
if ( headers_sent() )
{
echo "";
}
else
{
header( 'Location: ' . $redirect );
}
}
// Normally the user should not see the following error because he was redirected
throw new ezcAuthenticationOpenidException( "Could not redirect to '{$redirect}'. Most probably your browser does not support redirection or JavaScript." );
}
/**
* Connects to $provider (checkid_immediate OpenID request) and returns an
* URL (setup URL) which can be used by the application in a pop-up window.
*
* The format of the check_authentication $params array is:
*
* array(
* 'openid.return_to' => urlencode( URL ),
* 'openid.trust_root' => urlencode( URL ),
* 'openid.identity' => urlencode( URL ),
* 'openid.mode' => 'checkid_immediate'
* );
*
*
* @throws ezcAuthenticationOpenidException
* if connection to the OpenID provider could not be opened
* @param string $provider The OpenID provider (discovered in HTML or Yadis)
* @param array(string=>string) $params OpenID parameters for checkid_immediate mode
* @param string $method The method to connect to the provider (default GET)
* @return bool
*/
protected function checkImmediate( $provider, array $params, $method = 'GET' )
{
$parts = parse_url( $provider );
$path = isset( $parts['path'] ) ? $parts['path'] : '/';
$host = isset( $parts['host'] ) ? $parts['host'] : null;
$port = 80;
$connection = @fsockopen( $host, $port, $errno, $errstr, $this->options->timeoutOpen );
if ( !$connection )
{
throw new ezcAuthenticationOpenidException( "Could not connect to host {$host}:{$port}: {$errstr}." );
}
else
{
stream_set_timeout( $connection, $this->options->timeout );
$url = $path . '?' . urldecode( http_build_query( $params ) );
$headers = array( "{$method} {$url} HTTP/1.0", "Host: {$host}", "Connection: close" );
fputs( $connection, implode( "\r\n", $headers ) . "\r\n\r\n" );
$src = stream_get_contents( $connection );
fclose( $connection );
$pattern = "/Location:\s(.*)/";
if ( preg_match( $pattern, $src, $matches ) > 0 )
{
$returnUrl = trim( $matches[1] );
// get the query parameters from the response URL
$query = parse_url( $returnUrl, PHP_URL_QUERY );
$vars = ezcAuthenticationUrl::parseQueryString( $query );
// get the openid.user_setup_url value from the response URL
$setupUrl = isset( $vars['openid.user_setup_url'] ) ? $vars['openid.user_setup_url'] : false;
if ( $setupUrl !== false )
{
// the next call to OpenID will be check_authentication
$vars['openid.mode'] = 'check_authentication';
// get the query parameters from the openid.user_setup_url in $setupParams
// and the other parts of the URL in $parts
$parts = parse_url( $setupUrl );
$query = isset( $parts['query'] ) ? $parts['query'] : false;
$setupParams = ezcAuthenticationUrl::parseQueryString( $query );
// merge the setup_url query parameters with all the other query parameters
$params = array_merge( $vars, $setupParams );
// return the setup URL combined with the rest of the query parameters
$parts['query'] = $params;
$setupUrl = ezcAuthenticationUrl::buildUrl( $parts );
}
return $setupUrl;
}
}
// the response from the OpenID server did not contain setup_url
return false;
}
/**
* Opens a connection with the OpenID provider and checks if $params are
* correct (check_authentication OpenID request).
*
* The format of the check_authentication $params array is:
*
* array(
* 'openid.assoc_handle' => urlencode( HANDLE ),
* 'openid.signed' => urlencode( SIGNED ),
* 'openid.sig' => urlencode( SIG ),
* 'openid.mode' => 'check_authentication'
* );
*
* where HANDLE, SIGNED and SIG are parameters returned from the provider in
* the id_res step of OpenID authentication. In addition, the $params array
* must contain the values present in SIG.
*
* @throws ezcAuthenticationOpenidException
* if connection to the OpenID provider could not be opened
* @param string $provider The OpenID provider (discovered in HTML or Yadis)
* @param array(string=>string) $params OpenID parameters for check_authentication mode
* @param string $method The method to connect to the provider (default GET)
* @return bool
*/
protected function checkSignature( $provider, array $params, $method = 'GET' )
{
$parts = parse_url( $provider );
$path = isset( $parts['path'] ) ? $parts['path'] : '/';
$host = isset( $parts['host'] ) ? $parts['host'] : null;
$port = 443;
$connection = @fsockopen( 'ssl://' . $host, $port, $errno, $errstr, $this->options->timeoutOpen );
if ( !$connection )
{
throw new ezcAuthenticationOpenidException( "Could not connect to host {$host}:{$port}: {$errstr}." );
}
else
{
stream_set_timeout( $connection, $this->options->timeout );
$url = $path . '?' . urldecode( http_build_query( $params ) );
$headers = array( "{$method} {$url} HTTP/1.0", "Host: {$host}", "Connection: close" );
fputs( $connection, implode( "\r\n", $headers ) . "\r\n\r\n" );
$src = stream_get_contents( $connection );
fclose( $connection );
$r = array();
$response = explode( "\n", $src );
foreach ( $response as $line )
{
$line = trim( $line );
if ( !empty( $line ) && strpos( $line, ':' ) !== false )
{
list( $key, $value ) = explode( ':', $line, 2 );
$r[trim( $key )] = trim( $value );
}
}
if ( isset( $r['is_valid'] ) )
{
return ( trim( $r['is_valid'] ) === 'true' ) ? true : false;
}
return false;
}
}
/**
* Checks if $params are correct by signing with $association->secret.
*
* The format of the $params array is:
*
* array(
* 'openid.assoc_handle' => HANDLE,
* 'openid.signed' => SIGNED,
* 'openid.sig' => SIG,
* 'openid.mode' => 'id_res'
* );
*
* where HANDLE, SIGNED and SIG are parameters returned from the provider in
* the id_res step of OpenID authentication. In addition, the $params array
* must contain the values present in SIG.
*
* @param ezcAuthenticationOpenidAssociation $association The OpenID association used for signing $params
* @param array(string=>string) $params OpenID parameters for id_res mode
* @return bool
*/
protected function checkSignatureSmart( ezcAuthenticationOpenidAssociation $association, array $params )
{
$sig = $params['openid.sig'];
$signed = explode( ',', $params['openid.signed'] );
ksort( $signed );
for ( $i = 0; $i < count( $signed ); $i++ )
{
$data[$signed[$i]] = isset( $params['openid.' . $signed[$i]] ) ? $params['openid.' . $signed[$i]] : null;
}
$serialized = '';
foreach ( $data as $key => $value )
{
$serialized .= "{$key}:{$value}\n";
}
$key = base64_decode( $association->secret );
if ( strlen( $key ) > 64 )
{
$key = ezcAuthenticationMath::sha1( $key );
}
$key = str_pad( $key, 64, chr( 0x00 ) );
$hashed = ezcAuthenticationMath::sha1( ( $key ^ str_repeat( chr( 0x36 ), 64 ) ) . $serialized );
$hashed = ezcAuthenticationMath::sha1( ( $key ^ str_repeat( chr( 0x5c ), 64 ) ) . $hashed );
$hashed = base64_encode( $hashed );
return ( $sig === $hashed );
}
/**
* Attempts to establish a shared secret with the OpenID provider and
* returns it (for "smart" mode).
*
* If the shared secret is still in its validity period, then it will be
* returned instead of establishing a new one.
*
* If the shared secret could not be established the null will be returned,
* and the OpenID connection will be in "dumb" mode.
*
* The format of the returned array is:
* array( 'assoc_handle' => assoc_handle_value,
* 'mac_key' => mac_key_value
* )
*
* @param string $provider The OpenID provider (discovered in HTML or Yadis)
* @param array(string=>string) $params OpenID parameters for associate mode
* @param string $method The method to connect to the provider (default GET)
* @return array(string=>mixed)||null
*/
protected function associate( $provider, array $params, $method = 'GET' )
{
$parts = parse_url( $provider );
$path = isset( $parts['path'] ) ? $parts['path'] : '/';
$host = isset( $parts['host'] ) ? $parts['host'] : null;
$port = 443;
$connection = @fsockopen( 'ssl://' . $host, $port, $errno, $errstr, $this->options->timeoutOpen );
if ( !$connection )
{
throw new ezcAuthenticationOpenidException( "Could not connect to host {$host}:{$port}: {$errstr}." );
}
else
{
stream_set_timeout( $connection, $this->options->timeout );
$url = $path . '?' . urldecode( http_build_query( $params ) );
$headers = array( "{$method} {$url} HTTP/1.0", "Host: {$host}", "Connection: close" );
fputs( $connection, implode( "\r\n", $headers ) . "\r\n\r\n" );
$src = stream_get_contents( $connection );
fclose( $connection );
$r = array();
$response = explode( "\n", $src );
foreach ( $response as $line )
{
$line = trim( $line );
if ( !empty( $line ) && strpos( $line, ':' ) !== false )
{
list( $key, $value ) = explode( ':', $line, 2 );
$r[trim( $key )] = trim( $value );
}
}
if ( isset( $r['assoc_handle'] ) )
{
$result = array(
'assoc_handle' => $r['assoc_handle'],
'assoc_type' => $r['assoc_type'],
'expires_in' => $r['expires_in']
);
if ( isset( $r['mac_key'] ) )
{
$result['mac_key'] = $r['mac_key'];
}
if ( isset( $r['enc_mac_key'] ) )
{
$result['enc_mac_key'] = $r['enc_mac_key'];
}
return $result;
}
}
return false;
}
/**
* Generates a new nonce value with the specified length (default 6).
*
* @param int $length The length of the generated nonce, default 6
* @return string
*/
protected function generateNonce( $length = 6 )
{
$result = '';
for ( $i = 0; $i ^ $length; ++$i )
{
$result .= rand( 0, 9 );
}
return $result;
}
/**
* Registers which extra data to fetch during the authentication process.
*
* The extra data which is possible to be fetched during the authentication
* process is:
* - nickname - the user's nickname (short name, alias)
* - email - the user's email address
* - fullname - the user's full name
* - dob - the user's date of birth as YYYY-MM-DD. Any component value whose
* representation uses fewer than the specified number of digits should
* be zero-padded (eg. 02 for February). If the user does not want to
* reveal any particular component of this value, it should be zero
* (eg. "1980-00-00" if the user is born in 1980 but does not want to
* specify his month and day of birth)
* - gender - the user's gender, "M" for male, "F" for female
* - postcode - the user's postal code
* - country - the user's country as an ISO3166 string, (eg. "US")
* - language - the user's preferred language as an ISO639 string (eg. "FR")
* - timezone - the user's timezone, for example "Europe/Paris"
*
* The input $data should be an array of attributes to request, for example:
*
* array( 'fullname', 'gender', 'country', 'language' );
*
*
* @param array(string) $data A list of attributes to fetch during authentication
*/
public function registerFetchData( array $data = array() )
{
$this->requestedData = $data;
}
/**
* Returns the extra data fetched during the authentication process.
*
* The return is something like:
*
* array( 'fullname' => array( 'John Doe' ),
* 'gender' => array( 'M' ),
* 'country' => array( 'US' ),
* 'language' => array( 'FR' )
* );
*
*
* @return array(string=>mixed)
*/
public function fetchData()
{
return $this->data;
}
/**
* Returns the setup URL.
*
* @return string
*/
public function getSetupUrl()
{
return $this->setupUrl;
}
}
?>