Autoryzacja API – HMAC PHP

Autoryzacja API

Autoryzacja REST API jest czymś nieco innym niżeli autoryzacja użytkownika na stronie internetowej. Mechanizm  HTTP Basic Auth także tutaj nie pasuje. W przypadku naszego API nie tworzy się żadna sesja, która pozwalałaby na identyfikację konkretnego użytkownika / klienta korzystającego z dostępnych metod. Wykorzystujemy przecież protokół bezstanowy REST.

W minionym tygodniu zmierzyłem się z podobnym problemem w przypadku API, które piszę na potrzeby aplikacji mobilnej. Rozważane były tak naprawdę dwa rozwiązania: OAuth oraz wykorzystanie kluczy publicznych i prywatnych. Otwarty protokół OAuth wydawałby się idealnym rozwiązaniem. Okazuje się jednak, że jest kilka ALE, które komplikują jego implementację:

  • brak standaryzacji – Facebook i Twitter implementują w swoich API OAuth, ale jedno i drugie rozwiązanie różni się od siebie
  • skomplikowana implementacja po stronie serwera oraz aplikacji
  • spory narzut wydajnościowy

Finalne rozwiązanie oparte zostało o zasadę kluczy publicznych i prywatnych, ale zostało ono dostosowane do wymogów aplikacji mobilnej oraz możliwości jakie daje wewnętrzny framework. Chciałbym Wam dzisiaj jednak pokazać rozwiązanie oparte o metodę HMAC, która została zaimplementowana w języku PHP (>= 5.1.2) pod postacią funkcji hash_hmac.

Działanie HMAC

  1. stworzenie tak zwanego content hash przy użyciu private key, który znany jest tylko użytkownikowi (oraz systemowi)
  2. content hash przesyłany jest na serwer razem z public key
  3. serwer odczytuje public key i na jego podstawie szuka użytkownika, który pasuje do owego klucza , a następnie pobiera jego private key
  4. serwer używa pobranego private key do wykonania funkcji hash_hmac, która hashuje treść przesłaną w żądaniu HTTP i porównuje ją z content hash

Klucz publiczny i prywatny

By wygenerować public key i private key można posłużyć się kilkoma metodami. Poniżej pokażę dwie, chyba najprostsze, a zarazem skuteczne sposoby.

  1. Wykorzystanie OpenSSL
    $hash = hash('sha256', openssl_random_pseudo_bytes(32));

    OpenSSL nie jest domyślnym rozszerzeniem w PHP, dlatego też poniższa metoda powinna zastąpić wykorzystanie tej biblioteki.

  2. Wykorzystanie funkcji mt_rand:
    $hash = hash('sha256', mt_rand());

Oba przypadki jako funkcję hashującą wykosztują sha256, której obce są kolizje znane chociażby z funkcji skrótu jaką jest MD5.

Przykład wykorzystania HMAC w PHP

Czas na zabawę, czyli przejście do praktyki. Poniżej kod klienta i serwera, który może posłużyć jako przykład REST API.

Przykład wykorzystuje mechanizm cURL, który dostępny jest raczej w większości instalacji PHP. Zawsze można jednak użyć bardziej niskopoziomowej implementacji z socketami.

Kod klienta

<?php
	$publicKey  = '3441df0babc2a2dda551d7cd39fb235bc4e09cd1e4556bf261bb49188f548348';
	$privateKey = 'e249c439ed7697df2a4b045d97d4b9b7e1854c3ff8dd668c779013653913572e';
	$content    = json_encode(array(
		'test' => 'content'
	));

	$hash = hash_hmac('sha256', $content, $privateKey);

	$headers = array(
		'X-Public: '.$publicKey,
		'X-Hash: '.$hash
	);

	$ch = curl_init('http://test.localhost:8080/api-test/');
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_POSTFIELDS, $content);

	$result = curl_exec($ch);
	curl_close($ch);

	echo "RESULT\\n======\\n".print_r($result, true)."\\n\\n";
?>

Kod serwera

Po stronie serwera wykorzystam Slim Framework, który jest tak naprawdę mikroframeworkiem. Idealnie nadaje się on do małych aplikacji, które nie wymagają super mocy. Instalujemy Slim poprzez dodanie do swojego composer.json poniższych linijek:

{
    "require": {
        "slim/slim":"2.*"
    }
}

Composer zainstaluje nam cały framework w katalogu vendor/.

Więcej o Composer znajdziesz na: getcomposer.org

