I want to authenticate to another site using HTTP Digest authorization in PHP script.
My function has as parameter just content of the WWW-Authenticate header and I want to generate correct response (Authorization header). I have found many examples that explain how to implement this the other way (browser authenticate to my script) but not this way. I am missing function that is able to parse WWW-Authenticate header content a generate response. Is there some standard function or common library that implements this?
Ok, no answer yet, I have investigated python implementation that lied around here and rewrite it to PHP. It is the simplest possible piece of code. Supports only md5 hashing, but works for me:
function H($param) {
return md5($param);
}
function KD($a,$b) {
return H("$a:$b");
}
function parseHttpDigest($digest) {
$data = array();
$parts = explode(", ", $digest);
foreach ($parts as $element) {
$bits = explode("=", $element);
$data[$bits[0]] = str_replace('"','', $bits[1]);
}
return $data;
}
function response($wwwauth, $user, $pass, $httpmethod, $uri) {
list($dummy_digest, $value) = split(' ', $wwwauth, 2);
$x = parseHttpDigest($value);
$realm = $x['realm'];
$A1 = $user.":".$realm.":".$pass;
$A2 = $httpmethod.":".$uri;
if ($x['qop'] == 'auth') {
$cnonce = time();
$ncvalue = 1;
$noncebit = $x['nonce'].":".$ncvalue.":".$cnonce.":auth:".H($A2);
$respdig = KD(H($A1), $noncebit);
}else {
# FIX: handle error here
}
$base = 'Digest username="'.$user.'", realm="';
$base .= $x['realm'].'", nonce="'.$x['nonce'].'",';
$base .= ' uri="'.$uri.'", cnonce="'.$cnonce;
$base .= '", nc="'.$ncvalue.'", response="'.$respdig.'", qop="auth"';
return $base;
}
Usage:
# TEST
$www_header = 'Digest realm="TEST", nonce="356f2dbb8ce08174009d53c6f02c401f", algorithm="MD5", qop="auth"';
print response($www_header, "user", "password", "POST", "/my_url_query");
Don't know of a ready-made client-side implementation in PHP; you have to implement the RFC as if your script were the browser, authenticating to a remote server. Wikipedia's page on HTTP Digest has a nice example.
(it's not that hard - a couple of MD5 hashes. Some gotchas I encontered when building the server-side: string delimiter is ":" (colon), request method is also a part of the hash)
Related
I'm currently attempting to generate a signature to make API calls to quickbooks online, however I keep getting authentication errors. I'm sure the signature portion is where I'm going wrong. Is this incorrect:
//method to generate signature
//$this->method = "GET"
//QBO_SANDBOX_URL = 'https://some_url.com/'
//$this->_query = 'something=something'
public function generate_signature()
{
$base = $this->_method.'&'.rawurlencode($this->_url.QBO_SANDBOX_URL.'v3/company/'.$this->_realm_id).'&'
.rawurlencode("oauth_consumer_key=".rawurlencode($this->_consumer_key).'&'
.'&oauth_nonce='.rawurlencode('34604g54654y456546')
.'&oauth_signature_method='.rawurlencode('HMAC-SHA1')
.'&oauth_timestamp='.rawurlencode(time())
.'&oauth_token='.rawurlencode($this->_auth_token)
.'&oauth_version='.rawurlencode('1.0')
.'&'.rawurlencode($this->_query));
$key = rawurlencode($this->_consumer_secret.'&'.$this->_token_secret);
$this->_signature = base64_encode(hash_hmac("sha1", $base, $key, true));
}
Now when I go to send my request, here are the headers:
$this->_headers = array(
'Authorization: '.urlencode('OAuth oauth_token="'.$this->_auth_token.'",oauth_nonce="ea9ec8429b68d6b77cd5600adbbb0456",oauth_consumer_key="'.$this->_consumer_key.'",oauth_signature_method="HMAC-SHA1", oauth_timestamp="'.time().'", oauth_version ="1.0"oauth_signature="'.$this->_signature.'"').''
);
I get a 401 Authorization response. Am I signing incorrectly?
EDIT: All fields not included here (i.e $this->_auth_token) are set.
For anyone that might use this as a basis for their own integration, there is one other issue with the code originally posted:
$key = rawurlencode($this->_consumer_secret.'&'.$this->_token_secret);
should be
$key = rawurlencode($this->_consumer_secret).'&'.rawurlencode($this->_token_secret);
This issue was in the base string:
.rawurlencode("oauth_consumer_key=".rawurlencode($this->_consumer_key).'&'
.'&oauth_nonce='.rawurlencode('34604g54654y456546')
The & after the consumer key and once again before the oauth_nonce.
In the signature creation I think it lacks the call to rawurlencode():
$this->_signature = rawurlencode(base64_encode(hash_hmac("sha1", $base, $key, true)));
instead :
$this->_signature = base64_encode(hash_hmac("sha1", $base, $key, true));
hello fellow developers,
I’m facing an issue with the load callback (and the uninstall callback by extension).
I’m trying to verify the requests authenticity following the algorithm described in the documentation. https://developer.bigcommerce.com/apps/load#signed-payload
I am able to decode the json string and the data is correct, but the signatures never match. I made sure to use the right client secret and tried out different encoding/decoding scenarios with no luck.
An other concern is with the snippet of code (PHP) they provide in example (and in their sample app). They seem to return null when the signatures match and the decoded data when they don’t… (try secureCompare())
Meaning that the security test would pass every time, since in all my attempts the signatures didn’t match.
Am I missing something here ?
Edit: Here is the example in the doc. I can't really give you sample data as the client secret is to remain secret...
function verify($signedRequest, $clientSecret)
{
list($payload, $encodedSignature) = explode('.', $signedRequest, 2);
// decode the data
$signature = base64_decode($encodedSignature);
$data = json_decode(base64_decode($payload), true);
// confirm the signature
$expectedSignature = hash_hmac('sha256', $payload, $clientSecret, $raw = true);
if (secureCompare($signature, $expectedSignature)) {
error_log('Bad Signed JSON signature!');
return null;
}
return $data;
}
function secureCompare($str1, $str2)
{
$res = $str1 ^ $str2;
$ret = strlen($str1) ^ strlen($str2); //not the same length, then fail ($ret != 0)
for($i = strlen($res) - 1; $i >= 0; $i--) {
$ret += ord($res[$i]);
}
return !$ret;
}
You're not missing anything, and it's not a clock sync issue - the 28 lines of sample code provided both here and here has some pretty critical flaws:
The sample code does a hash_hmac of the raw base64-encoded JSON, instead of the base64-decoded JSON. (The hash provided to you by the BigCommerce API is really a hash of the base64-decoded JSON).
Since hash_hmac is called with $raw=true, this means the two strings will always be vastly different: one is raw binary, and the other is hexits.
Bad check of secureCompare logic. The if (secureCompare... part of the verify function expects opposite behavior from the secureCompare function. If the secureCompare function returns true when the strings match, why are we calling error_log?
Put all three of these issues together, and you end up with code that appears to work, but is actually silently failing. If you use the sample code, you're likely allowing any and all "signed" requests to be processed by your application!
Here's my corrected implementation of the verify function:
<?php
function verifySignedRequest($signedRequest, $clientSecret)
{
list($encodedData, $encodedSignature) = explode('.', $signedRequest, 2);
// decode the data
$signature = base64_decode($encodedSignature);
$jsonStr = base64_decode($encodedData);
$data = json_decode($jsonStr, true);
// confirm the signature
$expectedSignature = hash_hmac('sha256', $jsonStr, $clientSecret, $raw = false);
if (!hash_equals($expectedSignature, $signature)) {
error_log('Bad signed request from BigCommerce!');
return null;
}
return $data;
}
I can't seem to find a real answer to this problem so here I go:
How do you parse raw HTTP request data in multipart/form-data format in PHP? I know that raw POST is automatically parsed if formatted correctly, but the data I'm referring to is coming from a PUT request, which is not being parsed automatically by PHP. The data is multipart and looks something like:
------------------------------b2449e94a11c
Content-Disposition: form-data; name="user_id"
3
------------------------------b2449e94a11c
Content-Disposition: form-data; name="post_id"
5
------------------------------b2449e94a11c
Content-Disposition: form-data; name="image"; filename="/tmp/current_file"
Content-Type: application/octet-stream
�����JFIF���������... a bunch of binary data
I'm sending the data with libcurl like so (pseudo code):
curl_setopt_array(
CURLOPT_POSTFIELDS => array(
'user_id' => 3,
'post_id' => 5,
'image' => '#/tmp/current_file'),
CURLOPT_CUSTOMREQUEST => 'PUT'
);
If I drop the CURLOPT_CUSTOMREQUEST bit, the request is handled as a POST on the server and everything is parsed just fine.
Is there a way to manually invoke PHPs HTTP data parser or some other nice way of doing this?
And yes, I have to send the request as PUT :)
Edit - please read first: this answer is still getting regular hits 7 years later. I have never used this code since then and do not know if there is a better way to do it these days. Please view the comments below and know that there are many scenarios where this code will not work. Use at your own risk.
--
Ok, so with Dave and Everts suggestions I decided to parse the raw request data manually. I didn't find any other way to do this after searching around for about a day.
I got some help from this thread. I didn't have any luck tampering with the raw data like they do in the referenced thread, as that will break the files being uploaded. So it's all regex. This wasnt't tested very well, but seems to be working for my work case. Without further ado and in the hope that this may help someone else someday:
function parse_raw_http_request(array &$a_data)
{
// read incoming data
$input = file_get_contents('php://input');
// grab multipart boundary from content type header
preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
$boundary = $matches[1];
// split content by boundary and get rid of last -- element
$a_blocks = preg_split("/-+$boundary/", $input);
array_pop($a_blocks);
// loop data blocks
foreach ($a_blocks as $id => $block)
{
if (empty($block))
continue;
// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
// parse uploaded files
if (strpos($block, 'application/octet-stream') !== FALSE)
{
// match "name", then everything after "stream" (optional) except for prepending newlines
preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $block, $matches);
}
// parse all other fields
else
{
// match "name" and optional value in between newline sequences
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
}
$a_data[$matches[1]] = $matches[2];
}
}
Usage by reference (in order not to copy around the data too much):
$a_data = array();
parse_raw_http_request($a_data);
var_dump($a_data);
I used Chris's example function and added some needed functionality, such as R Porter's need for array's of $_FILES. Hope it helps some people.
Here is the class & example usage
<?php
include_once('class.stream.php');
$data = array();
new stream($data);
$_PUT = $data['post'];
$_FILES = $data['file'];
/* Handle moving the file(s) */
if (count($_FILES) > 0) {
foreach($_FILES as $key => $value) {
if (!is_uploaded_file($value['tmp_name'])) {
/* Use getimagesize() or fileinfo() to validate file prior to moving here */
rename($value['tmp_name'], '/path/to/uploads/'.$value['name']);
} else {
move_uploaded_file($value['tmp_name'], '/path/to/uploads/'.$value['name']);
}
}
}
I would suspect the best way to go about it is 'doing it yourself', although you might find inspiration in multipart email parsers that use a similar (if not the exact same) format.
Grab the boundary from the Content-Type HTTP header, and use that to explode the various parts of the request. If the request is very large, keep in mind that you might store the entire request in memory, possibly even multiple times.
The related RFC is RFC2388, which fortunately is pretty short.
I'm surprised no one mentioned parse_str or mb_parse_str:
$result = [];
$rawPost = file_get_contents('php://input');
mb_parse_str($rawPost, $result);
var_dump($result);
http://php.net/manual/en/function.mb-parse-str.php
I haven't dealt with http headers much, but found this bit of code that might help
function http_parse_headers( $header )
{
$retVal = array();
$fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $header));
foreach( $fields as $field ) {
if( preg_match('/([^:]+): (.+)/m', $field, $match) ) {
$match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
if( isset($retVal[$match[1]]) ) {
$retVal[$match[1]] = array($retVal[$match[1]], $match[2]);
} else {
$retVal[$match[1]] = trim($match[2]);
}
}
}
return $retVal;
}
From http://php.net/manual/en/function.http-parse-headers.php
Here is a universal solution working with arbitrary multipart/form-data content and tested for POST, PUT, and PATCH:
/**
* Parse arbitrary multipart/form-data content
* Note: null result or null values for headers or value means error
* #return array|null [{"headers":array|null,"value":string|null}]
* #param string|null $boundary
* #param string|null $content
*/
function parse_multipart_content(?string $content, ?string $boundary): ?array {
if(empty($content) || empty($boundary)) return null;
$sections = array_map("trim", explode("--$boundary", $content));
$parts = [];
foreach($sections as $section) {
if($section === "" || $section === "--") continue;
$fields = explode("\r\n\r\n", $section);
if(preg_match_all("/([a-z0-9-_]+)\s*:\s*([^\r\n]+)/iu", $fields[0] ?? "", $matches, PREG_SET_ORDER) === 2) {
$headers = [];
foreach($matches as $match) $headers[$match[1]] = $match[2];
} else $headers = null;
$parts[] = ["headers" => $headers, "value" => $fields[1] ?? null];
}
return empty($parts) ? null : $parts;
}
Update
The function was updated to support arrays in form fields. That is fields like level1[level2] will be translated into proper (multidimensional) arrays.
I've just added a small function to my HTTP20 library, that can help with this. It is made to parse form data for PUT, DELETE and PATCH and add it to respective static variable to simulate $_POST global.
For now it's just for text fields, though, no binary support, since I currently do not have a good use case in my project to properly test it and I'd prefer not to share something I can't test extensively. But if I do get to it at some point - I will update this answer.
Here is the code:
public function multiPartFormParse(): void
{
#Get method
$method = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] ?? $_SERVER['REQUEST_METHOD'] ?? null;
#Get Content-Type
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
#Exit if not one of the supported methods or wrong content-type
if (!in_array($method, ['PUT', 'DELETE', 'PATCH']) || preg_match('/^multipart\/form-data; boundary=.*$/ui', $contentType) !== 1) {
return;
}
#Get boundary value
$boundary = preg_replace('/(^multipart\/form-data; boundary=)(.*$)/ui', '$2', $contentType);
#Get input stream
$formData = file_get_contents('php://input');
#Exit if failed to get the input or if it's not compliant with the RFC2046
if ($formData === false || preg_match('/^\s*--'.$boundary.'.*\s*--'.$boundary.'--\s*$/muis', $formData) !== 1) {
return;
}
#Strip ending boundary
$formData = preg_replace('/(^\s*--'.$boundary.'.*)(\s*--'.$boundary.'--\s*$)/muis', '$1', $formData);
#Split data into array of fields
$formData = preg_split('/\s*--'.$boundary.'\s*Content-Disposition: form-data;\s*/muis', $formData, 0, PREG_SPLIT_NO_EMPTY);
#Convert to associative array
$parsedData = [];
foreach ($formData as $field) {
$name = preg_replace('/(name=")(?<name>[^"]+)("\s*)(?<value>.*$)/mui', '$2', $field);
$value = preg_replace('/(name=")(?<name>[^"]+)("\s*)(?<value>.*$)/mui', '$4', $field);
#Check if we have multiple keys
if (str_contains($name, '[')) {
#Explode keys into array
$keys = explode('[', trim($name));
$name = '';
#Build JSON array string from keys
foreach ($keys as $key) {
$name .= '{"' . rtrim($key, ']') . '":';
}
#Add the value itself (as string, since in this case it will always be a string) and closing brackets
$name .= '"' . trim($value) . '"' . str_repeat('}', count($keys));
#Convert into actual PHP array
$array = json_decode($name, true);
#Check if we actually got an array and did not fail
if (!is_null($array)) {
#"Merge" the array into existing data. Doing recursive replace, so that new fields will be added, and in case of duplicates, only the latest will be used
$parsedData = array_replace_recursive($parsedData, $array);
}
} else {
#Single key - simple processing
$parsedData[trim($name)] = trim($value);
}
}
#Update static variable based on method value
self::${'_'.strtoupper($method)} = $parsedData;
}
Obviously you can safely remove method check and assignment to a static, if you do not those.
Have you looked at fopen("php://input", "r") for parsing the content?
Headers can also be found as $_SERVER['HTTP_*'], names are always uppercased and dashes become underscores, eg $_SERVER['HTTP_ACCEPT_LANGUAGE'].
I have a problem right now with CodeIgniter : I use the REST Controller library (which is really awesome) to create an API but I can not get PUT requests...
This is my code :
function user_put() {
$user_id = $this->get("id");
echo $user_id;
$username = $this->put("username");
echo $username;
}
I use curl to make the request :
curl -i -X PUT -d "username=test" http://[...]/user/id/1
The user_id is full but the username variable is empty. Yet it works with the verbs POST and GET.
Have you any idea please?
Thank you !
According to: http://net.tutsplus.com/tutorials/php/working-with-restful-services-in-codeigniter-2/ we should consult https://github.com/philsturgeon/codeigniter-restserver/blob/master/application/libraries/REST_Controller.php#L544 to see that this method:
/**
* Detect method
*
* Detect which method (POST, PUT, GET, DELETE) is being used
*
* #return string
*/
protected function _detect_method() {
$method = strtolower($this->input->server('REQUEST_METHOD'));
if ($this->config->item('enable_emulate_request')) {
if ($this->input->post('_method')) {
$method = strtolower($this->input->post('_method'));
} else if ($this->input->server('HTTP_X_HTTP_METHOD_OVERRIDE')) {
$method = strtolower($this->input->server('HTTP_X_HTTP_METHOD_OVERRIDE'));
}
}
if (in_array($method, array('get', 'delete', 'post', 'put'))) {
return $method;
}
return 'get';
}
looks to see if we've defined the HTTP header HTTP_X_HTTP_METHOD_OVERRIDE and it uses that in favor of the actual verb we've implemented on the web. To use this in a request you would specify the header X-HTTP-Method-Override: method (so X-HTTP-Method-Override: put) to generate a custom method override. Sometimes the framework expects X-HTTP-Method instead of X-HTTP-Method-Override so this varies by framework.
If you were doing such a request via jQuery, you would integrate this chunk into your ajax request:
beforeSend: function (XMLHttpRequest) {
//Specify the HTTP method DELETE to perform a delete operation.
XMLHttpRequest.setRequestHeader("X-HTTP-Method-Override", "DELETE");
}
You can try to detect the method type first and seperate the different cases. If your controller only handles REST functions it could be helpful to put get the required information in the constructor.
switch($_SERVER['REQUEST_METHOD']){
case 'GET':
$var_array=$this->input->get();
...
break;
case 'POST':
$var_array=$this->input->post();
...
break;
case 'PUT':
case 'DELETE':
parse_str(file_get_contents("php://input"),$var_array);
...
break;
default:
echo "I don't know how to handle this request.";
}
In CodeIgniter 4 use getRawInput which will retrieve data and convert it to an array.
$data = $request->getRawInput();
look this issue in github
PUT parameters only work in JSON format
https://github.com/chriskacerguis/codeigniter-restserver/issues/362
Checkout this link in the official Code Igniter Docs Using the Input Stream for Custom Request Methods
This is the Code Igniter way to do it.
Just call the below if the body of the request is form-urlencoded
$var1 = $this->input->input_stream('var_key')
// Or
$var1 = $this->security->xss_clean($this->input->input_stream('var_key'));
Codeigniter put_stream has provided no help, instead I had to use php input stream as following method can be added to helpers, from there you can parse put request in any of the controllers:
function parsePutRequest()
{
// Fetch content and determine boundary
$raw_data = file_get_contents('php://input');
$boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));
// Fetch each part
$parts = array_slice(explode($boundary, $raw_data), 1);
$data = array();
foreach ($parts as $part) {
// If this is the last part, break
if ($part == "--\r\n") break;
// Separate content from headers
$part = ltrim($part, "\r\n");
list($raw_headers, $body) = explode("\r\n\r\n", $part, 2);
// Parse the headers list
$raw_headers = explode("\r\n", $raw_headers);
$headers = array();
foreach ($raw_headers as $header) {
list($name, $value) = explode(':', $header);
$headers[strtolower($name)] = ltrim($value, ' ');
}
// Parse the Content-Disposition to get the field name, etc.
if (isset($headers['content-disposition'])) {
$filename = null;
preg_match(
'/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/',
$headers['content-disposition'],
$matches
);
list(, $type, $name) = $matches;
isset($matches[4]) and $filename = $matches[4];
// handle your fields here
switch ($name) {
// this is a file upload
case 'userfile':
file_put_contents($filename, $body);
break;
// default for all other files is to populate $data
default:
$data[$name] = substr($body, 0, strlen($body) - 2);
break;
}
}
}
return $data;
}
CodeIgniter doesn't support reading incoming PUT requests and if it's not essential I would stick to GET/POST for your API as its probably not necessary.
If you do need to read PUT requests take a look at Accessing Incoming PUT Data from PHP.
Recently, I've begun writing my own PHP OpenID consumer class in order to better understand openID. As a guide, I've been referencing the [LightOpenID Class][1]. For the most part, I understand the code and how OpenID works. My confusion comes when looking at the author's discover function:
function discover($url)
{
if(!$url) throw new ErrorException('No identity supplied.');
# We save the original url in case of Yadis discovery failure.
# It can happen when we'll be lead to an XRDS document
# which does not have any OpenID2 services.
$originalUrl = $url;
# A flag to disable yadis discovery in case of failure in headers.
$yadis = true;
# We'll jump a maximum of 5 times, to avoid endless redirections.
for($i = 0; $i < 5; $i ++) {
if($yadis) {
$headers = explode("\n",$this->request($url, 'HEAD'));
$next = false;
foreach($headers as $header) {
if(preg_match('#X-XRDS-Location\s*:\s*(.*)#', $header, $m)) {
$url = $this->build_url(parse_url($url), parse_url(trim($m[1])));
$next = true;
}
if(preg_match('#Content-Type\s*:\s*application/xrds\+xml#i', $header)) {
# Found an XRDS document, now let's find the server, and optionally delegate.
$content = $this->request($url, 'GET');
# OpenID 2
# We ignore it for MyOpenID, as it breaks sreg if using OpenID 2.0
$ns = preg_quote('http://specs.openid.net/auth/2.0/');
if (preg_match('#<Service.*?>(.*)<Type>\s*'.$ns.'(.*?)\s*</Type>(.*)</Service>#s', $content, $m)
&& !preg_match('/myopenid\.com/i', $this->identity)) {
$content = $m[1] . $m[3];
if($m[2] == 'server') $this->identifier_select = true;
$content = preg_match('#<URI>(.*)</URI>#', $content, $server);
$content = preg_match('#<LocalID>(.*)</LocalID>#', $content, $delegate);
if(empty($server)) {
return false;
}
# Does the server advertise support for either AX or SREG?
$this->ax = preg_match('#<Type>http://openid.net/srv/ax/1.0</Type>#', $content);
$this->sreg = preg_match('#<Type>http://openid.net/sreg/1.0</Type>#', $content);
$server = $server[1];
if(isset($delegate[1])) $this->identity = $delegate[1];
$this->version = 2;
$this->server = $server;
return $server;
}
# OpenID 1.1
$ns = preg_quote('http://openid.net/signon/1.1');
if(preg_match('#<Service.*?>(.*)<Type>\s*'.$ns.'\s*</Type>(.*)</Service>#s', $content, $m)) {
$content = $m[1] . $m[2];
$content = preg_match('#<URI>(.*)</URI>#', $content, $server);
$content = preg_match('#<.*?Delegate>(.*)</.*?Delegate>#', $content, $delegate);
if(empty($server)) {
return false;
}
# AX can be used only with OpenID 2.0, so checking only SREG
$this->sreg = preg_match('#<Type>http://openid.net/sreg/1.0</Type>#', $content);
$server = $server[1];
if(isset($delegate[1])) $this->identity = $delegate[1];
$this->version = 1;
$this->server = $server;
return $server;
}
$next = true;
$yadis = false;
$url = $originalUrl;
$content = null;
break;
}
}
if($next) continue;
# There are no relevant information in headers, so we search the body.
$content = $this->request($url, 'GET');
if($location = $this->htmlTag($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'value')) {
$url = $this->build_url(parse_url($url), parse_url($location));
continue;
}
}
if(!$content) $content = $this->request($url, 'GET');
# At this point, the YADIS Discovery has failed, so we'll switch
# to openid2 HTML discovery, then fallback to openid 1.1 discovery.
$server = $this->htmlTag($content, 'link', 'rel', 'openid2.provider', 'href');
$delegate = $this->htmlTag($content, 'link', 'rel', 'openid2.local_id', 'href');
$this->version = 2;
# Another hack for myopenid.com...
if(preg_match('/myopenid\.com/i', $server)) {
$server = null;
}
if(!$server) {
# The same with openid 1.1
$server = $this->htmlTag($content, 'link', 'rel', 'openid.server', 'href');
$delegate = $this->htmlTag($content, 'link', 'rel', 'openid.delegate', 'href');
$this->version = 1;
}
if($server) {
# We found an OpenID2 OP Endpoint
if($delegate) {
# We have also found an OP-Local ID.
$this->identity = $delegate;
}
$this->server = $server;
return $server;
}
throw new ErrorException('No servers found!');
}
throw new ErrorException('Endless redirection!');
}
[1]: http://gitorious.org/lightopenid
Okay, Here's the logic as I understand it (basically):
Check to see if the $url sends you a valid XRDS file that you then parse to figure out the OpenID provider's endpoint.
From my understanding, this is called the Yadis authentication method.
If no XRDS file is found, Check the body of the response for an HTML <link> tag that contains the url of the endpoint.
What. The. Heck.
I mean seriously? Essentially screen scrape the response and hope you find a link with the appropriate attribute value?
Now, don't get me wrong, this class works like a charm and it's awesome. I'm just failing to grok the two separate methods used to discover the endpoint: XRDS (yadis) and HTML.
My Questions
Are those the only two methods used in the discovery process?
Is one only used in version 1.1 of OpenID and the other in version 2?
Is it critical to support both methods?
The site I've encountered the HTML method on is Yahoo. Are they nuts?
Thanks again for your time folks. I apologize if I sound a little flabbergasted, but I was genuinely stunned at the methodology once I began to understand what measures were being taken to find the endPoint.
Specification is your friend.
But answering your question:
Yes. Those are the only two methods defined by the OpenID specifications (at least, for URLs -- there is a third method for XRIs).
No, both can be used with both version of the protocol. Read the function carefully, and you'll see that it supports both methods for both versions.
If you want your library to work with every provider and user, you'd better do. Some users paste the HTML tags into their sites, so their site's url can be used as an openid.
Some providers even use both methods at once, to mantain compatibility with consumers not implementing YADIS discovery (which isn't part of OpenID 1.1, but can be used with it). So that does make sense.
And yes, HTML discovery is about searching for a <link> in the response body. That's why it's called HTML discovery.