$this->digest( $token ), 'hint' => substr( $token, 0, strlen( self::PREFIX ) + 4 ), 'user_id' => $user_id, 'created' => time(), 'last_used' => 0, ), false ); return $token; } /** * Whether a token has been issued. * * @return bool */ public function exists() { $stored = $this->stored(); return ! empty( $stored['hash'] ); } /** * Delete the token. The endpoint stays registered but nothing can * authenticate to it. */ public function revoke() { delete_option( self::OPTION ); } /** * Non-secret description of the current token, for the admin UI. * * @return array{issued:bool, hint:string, user_id:int, created:int, last_used:int} */ public function info() { $stored = $this->stored(); return array( 'issued' => ! empty( $stored['hash'] ), 'hint' => isset( $stored['hint'] ) ? (string) $stored['hint'] : '', 'user_id' => isset( $stored['user_id'] ) ? absint( $stored['user_id'] ) : 0, 'created' => isset( $stored['created'] ) ? absint( $stored['created'] ) : 0, 'last_used' => isset( $stored['last_used'] ) ? absint( $stored['last_used'] ) : 0, ); } /** * Verify a presented token and return the user it acts as. * * @param string $token Plaintext token from the Authorization header. * @return int User ID, or 0 when the token is absent, wrong or orphaned. */ public function verify( $token ) { $token = is_string( $token ) ? trim( $token ) : ''; $stored = $this->stored(); if ( '' === $token || empty( $stored['hash'] ) ) { return 0; } if ( ! hash_equals( (string) $stored['hash'], $this->digest( $token ) ) ) { return 0; } $user_id = isset( $stored['user_id'] ) ? absint( $stored['user_id'] ) : 0; if ( ! $user_id || ! get_userdata( $user_id ) ) { // The issuing user is gone. Refuse rather than fall back to an // unscoped connection — the token's reach was defined by that user. return 0; } $this->touch( $stored ); return $user_id; } /** * A short fingerprint of the current token, for rate-limit bucket keys. * Never the token itself, and never the full digest. * * @return string */ public function fingerprint() { $stored = $this->stored(); return empty( $stored['hash'] ) ? '' : substr( (string) $stored['hash'], 0, 12 ); } /** * Record use, at most once per TOUCH_INTERVAL. * * @param array $stored Current stored record. */ private function touch( array $stored ) { $now = time(); $last = isset( $stored['last_used'] ) ? absint( $stored['last_used'] ) : 0; if ( $now - $last < self::TOUCH_INTERVAL ) { return; } $stored['last_used'] = $now; update_option( self::OPTION, $stored, false ); } /** * Storage digest of a plaintext token. * * @param string $token Plaintext token. * @return string */ private function digest( $token ) { return hash( 'sha256', $token ); } /** * The stored record. * * @return array */ private function stored() { $stored = get_option( self::OPTION, array() ); return is_array( $stored ) ? $stored : array(); } }