Nasz klient przesyłał do serwera nagłówki X-Public oraz X-Hash, które musimy pobrać po stronie serwera. Private key jest oczywiście hard-coded bezpośrednio w kodzie. W rzeczywistym systemie należałoby odpytać bazę danych i pobrać taką wartość z obiektu użytkownika.

<?php
require_once 'vendor/autoload.php';

$app = new \\Slim\\Slim();
$app->post('/', function() use ($app) {

    $request = $app->request();

    $publicHash  = $request->headers('X-Public');
    $contentHash = $request->headers('X-Hash');
    $privateKey  = 'e249c439ed7697df2a4b045d97d4b9b7e1854c3ff8dd668c779013653913572e';
    $content     = $request->getBody();

    $hash = hash_hmac('sha256', $content, $privateKey);

    if ($hash == $contentHash){
        echo "match!\n";
    }
});
?>

PS. Staram się by wpis miał przyjemną formę i był łatwy do czytania. Udało mi się? Okaże się tym samym kto dotarł do końca.

Jak sprawdzić za pomocą PHP, czy adres URL oraz email są poprawne i rzeczywiście istnieją?

Jak sprawdzić za pomocą PHP, czy adres URL oraz email są poprawne i rzeczywiście istnieją?

PHP posiada wiele wbudowanych funkcji (file_exist, is_dir), dzięki którym jesteśmy w stanie sprawdzić, czy na przykład konkretny plik istnieje na dysku, ale jak możemy zdalnie sprawdzić, czy dany adres URL, email, czy link do obrazka są poprawne oraz rzeczywiście istnieją? Postaram się to Wam dzisiaj pokazać dzięki kilku kawałkom kodu, które posłużą do konkretnych zadań.

Czy ten obrazek rzeczywiście tam jest?

PHP5 daje nam możliwość sprawdzenia obrazka, który kryje się nam pod konkretnym linkiem. Dzięki takiej funkcji jak GetImageSize jesteśmy w stanie pobrać obrazek trzymany na zdalnym serwerze i dzięki bibliotece GD zainstalowanej na naszej maszynie.

$external_link = 'http://mrzepinski.pl/wp-content/uploads/2012/08/mrzepinski-resized.png';
if (@GetImageSize($external_link)) {
    echo "obrazek istnieje!";
} else {
    echo "nic tam nie ma :(";
}

Powyższy sposób jest jednak mało efektywny. Cały obrazek jest najpierw pobierany na nasz serwer i dopiero wtedy następuje rzeczywiste sprawdzenie rozmiaru obrazka. Stwarza to też pewne zagrożenie bezpieczeństwa, bo przecież dobrze znane są przypadki, że zły kod krył się właśnie pod postacią rozszerzenia pliku, które sugerowałoby zupełnie inne przeznaczenie niżeli próba odpalenia złośliwego kodu i przejęcie kontroli nad funkcjonowaniem naszego systemu lub narobieniem w nim szkód.

Całość możemy jednak wykonać nieco inaczej. Jeżeli nie boimy się używać biblioteki cURL, to nasz kod może wyglądać następująco:

function checkRemoteFile($url) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    // nie pobieraj kontentu na serwer
    curl_setopt($ch, CURLOPT_NOBODY, 1);
    curl_setopt($ch, CURLOPT_FAILONERROR, 1);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    if (curl_exec($ch) !== false) {
        return true;
    } else {
        return false;
    }
}

Jeżeli są jednak tacy, którzy boją się używać cURL’a albo zwyczajnie nie mogą, to istnieje jeszcze funkcja file_get_contents, która pozwala nam na pobranie wystarczającej ilości danych z zewnętrznego serwera, by stwierdzić, że link jest poprawny i prowadzi do konkretnego pliku. Wystarczy, że pobierzemy w ten sposób 1 bajt informacji, tak jak w przykładzie poniżej.

function url_exists($url) {
    if (@file_get_contents($url, 0, null, 0, 1)) {
        return 1;
    } else { 
        return 0;
    }
}

Sprawdzamy adres URL

Taką funkcjonalność możemy uzyskać na wiele sposobów, ale nie wszystkie dobre są w każdej sytuacji. Poniżej kilka przykładów.

function url_exists($url) {
    if (strstr($url, "http://")) 
        $url = "http://".$url;
    $fp = @fsockopen($url, 80);

    return !($fp === false);
}

