2-legged OAuth PHP İmplementasyonu

Şirket içinde geliştirilen bir uygulamaya hem web’den hem de native mobile uygulamalardan erişilmesi planlanmaktaydı. O nedenle API-Centric bir uygulama yapıp, tüm client’ların bu API üzerinden işlemlerini gerçekleştirmesine karar verdik. Güvenlik katmanında da 2-legged OAuth kullanmaktayız. Normal OAuth kullanımından farklı olarak 2-legged OAuth’da ziyaretçinin (resource owner) API’ye (source) izin vermesi gerekmiyor çünkü API’yi sadece kendi güvenilir client’ların (consumer) kullanması amaçlanıyor, bu nedenle uygulamaya ait client’ın yine kendisine ait API’yi kullanmak için izin alması gereksiz oluyor.

Şimdi PHP uygulamalarında 2-legged OAuth’un nasıl kullanılabileceğine bakalım. Bu demo uygulama için bir php micro framework olan Silex‘i kullandım.

Composer ile Silex’i kuralım. Örnek composer.json dosyası:

{
    "require": {
        "silex/silex": "1.0.*"
    },
    "minimum-stability": "dev"
}

Dependency’leri yükleyelim:

curl -s https://getcomposer.org/installer | php
php composer.phar install

Silex kullanılmaya hazır. Uygulama için 3 adet dosyamız var; config.php (Silex init ve diğer ayarlar), api.php (API route’ları) ve index.php (diğer route’lar).

config.php dosyası basitçe şöyle:

<?php

date_default_timezone_set('Europe/Istanbul');

$path = realpath(dirname(__FILE__)) . '/';
require_once $path . 'vendor/autoload.php';

// Silex init
$app = new Silex\Application();
$app['debug'] = true;

index.php içeriği:

<?php 

require 'config.php'; 
require 'api.php';   

$app->get('/', function () {
    return 'merhaba dunya!';
});

$app->run();

Son olarak api.php :

<?php

// save a user
$app->post('/api/user', function () use ($app) {
    
    // stuff..
    
    $payload = array(
        'success'  => true,
        'user_id'  => 1
    );
    
    return $app->json($payload);
    
});

// get a user info
$app->get('/api/user/{id}', function ($id) use ($app) {
    
    // stuff..
    
    $user = array(
        'success' => true,
        'user'    => array(
            'id'      => $id,
            'name'    => 'Aykut Farsak',
            'email'   => 'aykutfarsak@gmail.com'
        )
    );
    
    return $app->json($user);
    
});

Yukarıda gördüğünüz gibi 1 adet anasayfa, 2 adet de api endpoint adresi mevcut. /api/user adresi POST methodu kabul ediyor ve yeni kullanıcı oluşturmak için kullanılıyor. /api/user/{id} adresi ise mevcut bir kullanıcının bilgilerini almak için kullanılıyo ve GET methodu kabul ediyor.

Sıra geldi bu örnek uygulamaya 2-legged OAuth implementasyonuna. Designing a Secure REST (Web) API without OAuth adresinde yer alan çalışma yöntemini uygulayalım. Öncelikle akışı inceleyelim:

  1. [CLIENT] REST API’ye gitmeden önce, göndereceğimiz tüm data’ları bir araya getirelim.
  2. [CLIENT] İlk adımda bir araya getirdiğimiz data’yı, elimizdeki client_secret, time ve uri bilgisi ile (aşağıda parametrelerin ayrıntıları mevcut), HMAC-SHA1 ya da SHA256 gibi bir algoritma kullanarak hash’leyelim.
  3. [CLIENT] Server’a göndereceğimiz paremetreler şunlar:
    • Server’ın bizi tanıyabileceği bir client_id. Bu id’nin public olması önemli değil, asıl saklanması, paylaşılmaması gereken client_secret bilgisidir. Yani server’a istek gönderilirken bu id’nin görünür olması bir güvenlik tehlikesi oluşturmayacaktır. İsteğin kim tarafından gönderildiğinin anlaşılması için gerekli.
    • “replay attacks” ı önlemek için timestamp (time). Bu timestamp bilgisi server’ın isteğin eski mi yeni mi olduğuna karar vermesini sağlıyor.
    • API endpoint’i belirten uri bilgisi.
    • Server’da işlem yapmak için göndereceğimiz data’lar.
    • Yukarıdaki tüm parametreler ile oluşturduğumuz hash.
  4. [SERVER] Client’dan gelen data’ları alalım.
  5. [SERVER] İstekdeki timestamp’i kontrol edelim. Eğer izin verilen gecikme süresinin altında kalan bir timestamp ise timeout hatası verelim. Timestamp oluşturulurken ve server tarafında kontrol edilirken UTC kullanılırsa farklı time zone’larda sorun çıkarmayacaktır.
  6. [SERVER] client_id parametresine bakıp client_secret‘ı bulalım. Gelen data içinden hash’i çıkartıp, client_secret ile tekrar hash’leyelim (aynı hash algoritması ile). Eğer client’dan gelen hash ile yeniden üretilen hash aynıysa isteği yapan client güvenilirdir diye kabul edelim ve client’ın istediği işlemi gerçekleştirelim. Aynı değilse isteği reddedelim!

