Wez Furlong I am Wez Furlong, Chief Software Architect at Message Systems. We're responsible for building an awesome Messaging Platform.

I'm also a PHP Core developer and OpenSource contributor, residing in Maryland, USA with Juliette, Xander and Lily. (read more)

Subscribe. (circulation 945)
Comments. (circulation 8)

Search powered by Google

OpenID (and TypeKey) using native OpenSSL functions in PHP

15th February 2007 @ 22:23 EDT

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
  // convert openssl unsigned big endian into signed two's complement
  // notation
  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); // talk to server
  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
  // $identifier is the URL they gave to you
  // $server is the server you discovered
  // $delegate is the identity you discovered
  // $returnURL is your auth endpoint to receive the results
  $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);
    }
  }
  // get assoc details from cache, or associate now.
  $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);
  // you can now assemble $x into a URL and redirect the user there
?>

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 = '';
    /* note well: the name in the token_contents hash is the
     * name without any prefix.
     * This nuance can keep you occupied for hours. */
    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']) {
      // Authenticated
      return true;
    }
    /* not valid for whatever reason; we need to do a dumb mode check */
    $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'];
    /* broken spec.  You need to set openid.mode to
     * check_authentication to get it to do the auth checks.
     * But, it needs openid.mode to be id_res for the signature to work.
     */
    $params['openid.mode'] = 'check_authentication';
    $res = perform_openid_rpc($server, $params);
    if (isset($res['invalidate_handle'])) {
      if ($res['invalidate_handle'] === $assoc['handle']) {
        /* remove association */
        $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']);
?>

by Wez Furlong in .
Post a comment

Andi Gutmans

10th February 2007 @ 16:52 EDT

Very cool. Shouldn't we have an ext/openid to make it really easy for our users to use it? Or do you think we should implement this in user-space in the frameworks?

Library/Framework

10th February 2007 @ 17:09 EDT

The actual integration process requires knowledge about your application. For instance, in my implementation, I have a helper that encodes information into the $returnURL (and signs it) so that I can validate the incoming request and protect against replay attacks. The form that this takes depends on the way that my app works, and I can see that being different for different places, so I think that any canned solution should be in user-space to make it easy to customize.

Library

10th February 2007 @ 19:10 EDT

I have been asked the same thing about InfoCards as well, but my thoughts have been along the same lines as Wez. As these technologies are starting to be converge and stabilize a bit, does it make sense to at least have a single library providing some of the core functionality for these technologies? Now that they are talking about these leveraging each other, and adding more functionality, some of this is getting difficult to do in user space and cover all the various combinations that can be used.

Post a comment

Would you like to work with me?
I have positions open for server/infrastructure software development (C) and QA.

Ohloh profile for wez