Powyższa metoda jest zdecydowanie szybsza niż funkcja fopen, ale tylko dla adresów z https:// lub nazw domenowych, ponieważ sprawdzając już adres http://example.com?p=231 możemy napotkać na małe problemy wydajnościowe.

Poniższy sposób jest bardziej kompleksowy, ponieważ sprawdza on tylko odpowiedź serwera, a dokładniej przesłane nagłówki.

function url_exists($url) {
     if ((strpos($url, "http")) === false) 
         $url =  "http://".$url;

     return is_array(@get_headers($url));
}

Całość jednak działa tylko dla URL z http:// oraz z PHP5 i nowszym. Możemy ją jednak nieco rozbudować, tak by i starsza wersja PHP nie była dla nas problemem.

function is_valid_url($url) {
    $url = @parse_url($url);
    if (!$url) {
        return false;
    }

    $url = array_map('trim', $url);
    $url['port'] = (!isset($url['port'])) ? 80 : (int)$url['port'];
    $path = (isset($url['path'])) ? $url['path'] : '';
    if ($path == '') {
        $path = '/';
    }

    $path .= (isset($url['query'])) ? "?$url[query] " : '';
    if (isset($url['host']) AND $url['host'] != gethostbyname($url['host'])) {
        if (PHP_VERSION >= 5) {
            $headers = get_headers("$url[scheme]://$url[host]:$url[port]$path ");
        } else {
            $fp = fsockopen($url['host'], $url['port'], $errno, $errstr, 30);
            if (!$fp) {
                return false;
            }
            fputs($fp, "HEAD $path HTTP/1.1rnHost: $url[host]rnrn");
            $headers = fread($fp, 4096);
            fclose($fp);
        }
        $headers = (is_array($headers)) ? implode("n", $headers) : $headers;
        return (bool)preg_match('#^HTTP/.*s+[(200|301|302)]+s#i', $headers);
    }
    return false;
}

Czego się nie robi, by zachować wsteczną kompatybilność :)

Pozostał nam jeszcze jeden sposób

function image_exist($url) {
    return (@fclose(@fopen( $url,  "r ")));
}

, ale wymaga on by zmienna allow_url_fopen była ustawiona na true w naszym pliku konfiguracyjnym PHP (php.ini).

Sprawdzamy adres em@il

Nadszedł czas na sposoby sprawdzenia poprawności i zweryfikowanie adresu email. Tutaj także mamy kilka ścieżek do wyboru.

Jak się okazuje, proces weryfikacji adresu email w dużej mierze zależy także od ustawień konkretnego serwera SMTP. Mamy jednak możliwość sprawdzenia istnienia konkretnej domeny, by zaoszczędzić sobie trochę czasu i od razu eliminować takie nieprawdziwe adresy. Jesteśmy w stanie to osiągnąć poprzez wykorzystanie funkcji PHP, jaką jest checkdnsrr, która – jak wskazuje nam na to jej nazwa – sprawdza rekordy DNS dla danego URL / hosta. Przykład poniżej.

function email_exist($email) {
    list($userid, $domain) = split("@", $email);

    return checkdnsrr($domain, "MX"));
}

Wszystko byłoby dobrze, ale oczywiście pojawia się kolejny problem. Windows do dzisiaj nie wspiera tej funkcji i aby móc z niej skorzystać, musimy ją sobie stworzyć sami, sic!

if (!function_exists('checkdnsrr'))
function checkdnsrr($hostName, $recType = '') {
   if (!empty($hostName)) {
       if ($recType == '') $recType = "MX";
           exec("nslookup -type=$recType $hostName", $result);

       foreach ($result as $line) {
           if (eregi("^$hostName", $line))
                return true;
       }

       return false;
   }

   return false;
}

Kiedy mamy już gotową obsługę dwóch platform (Linux i Windows), przejdźmy teraz do rzeczy właściwej, a mianowicie, sprawdźmy czy dany email jest poprawny oraz zweryfikujmy jego istnienie.

function isValidEmail($email) {
        $emailError = false;
        $email = htmlspecialchars(stripslashes(strip_tags(trim($email))));
        if ($email == " ") { 
            $emailError = true; 
        } elseif (!eregi( "^([a-zA-Z0-9._-])+@([a-zA-Z0-9._-])+.([a-zA-Z0-9._-])([a-zA-Z0-9._-])+ ", $email)) { 
            $emailError = true; 
        } else {
            list($email, $domain) = split("@", $email, 2);
            if (!checkdnsrr($domain, "MX")) { 
                emailError = true; 
            } else {
                $array = array($email, $domain);
                $email = implode("@", $array);
            }
        }
        return !emailError;
}

