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.