PageRenderTime 67ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/functions-auth.php

https://gitlab.com/Slind/YOURLS
PHP | 426 lines | 313 code | 37 blank | 76 comment | 59 complexity | 0f54bdc78cd667c9f6f1a7301ca29335 MD5 | raw file
  1. <?php
  2. /**
  3. * Check for valid user via login form or stored cookie. Returns true or an error message
  4. *
  5. */
  6. function yourls_is_valid_user() {
  7. static $valid = false;
  8. if( $valid )
  9. return true;
  10. // Allow plugins to short-circuit the whole function
  11. $pre = yourls_apply_filter( 'shunt_is_valid_user', null );
  12. if ( null !== $pre ) {
  13. $valid = ( $pre === true ) ;
  14. return $pre;
  15. }
  16. // $unfiltered_valid : are credentials valid? Boolean value. It's "unfiltered" to allow plugins to eventually filter it.
  17. $unfiltered_valid = false;
  18. // Logout request
  19. if( isset( $_GET['action'] ) && $_GET['action'] == 'logout' ) {
  20. yourls_do_action( 'logout' );
  21. yourls_store_cookie( null );
  22. return yourls__( 'Logged out successfully' );
  23. }
  24. // Check cookies or login request. Login form has precedence.
  25. yourls_do_action( 'pre_login' );
  26. // Determine auth method and check credentials
  27. if
  28. // API only: Secure (no login or pwd) and time limited token
  29. // ?timestamp=12345678&signature=md5(totoblah12345678)
  30. ( yourls_is_API() &&
  31. isset( $_REQUEST['timestamp'] ) && !empty($_REQUEST['timestamp'] ) &&
  32. isset( $_REQUEST['signature'] ) && !empty($_REQUEST['signature'] )
  33. )
  34. {
  35. yourls_do_action( 'pre_login_signature_timestamp' );
  36. $unfiltered_valid = yourls_check_signature_timestamp();
  37. }
  38. elseif
  39. // API only: Secure (no login or pwd)
  40. // ?signature=md5(totoblah)
  41. ( yourls_is_API() &&
  42. !isset( $_REQUEST['timestamp'] ) &&
  43. isset( $_REQUEST['signature'] ) && !empty( $_REQUEST['signature'] )
  44. )
  45. {
  46. yourls_do_action( 'pre_login_signature' );
  47. $unfiltered_valid = yourls_check_signature();
  48. }
  49. elseif
  50. // API or normal: login with username & pwd
  51. ( isset( $_REQUEST['username'] ) && isset( $_REQUEST['password'] )
  52. && !empty( $_REQUEST['username'] ) && !empty( $_REQUEST['password'] ) )
  53. {
  54. yourls_do_action( 'pre_login_username_password' );
  55. $unfiltered_valid = yourls_check_username_password();
  56. }
  57. elseif
  58. // Normal only: cookies
  59. ( !yourls_is_API() &&
  60. isset( $_COOKIE['yourls_username'] ) )
  61. {
  62. yourls_do_action( 'pre_login_cookie' );
  63. $unfiltered_valid = yourls_check_auth_cookie();
  64. }
  65. // Regardless of validity, allow plugins to filter the boolean and have final word
  66. $valid = yourls_apply_filter( 'is_valid_user', $unfiltered_valid );
  67. // Login for the win!
  68. if ( $valid ) {
  69. yourls_do_action( 'login' );
  70. // (Re)store encrypted cookie if needed
  71. if ( !yourls_is_API() ) {
  72. yourls_store_cookie( YOURLS_USER );
  73. // Login form : redirect to requested URL to avoid re-submitting the login form on page reload
  74. if( isset( $_REQUEST['username'] ) && isset( $_REQUEST['password'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
  75. $url = $_SERVER['REQUEST_URI'];
  76. yourls_redirect( $url );
  77. }
  78. }
  79. // Login successful
  80. return true;
  81. }
  82. // Login failed
  83. yourls_do_action( 'login_failed' );
  84. if ( isset( $_REQUEST['username'] ) || isset( $_REQUEST['password'] ) ) {
  85. return yourls__( 'Invalid username or password' );
  86. } else {
  87. return yourls__( 'Please log in' );
  88. }
  89. }
  90. /**
  91. * Check auth against list of login=>pwd. Sets user if applicable, returns bool
  92. *
  93. */
  94. function yourls_check_username_password() {
  95. global $yourls_user_passwords;
  96. if( isset( $yourls_user_passwords[ $_REQUEST['username'] ] ) && yourls_check_password_hash( $_REQUEST['username'], $_REQUEST['password'] ) ) {
  97. yourls_set_user( $_REQUEST['username'] );
  98. return true;
  99. }
  100. return false;
  101. }
  102. /**
  103. * Check a submitted password sent in plain text against stored password which can be a salted hash
  104. *
  105. */
  106. function yourls_check_password_hash( $user, $submitted_password ) {
  107. global $yourls_user_passwords;
  108. if( !isset( $yourls_user_passwords[ $user ] ) )
  109. return false;
  110. if ( yourls_has_phpass_password( $user ) ) {
  111. // Stored password is hashed with phpass
  112. list( , $hash ) = explode( ':', $yourls_user_passwords[ $user ] );
  113. $hash = str_replace( '!', '$', $hash );
  114. return ( yourls_phpass_check( $submitted_password, $hash ) );
  115. } else if( yourls_has_md5_password( $user ) ) {
  116. // Stored password is a salted md5 hash: "md5:<$r = rand(10000,99999)>:<md5($r.'thepassword')>"
  117. list( , $salt, ) = explode( ':', $yourls_user_passwords[ $user ] );
  118. return( $yourls_user_passwords[ $user ] == 'md5:'.$salt.':'.md5( $salt . $submitted_password ) );
  119. } else {
  120. // Password stored in clear text
  121. return( $yourls_user_passwords[ $user ] == $submitted_password );
  122. }
  123. }
  124. /**
  125. * Overwrite plaintext passwords in config file with phpassed versions.
  126. *
  127. * @since 1.7
  128. * @param string $config_file Full path to file
  129. * @return true if overwrite was successful, an error message otherwise
  130. */
  131. function yourls_hash_passwords_now( $config_file ) {
  132. if( !is_readable( $config_file ) )
  133. return 'cannot read file'; // not sure that can actually happen...
  134. if( !is_writable( $config_file ) )
  135. return 'cannot write file';
  136. // Include file to read value of $yourls_user_passwords
  137. // Temporary suppress error reporting to avoid notices about redeclared constants
  138. $errlevel = error_reporting();
  139. error_reporting( 0 );
  140. require $config_file;
  141. error_reporting( $errlevel );
  142. $configdata = file_get_contents( $config_file );
  143. if( $configdata == false )
  144. return 'could not read file';
  145. $to_hash = 0; // keep track of number of passwords that need hashing
  146. foreach ( $yourls_user_passwords as $user => $password ) {
  147. if ( !yourls_has_phpass_password( $user ) && !yourls_has_md5_password( $user ) ) {
  148. $to_hash++;
  149. $hash = yourls_phpass_hash( $password );
  150. // PHP would interpret $ as a variable, so replace it in storage.
  151. $hash = str_replace( '$', '!', $hash );
  152. $quotes = "'" . '"';
  153. $pattern = "/[$quotes]${user}[$quotes]\s*=>\s*[$quotes]" . preg_quote( $password, '-' ) . "[$quotes]/";
  154. $replace = "'$user' => 'phpass:$hash' /* Password encrypted by YOURLS */ ";
  155. $count = 0;
  156. $configdata = preg_replace( $pattern, $replace, $configdata, -1, $count );
  157. // There should be exactly one replacement. Otherwise, fast fail.
  158. if ( $count != 1 ) {
  159. yourls_debug_log( "Problem with preg_replace for password hash of user $user" );
  160. return 'preg_replace problem';
  161. }
  162. }
  163. }
  164. if( $to_hash == 0 )
  165. return 0; // There was no password to encrypt
  166. $success = file_put_contents( $config_file, $configdata );
  167. if ( $success === FALSE ) {
  168. yourls_debug_log( 'Failed writing to ' . $config_file );
  169. return 'could not write file';
  170. }
  171. return true;
  172. }
  173. /**
  174. * Hash a password using phpass
  175. *
  176. * @since 1.7
  177. * @param string $password password to hash
  178. * @return string hashed password
  179. */
  180. function yourls_phpass_hash( $password ) {
  181. $hasher = yourls_phpass_instance();
  182. return $hasher->HashPassword( $password );
  183. }
  184. /**
  185. * Check a clear password against a phpass hash
  186. *
  187. * @since 1.7
  188. * @param string $password clear (eg submitted in a form) password
  189. * @param string $hash hash supposedly generated by phpass
  190. * @return bool true if the hash matches the password once hashed by phpass, false otherwise
  191. */
  192. function yourls_phpass_check( $password, $hash ) {
  193. $hasher = yourls_phpass_instance();
  194. return $hasher->CheckPassword( $password, $hash );
  195. }
  196. /**
  197. * Helper function: create new instance or return existing instance of phpass class
  198. *
  199. * @since 1.7
  200. * @param int $iteration iteration count - 8 is default in phpass
  201. * @param bool $portable flag to force portable (cross platform and system independant) hashes - false to use whatever the system can do best
  202. * @return object a PasswordHash instance
  203. */
  204. function yourls_phpass_instance( $iteration = 8, $portable = false ) {
  205. $iteration = yourls_apply_filter( 'phpass_new_instance_iteration', $iteration );
  206. $portable = yourls_apply_filter( 'phpass_new_instance_portable', $portable );
  207. if( !class_exists( 'PasswordHash' ) ) {
  208. require_once( YOURLS_INC.'/phpass/PasswordHash.php' );
  209. }
  210. static $instance = false;
  211. if( $instance == false ) {
  212. $instance = new PasswordHash( $iteration, $portable );
  213. }
  214. return $instance;
  215. }
  216. /**
  217. * Check to see if any passwords are stored as cleartext.
  218. *
  219. * @since 1.7
  220. * @return bool true if any passwords are cleartext
  221. */
  222. function yourls_has_cleartext_passwords() {
  223. global $yourls_user_passwords;
  224. foreach ( $yourls_user_passwords as $user => $pwdata ) {
  225. if ( !yourls_has_md5_password( $user ) && !yourls_has_phpass_password( $user ) ) {
  226. return true;
  227. }
  228. }
  229. return false;
  230. }
  231. /**
  232. * Check if a user has a hashed password
  233. *
  234. * Check if a user password is 'md5:[38 chars]'.
  235. * TODO: deprecate this when/if we have proper user management with password hashes stored in the DB
  236. *
  237. * @since 1.7
  238. * @param string $user user login
  239. * @return bool true if password hashed, false otherwise
  240. */
  241. function yourls_has_md5_password( $user ) {
  242. global $yourls_user_passwords;
  243. return( isset( $yourls_user_passwords[ $user ] )
  244. && substr( $yourls_user_passwords[ $user ], 0, 4 ) == 'md5:'
  245. && strlen( $yourls_user_passwords[ $user ] ) == 42 // http://www.google.com/search?q=the+answer+to+life+the+universe+and+everything
  246. );
  247. }
  248. /**
  249. * Check if a user's password is hashed with PHPASS.
  250. *
  251. * Check if a user password is 'phpass:[lots of chars]'.
  252. * TODO: deprecate this when/if we have proper user management with password hashes stored in the DB
  253. *
  254. * @since 1.7
  255. * @param string $user user login
  256. * @return bool true if password hashed with PHPASS, otherwise false
  257. */
  258. function yourls_has_phpass_password( $user ) {
  259. global $yourls_user_passwords;
  260. return( isset( $yourls_user_passwords[ $user ] )
  261. && substr( $yourls_user_passwords[ $user ], 0, 7 ) == 'phpass:'
  262. );
  263. }
  264. /**
  265. * Check auth against encrypted COOKIE data. Sets user if applicable, returns bool
  266. *
  267. */
  268. function yourls_check_auth_cookie() {
  269. global $yourls_user_passwords;
  270. foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
  271. if ( yourls_salt( $valid_user ) == $_COOKIE['yourls_username'] ) {
  272. yourls_set_user( $valid_user );
  273. return true;
  274. }
  275. }
  276. return false;
  277. }
  278. /**
  279. * Check auth against signature and timestamp. Sets user if applicable, returns bool
  280. *
  281. */
  282. function yourls_check_signature_timestamp() {
  283. // Timestamp in PHP : time()
  284. // Timestamp in JS: parseInt(new Date().getTime() / 1000)
  285. global $yourls_user_passwords;
  286. foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
  287. if (
  288. (
  289. md5( $_REQUEST['timestamp'].yourls_auth_signature( $valid_user ) ) == $_REQUEST['signature']
  290. or
  291. md5( yourls_auth_signature( $valid_user ).$_REQUEST['timestamp'] ) == $_REQUEST['signature']
  292. )
  293. &&
  294. yourls_check_timestamp( $_REQUEST['timestamp'] )
  295. ) {
  296. yourls_set_user( $valid_user );
  297. return true;
  298. }
  299. }
  300. return false;
  301. }
  302. /**
  303. * Check auth against signature. Sets user if applicable, returns bool
  304. *
  305. */
  306. function yourls_check_signature() {
  307. global $yourls_user_passwords;
  308. foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
  309. if ( yourls_auth_signature( $valid_user ) == $_REQUEST['signature'] ) {
  310. yourls_set_user( $valid_user );
  311. return true;
  312. }
  313. }
  314. return false;
  315. }
  316. /**
  317. * Generate secret signature hash
  318. *
  319. */
  320. function yourls_auth_signature( $username = false ) {
  321. if( !$username && defined('YOURLS_USER') ) {
  322. $username = YOURLS_USER;
  323. }
  324. return ( $username ? substr( yourls_salt( $username ), 0, 10 ) : 'Cannot generate auth signature: no username' );
  325. }
  326. /**
  327. * Check if timestamp is not too old
  328. *
  329. */
  330. function yourls_check_timestamp( $time ) {
  331. $now = time();
  332. // Allow timestamp to be a little in the future or the past -- see Issue 766
  333. return yourls_apply_filter( 'check_timestamp', abs( $now - $time ) < YOURLS_NONCE_LIFE, $time );
  334. }
  335. /**
  336. * Store new cookie. No $user will delete the cookie.
  337. *
  338. */
  339. function yourls_store_cookie( $user = null ) {
  340. if( !$user ) {
  341. $pass = null;
  342. $time = time() - 3600;
  343. } else {
  344. global $yourls_user_passwords;
  345. if( isset($yourls_user_passwords[$user]) ) {
  346. $pass = $yourls_user_passwords[$user];
  347. } else {
  348. die( 'Stealing cookies?' ); // This should never happen
  349. }
  350. $time = time() + YOURLS_COOKIE_LIFE;
  351. }
  352. $domain = yourls_apply_filter( 'setcookie_domain', parse_url( YOURLS_SITE, 1 ) );
  353. $secure = yourls_apply_filter( 'setcookie_secure', yourls_is_ssl() );
  354. $httponly = yourls_apply_filter( 'setcookie_httponly', true );
  355. // Some browser refuse to store localhost cookie
  356. if ( $domain == 'localhost' )
  357. $domain = '';
  358. if ( !headers_sent() ) {
  359. // Set httponly if the php version is >= 5.2.0
  360. if( version_compare( phpversion(), '5.2.0', 'ge' ) ) {
  361. setcookie('yourls_username', yourls_salt( $user ), $time, '/', $domain, $secure, $httponly );
  362. } else {
  363. setcookie('yourls_username', yourls_salt( $user ), $time, '/', $domain, $secure );
  364. }
  365. } else {
  366. // For some reason cookies were not stored: action to be able to debug that
  367. yourls_do_action( 'setcookie_failed', $user );
  368. }
  369. }
  370. /**
  371. * Set user name
  372. *
  373. */
  374. function yourls_set_user( $user ) {
  375. if( !defined( 'YOURLS_USER' ) )
  376. define( 'YOURLS_USER', $user );
  377. }