I to byłoby na tyle. Znacie jeszcze jakieś sposoby na realizację powyżej przedstawionych funkcjonalności? Jeżeli tak, to oczywiście piszcie w komentarzach. Podyskutujemy :)

Notyfikacje PUSH dla urządzeń z Androidem w PHP – GCM

Google Cloud Messaging for Android

Google Cloud Messaging to usługa uruchomiona przez Google niecały miesiąc temu (wcześniej C2DM). Pozwala ona na wysyłanie powiadomień PUSH na urządzenia z Androidem bezpośrednio z serwera obsługującego aplikację mobilną. Takie rozwiązanie posiadają wszystkie największe platformy – iOS, Windows Phone 7, Blackberry, a sama platforma Android praktycznie od samego początku swojego istnienia. Jednak to urządzenia od Apple cieszą się większą popularnością takiej usługi. Google postanowiło to zmienić i zbudowało od nowa całą usługę, tak by ułatwić deweloperom implementację w swoich projektach, a także obsługę takich notyfikacji po stronie serwera aplikacji. Jako, że w pracy przymierzam się do zadania stworzenia ogólnodostępnego API REST na wiele platform mobilnych, postanowiłem, że zacznę od napisania prostej klasy w PHP, która obsługuje właśnie GCM do Google.

Cała dokumentacja usługi od Google dostępna jest na stronie oficjalnej i nie ma sensu tutaj przytaczać tego co bardzo dobrze opisano właśnie tam. Bibliotekę JAR dla Javy mamy dostępną “od ręki” i możemy wykorzystać ją w swoim projekcie, ale z PHP nie jest już tak wesoło i sami musimy podjąć się implementacji GCM po swojej stronie.

Do napisania poniżej klasy posłużyłem się biblioteką Buzz, która jest niczym więcej jak oprogramowanym cURL’em.

<?php

namespace Service\Gcm;

use Buzz\Browser;
use Buzz\Client\MultiCurl;

class Gcm
{
    /**
     * @var string
     */
    protected $apiUrl = 'https://android.googleapis.com/gcm/send';

    /**
     * @var string
     */
    protected $apiKey;

    /**
     * @var string
     */
    protected $registrationIdMaxCount = 1000;

    /**
     * @var \Buzz\Browser
     */
    protected $browser;

    /**
     * @var array
     */
    protected $responses;

    /**
     * Class constructor
     *
     * @param $apiKey
     * @param null $baseUrl
     */
    public function __construct($apiKey, $apiUrl = null)
    {
        $this->apiKey = $apiKey;

        if ($apiUrl) {
            $this->apiUrl = $apiUrl;
        }

        $this->browser = new Browser(new MultiCurl());
    }

    /**
     * Sends the data to the given registration ID's via the GCM server
     *
     * @param mixed $data
     * @param array $registrationIds
     * @param array $options to add along with message, such as collapse_key, time_to_live, delay_while_idle
     * @return bool
     */
    public function send($data, array $registrationIds, array $options = array())
    {
        $headers = array(
            'Authorization: key='.$this->apiKey,
            'Content-Type: application/json'
        );

        $data = array_merge($options, array(
            'data' => $data,
        ));

        // Chunk number of registration ID's according to the maximum allowed by GCM
        $chunks = array_chunk($registrationIds, $this->registrationIdMaxCount);

        // Perform the calls (in parallel)
        $this->responses = array();
        foreach ($chunks as $registrationIds) {
            $data['registration_ids'] = $registrationIds;
            $this->responses[] = $this->browser->post($this->apiUrl, $headers, json_encode($data));
        }
        $this->browser->getClient()->flush();

        // Determine success
        foreach ($this->responses as $response) {
            $message = json_decode($response->getContent());
            if ($message === null || $message->success == 0 || $message->failure > 0) {
                return false;
            }
        }

        return true;
    }

    /**
     * @return array
     */
    public function getResponses()
    {
        return $this->responses;
    }
}

Prawda, że nie wygląda to skomplikowanie? Jest to tak naprawdę klasa-interfejs, do użycia w naszym kodzie, gdzie z bazy danych będziemy mogli pobrać zapisanie urządzenia oraz wysłać im przygotowane powiadomienia. Brakuje tutaj obsługi błędów, ale to dopiero przede mną :)