Posted By

Jargon on 04/09/07


Tagged

php openid


Versions (?)

Who likes this?

3 people have marked this snippet as a favorite

vali29
Nix
illogic


Single-Page OpenID server


 / Published in: PHP
 

Not sure if this works yet

  1. <?
  2. # Copyright (c) 2005 JP Sugarbroad
  3. # Permission is granted to use this in any way you want, provided that you
  4. # include the above copyright notice (or one with similar effect) in any work
  5. # including non-trivial parts of this one.
  6.  
  7. # Change history:
  8. # 20050706.0: Fixed XTEA routines and signatures
  9. # 20050707.0: More XTEA fixes
  10. # 20050707.1: valid_root fix (thanks to meepbear)
  11. # 20050708.0: Fix cookie check
  12. # 20050708.1: Remove issued field from token
  13. # 20050713.0: Update to new protocol, added example login code
  14. # 20070110.0: Fix a bug in the XTEA implementation
  15. # (changed a = intval(a + b) to a = intval(a + (b))
  16.  
  17. define("SIGKEY", "something");
  18. define("COOKIE_NAME", "secret");
  19. define("COOKIE_VALUE", "cookie");
  20. define("VALID_TIME", 5 * 60); # token validity time
  21. define("ASSOC_TIME", 15 * 60); # assocation validity time
  22. define("KEY_LEN", 20); # Don't touch me
  23.  
  24. function t2utc($t) {
  25. return gmdate("Y-m-d\\TH:i:s\\Z", $t);
  26. }
  27.  
  28. function utc2t($s) {
  29. return strtotime(str_replace("T", " ", $s));
  30. }
  31.  
  32. # * is NOT OK
  33. # *.x is NOT OK
  34. # *.x.<tld_list non-element> is NOT OK
  35. # Anything else is OK
  36. # List from: http://www.iana.org/gtld/gtld.htm
  37. $tld_list = array(
  38. "aero" => 1,
  39. "biz" => 1,
  40. "com" => 1,
  41. "coop" => 1,
  42. "edu" => 1,
  43. "gov" => 1,
  44. "info" => 1,
  45. "int" => 1,
  46. "mil" => 1,
  47. "museum" => 1,
  48. "name" => 1,
  49. "net" => 1,
  50. "org" => 1,
  51. "pro" => 1);
  52.  
  53. function valid_wildcard($h) {
  54. switch (strrpos($h, "*")) {
  55. case false:
  56. # Not wildcard
  57. return true;
  58. case 0:
  59. # Wildcard
  60. break;
  61. default:
  62. # * not at start
  63. return false;
  64. }
  65. $h = explode(".", $h);
  66. if ($h[0] != "*") return false; # *xyz.stuff is bad
  67. switch (count($h)) {
  68. case 0:
  69. case 1:
  70. case 2:
  71. return false;
  72. case 3:
  73. return array_key_exists($h[-1], $tld_list);
  74. default:
  75. return true;
  76. }
  77. }
  78.  
  79. function valid_root($root, $ret) {
  80. $root = parse_url($root);
  81. if (isset($root["fragment"])) return false;
  82. $ret = parse_url($ret);
  83. if ($root["scheme"] != $ret["scheme"]) return false;
  84. if ($root["port"] != $ret["port"]) return false;
  85. if (isset($root["user"]) && $root["user"] != $ret["user"]) return false;
  86. if (isset($root["pass"]) && $root["pass"] != $ret["pass"]) return false;
  87. if (isset($root["query"]) && $root["query"] != $ret["query"]) return false;
  88. $h = $root["host"];
  89. if (!valid_wildcard($h)) return false;
  90. if ($h[0] == "*") {
  91. $hn = strlen($h) - 1;
  92. if (substr($h, 2) != substr($ret["host"], -$hn, $hn)) return false;
  93. } else {
  94. if ($h != $ret["host"]) return false;
  95. }
  96. $p1 = explode("/", rtrim($root["path"], "/"));
  97. $p2 = explode("/", rtrim($ret["path"], "/"));
  98. foreach ($p1 as $k => $v) {
  99. if ($p2[$k] != $v) return false;
  100. }
  101. return true;
  102. }
  103.  
  104. function randbytes($n) {
  105. $r = fopen("/dev/urandom", "rb");
  106. $s = fread($r, $n);
  107. fclose($r);
  108. return $s;
  109. }
  110.  
  111. function xtea_block($k, $v) {
  112. list(, $v0, $v1) = unpack("N*", $v);
  113. $sum = 0;
  114. $delta = 0x9E3779B9;
  115. for ($i = 0; $i < 32; $i++) {
  116. $v0 = intval($v0 + (($v1 << 4 ^ $v1 >> 5) + $v1 ^ $sum + $k[$sum & 3]));
  117. $sum = intval($sum + $delta);
  118. $v1 = intval($v1 + (($v0 << 4 ^ $v0 >> 5) + $v0 ^ $sum + $k[$sum >> 11 & 3]));
  119. }
  120. return pack("N2", $v0, $v1);
  121. }
  122.  
  123. function xtea_encrypt($key, $data) {
  124. $key = array_merge(unpack("N*", str_pad($key, 16, chr(0))));
  125. $v = randbytes(8);
  126. $out = $v;
  127. $i = 0;
  128. $l = strlen($data);
  129. while ($i < $l) {
  130. $v = xtea_block($key, $v);
  131. $p = substr($data, $i, 8);
  132. $i += 8;
  133. $v ^= $p;
  134. $out .= $v;
  135. }
  136. return $out;
  137. }
  138.  
  139. function xtea_decrypt($key, $data) {
  140. $key = array_merge(unpack("N*", str_pad($key, 16, chr(0))));
  141. $v = substr($data, 0, 8);
  142. $i = 8;
  143. $l = strlen($data);
  144. $out = "";
  145. while ($i < $l) {
  146. $v = xtea_block($key, $v);
  147. $c = substr($data, $i, 8);
  148. $i += 8;
  149. $out .= $v ^ $c;
  150. $v = $c;
  151. }
  152. return $out;
  153. }
  154.  
  155. define("HASH_LEN", 20);
  156. function hmac($key, $str) {
  157. $key = str_pad($key, 64, chr(0));
  158. $ipad = $key ^ str_repeat(chr(0x36), 64);
  159. $opad = $key ^ str_repeat(chr(0x5C), 64);
  160.  
  161. return pack("H*", sha1($opad . pack("H*", sha1($ipad . $str))));
  162. }
  163.  
  164. function sign_array($key, $data) {
  165. $token = "";
  166. foreach (explode(",", $data["signed"]) as $f) {
  167. $token .= "$f:$data[$f]\n";
  168. }
  169. return base64_encode(hmac($key, $token));
  170. }
  171.  
  172. function make_handle($expiry, $exposed, $key) {
  173. $token = pack("lc", $expiry, $exposed ? 1 : 0) . $key;
  174. return base64_encode(xtea_encrypt(SIGKEY, hmac(SIGKEY, $token) . $token));
  175. }
  176.  
  177. function check_handle($bh, $exposed_ok) {
  178. $handle = base64_decode($bh);
  179. # IV + HMAC + expiry + exposed
  180. if (!$handle || strlen($handle) < 8 + HASH_LEN + 5) return false;
  181. $handle = xtea_decrypt(SIGKEY, $handle);
  182. $data = substr($handle, HASH_LEN);
  183. if (hmac(SIGKEY, $data) != substr($handle, 0, HASH_LEN)) return false;
  184. $t = unpack("lexpiry/cexposed", $data);
  185. if ($t["expiry"] < time() || $t["exposed"] && !$exposed_ok) return false;
  186. return substr($data, 5);
  187. }
  188.  
  189. function make_args($prefix, $data) {
  190. $url = "";
  191. foreach ($data as $k => $v) {
  192. $url .= "$prefix$k=" . urlencode($v) . "&";
  193. }
  194. return rtrim($url, "&");
  195. }
  196.  
  197. function continuation() {
  198. $url = "";
  199. foreach ($_REQUEST as $k => $v) {
  200. if (strncmp($k, "openid_", 7) || $k == "openid_mode") continue;
  201. $url .= "&openid." . substr($k, 7) . "=" . urlencode($v);
  202. }
  203. return $url;
  204. }
  205.  
  206. $ret = $_REQUEST["openid_return_to"];
  207. if ($ret) {
  208. if (!preg_match("/^https?:/", $ret)) {
  209. header("HTTP/1.0 400 Bad Request");
  210. ?><html>
  211. <head><title>Bad Request</title></head>
  212. <body><p>The OpenID endpoint received an invalid request.</p></body>
  213. </html><?
  214. }
  215. if (strchr($ret, "?")) {
  216. $retp = "$ret&";
  217. } else {
  218. $retp = "$ret?";
  219. }
  220. }
  221.  
  222. $self = "http://" . $_SERVER["SERVER_NAME"];
  223. if ($_SERVER["SERVER_PORT"] != 80)
  224. $self .= ":" . $SERVER["SERVER_PORT"];
  225. $self .= "$_SERVER[PHP_SELF]?";
  226.  
  227. function badreq($msg) {
  228. global $ret, $retp;
  229. if ($_SERVER["REQUEST_METHOD"] == "POST") {
  230. header("HTTP/1.0 400 Bad Request");
  231. print "error:$msg\n";
  232. }
  233. if ($ret) {
  234. header("Location: " . $retp . "openid.mode=error&openid.error=" . urlencode($msg));
  235. } else {
  236. header("HTTP/1.0 400 Bad Request");
  237. }
  238. ?><html>
  239. <head><title>OpenID endpoint</title></head>
  240. <body><p>This is an OpenID server endpoint, not a human-readable resource. For more information, see <a href="http://openid.net/">http://openid.net/</a>.</p>
  241. <? if ($msg) { ?>
  242. <p>An error occurred processing your request: <?=htmlspecialchars($msg)?></body>
  243. <? } ?>
  244. </html><?
  245. }
  246.  
  247. $mode = $_REQUEST["openid_mode"];
  248. switch ($mode) {
  249. case "check_authentication":
  250. $resp = array();
  251. $sig = $_REQUEST["openid_signed"];
  252. $resp["signed"] = $sig;
  253. foreach (explode(",", $sig) as $f) {
  254. $resp[$f] = $_REQUEST["openid_" . $f];
  255. }
  256. $resp["mode"] = "id_res";
  257.  
  258. header("Content-Type: text/plain");
  259. $key = check_handle($_REQUEST["openid_assoc_handle"], false);
  260. if ($key && $_REQUEST["openid_sig"] == sign_array($key, $resp)) {
  261. $l = max(0, utc2t($resp["valid_to"]) - time());
  262. print "is_valid:" . ($l ? 1 : 0) . "\nlifetime:" . $l . "\n";
  263. } else {
  264. print "is_valid:0\nlifetime:0\n";
  265. }
  266. $ih = $_REQUEST["openid_invalidate_handle"];
  267. if ($ih && !check_handle($ih, true)) {
  268. print "invalidate_handle:$ih\n";
  269. }
  270. case "associate":
  271. $t = $_REQUEST["openid_assoc_type"];
  272. if (isset($t) && $t != "HMAC-SHA1") badreq("Unknown association type");
  273. $t = time();
  274. $e = $t + ASSOC_TIME;
  275. $r = randbytes(KEY_LEN);
  276. $handle = make_handle($e, true, $r);
  277. header("Content-Type: text/plain");
  278. print "assoc_type:HMAC-SHA1\nassoc_handle:" . $handle .
  279. "\nissued:" . t2utc($t) . # COMPAT
  280. "\nexpiry:" . t2utc($e) . # COMPAT
  281. "\nexpires_in:" . ASSOC_TIME .
  282. "\nmac_key:" . base64_encode($r) .
  283. "\n";
  284. case "login":
  285. case "checkid_immediate":
  286. case "checkid_setup":
  287. if ($_SERVER["REQUEST_METHOD"] != "GET") {
  288. badreq("Mode $mode requires GET method");
  289. }
  290. break;
  291. case null:
  292. badreq(null);
  293. default:
  294. badreq("Unknown mode $mode");
  295. }
  296.  
  297. if (!$ret) badreq("return_to required");
  298. # If we have checkid_setup issue a redirect with mode login, then we don't
  299. # have to make this a function. But that's an extra redirect we don't need.
  300. function login() {
  301. global $mode, $retp, $self;
  302. # Temporary code
  303. setcookie(COOKIE_NAME, COOKIE_VALUE, 0, $_SERVER["PHP_SELF"]);
  304. $_COOKIE[COOKIE_NAME] = COOKIE_VALUE;
  305. $mode = "checkid_immediate";
  306.  
  307. # login.php should set the cookie correctly and then send the user back to
  308. # the return_to parameter. Don't use a redirect, that can cause a redirect
  309. # loop. Change checkid_immediate to login_cancel if they cancel the login.
  310. # (Use substr_replace, not str_replace, to avoid corrupting the
  311. # continuation.)
  312.  
  313. # $url = $self . "openid.mode=checkid_immediate" . continuation();
  314. # header("Location: http://host/path/to/login.php?return_to=" . urlencode($url));
  315. # exit;
  316. }
  317. if ($mode == "login") login();
  318. if ($mode == "login_cancel") {
  319. header("Location: " . $retp . "openid.mode=cancel");
  320. }
  321.  
  322. if ($_COOKIE[COOKIE_NAME] != COOKIE_VALUE) {
  323. if ($mode == "checkid_setup") {
  324. login();
  325. } else {
  326. $url = "http://$_SERVER[SERVER_NAME]$_SERVER[PHP_SELF]?openid.mode=login" . continuation();
  327. $url = $retp . "openid.mode=id_res&openid.user_setup_url=" . urlencode($url);
  328. header("Location: $url");
  329. ?><html>
  330. <head><title>Login required</title></head>
  331. <body><p>You need to <a href="<?=htmlspecialchars($url)?>">log in</a> to be authenticated.</p></body>
  332. </html><?
  333. }
  334. }
  335.  
  336. $id = $_REQUEST["openid_identity"];
  337. #if ($id != "http://taral.net/") badreq("Unrecognized identity");
  338.  
  339. $root = $_REQUEST["openid_trust_root"];
  340. if (!$root || !valid_root($root, $ret)) {
  341. $root = $ret;
  342. }
  343. switch ($root) {
  344. case "http://www.lifewiki.net/":
  345. case "http://*.danga.com/openid/demo/":
  346. break;
  347. default:
  348. badreq("Requester not trusted");
  349. }
  350.  
  351. $t = time();
  352. $resp = array(
  353. "mode" => "id_res",
  354. "identity" => $id,
  355. "issued" => t2utc($t), # COMPAT
  356. "valid_to" => t2utc($t + VALID_TIME),
  357. "return_to" => $ret,
  358. "signed" => "mode,issued,valid_to,identity,return_to");
  359. $handle = $_REQUEST["openid_assoc_handle"];
  360. if ($handle) {
  361. $key = check_handle($handle, true);
  362. if ($key == false) $resp["invalidate_handle"] = $handle;
  363. }
  364. if (!$key) {
  365. $key = randbytes(KEY_LEN);
  366. $handle = make_handle($t + ASSOC_TIME, false, $key);
  367. }
  368. $resp["sig"] = sign_array($key, $resp);
  369. $resp["assoc_handle"] = $handle;
  370. $url = $retp . make_args("openid.", $resp);
  371. header("Location: $url");
  372. ?><html>
  373. <head><title>Authentication OK</title></head>
  374. <body><p>Authentication complete. <a href="<?=htmlspecialchars($url)?>">Click here to proceed</a></p></body>
  375. </html>

Report this snippet  

You need to login to post a comment.