Örnek bir input datası şöyle olmalı:

$inputData = array(
    // client id
    'client_id' => 'web_client',
    // API endpoint
    'uri'       => 'user',
    // UTC timestamp
    'time'      => 1356354528,
    // data
    'username'  => 'aykutfarsak',
    'email'     => 'aykutfarsak@gmail.com',
    'password'  => 's3cr3t',
    // generated hash (sha256)
    'hash'      => 'e374369933c535fa458210b2131b7c9706895c098bf412e174ed678444226c99'
);

Yukarıdaki input data’sındaki hash’i şöyle oluşturabilirsiniz (NOT: $allData + hash = $inputData):

$hash = hash_hmac('sha256', json_encode($allData), 'my_client_secret');

Buraya kadarki bölümde 2-legged OAuth’un nasıl çalıştığını anlamış olduk. Yazının en başında da belirtiğim gibi API-Centric bir uygulama yapmayı hedeflemiştik. Yani tüm işler tek bir api üzerinden gerçekleşecek. Bu nedenle API ile aynı code base’de bulunan uygulamamızda (web client) işlemler için API’ye istek yapmak zorunda. Sonuç olarak uygulama içinde API’ye bir request göndermemiz gerekiyor. Bu request’i cURL ile gerçekleştirebiliriz. İlk bakışta bize performans kaybı yaşatacak gibi gözükse de, uygulama içinden yine uygulamaya istekde bulunmak, aynı network’de iki client’in haberleşmesi olduğundan epey hızlı gerçekleşecektir.

O zaman bize iki adet class gerekli. Hash sınıfını hash oluşturmak ve client’dan gelen hash’in doğru olup kontrol etmek için kullanacağız. ApiRequest sınıfı ise uygulama içindeki api isteklerimizi gerçekleştirecek.

(Hash sınıfına buradan ulaşabilirsiniz.) Hash sınıfı kullanımı şu şekilde:

// hash oluşturma
$hash = Hash::make($allData, 'my_secret_key');

// hash kontrolü
$isRequestValid = Hash::check($inputData, $hash);

(ApiRequest sınıfı burada.) ApiRequest sınıfının kullanımı ise şöyle:

$api = new ApiRequest();

// client ve api bilgileri
$api->setClientId('A_CLIENT')
    ->setSecretKey('CLIENT_SECRET_KEY')
    ->setEndpointUrl('http://localhost/2leggedoauth/api/'); // api adresi

// request
$response = $api->make('user', 'POST', array(
    'username' => 'aykutfarsak',
    'email'    => 'aykutfarsak@gmail.com',
    'password' => 's3cr3t'
));

Şimdi örnek uygulamamızdaki API metodları için bir middleware tanımlayalım.

use Symfony\Component\HttpFoundation\Request;

// before middleware
$hashValidation = function (Request $request) use ($app) {
    
    $allInputs = array_merge($request->query->all(), $request->request->all());
    $hash      = $request->get('hash');
    
    try {
    
        if ( !$hash ) {
            throw new Exception('Missing argument: hash');
        }
        
        if ( !Hash::check($allInputs, $hash) ) {
            throw new Exception('Invalid hash');
        }
    
    } catch (Exception $e) {
        return $app->json(array('success' => false, 'error' => $e->getMessage()), 500);
    }
};

$hashValidation middleware’ı gelen isteğin güvenilir olup olmadığını kontrol ediyor. Eğer gelen hash doğru ise isteğin devam etmesine izin veriyor, ama hash doğru değil ise istek api’ye ulaşmadan hata dönüyor.

Bu middleware’ı aktif etmek için API route closure’larının sonuna before filter olarak ekleyelim:

// save a user
$app->post('/api/user', function () use ($app) {
    
    // stuff..
    
    $payload = array(
        'success'  => true,
        'user_id'  => 1
    );
    
    return $app->json($payload);
    
})->before($hashValidation);

// get a user info
$app->get('/api/user/{id}', function ($id) use ($app) {
    
    // stuff..
    
    $user = array(
        'success' => true,
        'user'    => array(
            'id'      => $id,
            'name'    => 'Aykut Farsak',
            'email'   => 'aykutfarsak@gmail.com'
        )
    );
    
    return $app->json($user);
    
})->before($hashValidation);

Artık /api/user ve /api/user/{id} endpoint’lerine istek gelmeden önce, gelen isteğin güvenilir bir kaynaktan gelip gelmediği kontrol edilecek.

Son olarak config.php‘ye izin verilen client’ları ekleyip demo uygulamamızı bitiriyoruz.

// API Clients
Hash::setClients(array(
    'A_CLIENT' => 'CLIENT_SECRET_KEY',
));

Yukarıdaki demo uygulamanın kodlarına ve unit test’lerine buradan erişebilirsiniz.