Update: fixed a flaw in my implementation
I may have hinted at this a couple of times before, but now I'm actually saying something useful about it... I have a patch (php-openid.diff, for PHP 5, might also apply to PHP 4) for the openssl extension that makes it easier to build OpenID and TypeKey authentication support into your PHP apps.
I don't have a canned solution for you to deploy, but I can give you some pointers on how to use these bits. I'm assuming that you know a bit about how OpenID works.
This worked for me in my tests; it's not necessarily the most optimal way to do it, but it highlights how it works.
Thanks to the folks at JanRain, there was a flaw in my implementation that is now fixed.
Associate
Association allows you to generate a relationship with an OpenID server by generating and exchanging keys. It has nothing to do with an authentication request per-se; the result of the request can be used to authenticate a user later (and other users of that same identity server). The results of the association should be cached.
If you haven't already associated with an OpenID server, you'll want to do something like the following:
<?php
function btowc($str) {
if (ord($str[0]) > 127) {
return "\\x00" . $str;
}
return $str;
}
$assoc = array();
$crypto = array();
$dh = openssl_dh_generate_key(OPENID_P_VALUE, '2');
foreach (openssl_dh_get_params($dh) as $n => $v) {
$crypto[$n] = openssl_bignum_to_string($v, 10);
}
$params = array(
'openid.mode' => 'associate',
'openid.assoc_type' => 'HMAC-SHA1',
'openid.session_type' => 'DH-SHA1',
'openid.dh_modulus' => base64_encode(
btwoc(openssl_bignum_to_string(OPENID_P_VALUE))),
'openid.dh_gen' => base64_encode(
btwoc(openssl_bignum_to_string('2'))),
'openid.dh_consumer_public' => base64_encode(btwoc(
openssl_bignum_to_string($crypto['pub_key']))),
);
$r = perform_openid_rpc($server, $params); if ($r['session_type'] == 'DH-SHA1') {
$s_pub = openssl_bignum_from_bin(
base64_decode($r['dh_server_public']));
$dh_sec = openssl_dh_compute_key($dh, $s_pub);
if ($dh_sec === false) {
do {
$err = openssl_error_string();
if ($err === false) {
break;
}
echo "$err<br>\\n";
} while (true);
}
$sh_sec = sha1($dh_sec, true);
$enc_mac = base64_decode($r['enc_mac_key']);
$secret = $enc_mac ^ $sh_sec;
$assoc['secret'] = $secret;
$assoc['handle'] = $r['assoc_handle'];
$assoc['assoc_type'] = $r['assoc_type'];
$assoc['expires'] = time() + $r['expires_in'];
} else {
$assoc = false;
}
?>
Performing Authentication
Authentication is browser based; the user enters their URL into your site, and you then redirect to their OpenID server with a sprinkle of magic sauce in the get parameters. Here's how you create the sauce:
<?php
$x = parse_url($server);
$params = array();
if (isset($x['query'])) {
foreach (explode('&', $x['query']) as $param) {
list($k, $v) = explode('=', $param, 2);
$params[urldecode($k)] = urldecode($v);
}
}
$assoc = $this->associate($server);
$params['openid.mode'] = 'checkid_immediate';
$params['openid.identity'] = $delegate;
$params['openid.return_to'] = $returnURL;
$params['openid.trust_root'] = YOUR_TRUST_ROOT_URL;
$params['openid.sreg.required'] = 'nickname,email';
if ($assoc !== false) {
$params['openid.assoc_handle'] = $assoc['handle'];
}
$x['query'] = http_build_query($params);
?>
Once the user has authenticated against their ID server, they'll be redirected back to your $returnURL:
<?php
$assoc = $this->associate($args['srv']);
$token_contents = '';
foreach (explode(',', $_GET['openid_signed']) as $name) {
$token_contents .= "$name:" . $_GET["openid_" . str_replace('.', '_', $name)] . "\\n";
}
$x = hash_hmac('sha1', $token_contents, $assoc['secret'], true);
$hash = base64_encode($x);
if ($hash === $_GET['openid_sig']) {
return true;
}
$params = array();
$signed = explode(',', $_GET['openid_signed']);
$signed = array_merge($signed, array('assoc_handle', 'sig', 'signed', 'invalidate_handle'));
foreach ($signed as $name) {
$k = "openid_" . str_replace('.', '_', $name);
if (array_key_exists($k, $_GET)) {
$params["openid.$name"] = $_GET[$k];
}
}
$server = $args['srv'];
$params['openid.mode'] = 'check_authentication';
$res = perform_openid_rpc($server, $params);
if (isset($res['invalidate_handle'])) {
if ($res['invalidate_handle'] === $assoc['handle']) {
$this->associate($server, true);
}
}
return $res['is_valid'] === 'true';
?>
Didn't he also mention TypeKey?
Yeah, here's how to validate the signature you get when your user is redirected back from TypeKey:
<?php
$keydata = array();
$regkeys = cache::httpGet('http://www.typekey.com/extras/regkeys.txt', 24*60*60);
if ($regkeys === false) {
die("urgh");
}
foreach (explode(' ', $regkeys) as $pair) {
list($k, $v) = explode('=', trim($pair));
$keydata[$k] = $v;
}
$sig = str_replace(' ', '+', $_GET['sig']);
$email = $_GET['email'];
$name = $_GET['name'];
$nick = $_GET['nick'];
$ts = $_GET['ts'];
$msg = "$email::$name::$nick::$ts::" . TYPEKEY_TOKEN;
if (time() - $ts > 300) {
die("possible replay");
}
list($r_sig, $s_sig) = explode(':', $sig, 2);
$r_sig = base64_decode($r_sig);
$s_sig = base64_decode($s_sig);
$valid = openssl_dsa_verify(sha1($msg, true),
openssl_bignum_from_bin($r_sig),
openssl_bignum_from_bin($s_sig),
$keydata['p'], $keydata['q'],
$keydata['g'], $keydata['pub_key']);
?>