15 Jan

Konstruáljunk web API-t - Passporticus maximus


Az előző cikkben láthattuk, hogy az Apigility, habár nagyon sok mindenben megkönnyíti az ügyünket, amikor az OAuth dolgaira kerül a sor, itt sem ússzuk meg azt, hogy belemásszunk a rendszerbe. A mostani cikkben arra vagyunk kiváncsiak, hogy vajon a Laravel 5.3-al érkező modul, a Passport mennyivel lesz jobb/rosszabb megoldás.



A Passport a Laravel 5.3 része, ennélfogva nem árt, hogyha upgradeljük azt előtte (ha esetleg dockerbe szeretnénk tenni). Ha ezzel megvagyunk, akkor már csak fel kell telepítenünk az ide tartozó csomagot:

composer require laravel/passport

Ha ezzel megvolnánk, akkor jöhet a szokásos Laravel feketemágia, mégpedig az, hogy beregisztráljuk a csomaggal érkező ServiceProvidert a config/app.php-ben a providers tömbbe:
Laravel\Passport\PassportServiceProvider::class

Most, hogy már az alkalmazásunk rálát a Passport nyújtotta szolgáltatásokra, futtassuk le a migrációt, amit regisztrált nekünk!
php artisan migrate

A kreált táblák nagyban hasonlítanak az apigilitysre, de ez nyílván az OAuth specifikációja miatt van így.

Viszont a táblák még üresek, ennélfogva szükségünk lesz kulcsokra, amik segítségével használjuk majd, ezeket a
php artisan passport:install

segítségével tudjuk véghezvinni.  A konzolból jól látszik, hogy csinált két klienst is, egyik ún. personal access, a másik pedig ún. password grant kliens.  Ezekről mindjárt beszélünk bővebben. Na most ahhoz, hogy egy felhasználónak tudjunk tokent adni, szükség lesz arra, hogy tudja a Passport, hogy ő bizony kaphat tokent. Ezért adjunk hozzá a HasApiTokens trait-et a User modelünkhöz:
class User extends Authenticatable
{
    use Notifiable, HasApiTokens;

Ez egy csomó helper metódust fog számunkra biztosítani, amikkel azonosítani tudjuk a felhasználót, tokent lekérni, stb.

Most haladjunk még egy lépést előre és regisztráljuk be a szükséges endpointokat. Ehhez az AuthServiceProviderünk boot metódusában be kell regisztrálnunk azokat:
public function boot()
{
    $this->registerPolicies();

    Passport::routes();
    
}

Ezáltal a route-ok bekerülnek, viszont továbbra sem tud minket authentikálni a rendszer, mert nem ő van kijelölve, mint felelős. Ahhoz, hogy ezt is megtegyük, vegyük fel a config/auth.php-ben:
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

Ezáltal ha egy auth:api middleware-el védünk le egy route-ot, akkor már a passportot fogja meghívni, nem pedig api_token fieldet keres a users táblában az adott user mellé. Bamm, akkor mostmár megy igaz? Bizony, az alapja ez az egésznek, de majd rálesünk hogy is kell személyre szabni az egészet. Az alap guide egy vue.js-es frontendet röttyintetne össze velünk, de minket most a backend érdekel, a frontendet majd mi megírjuk Angular 2-ben, igaz?:)

Nézzük meg, hogy milyen route-ok lettek beregisztrálva, hogy tudjuk mire lövünk:
php artisan route:list

Az eredmény egy szép lista lesz:



 

Na most, ha emlékszünk még az Oauthos alapokra, akkor tudjuk, hogy mi most nem egy kliens nevében, hanem egy felhasználó nevében szeretnénk valamit csinálni. De nézzük meg, hogy mire is gondolunk most?

Hozzunk létre egy klienst magunknak:
php artisan passport:client

Adjuk meg a nevét, az ID-t és a redirect URL-t.

Lőjük meg egy GET kéréssel a fent látható URL-ek közül az authorize-ot:
GET /oauth/authorize?response_type=code&client_id=CLIENT_ID

 
Emlékszünk még az apigility-re, ugye? Az biza már itt is megkövetelte a redirect_uri paraméter meglétét.

Na most a szomorú történet az, hogy redirektálnak minket a /login oldalra, ahol egy jól szituált 404 fogad bennünket. Bizony, szükségünk lesz egy bejelentkezett userre ebben az esetben. Ahhoz pedig létre kell azt hoznunk és a bejelentkező felület dolgait be kell húzzuk:
php artisan make:auth

Ezzel behúztuk a loginhoz tartozó view-t és route-okat, így a /login már bejön rendesen, viszont a users táblánk töküres. Tegyünk bele egy usert! Hozzuk elő az interaktív shellt:
php artisan tinker

Azután pedig hozzunk létre egy usert:
$u = new App\User();
$u->email = "test@test.hu";
$u->password = bcrypt("test");
$u->name = "Dezső";
$u->save();

Ezzel létre is hoztuk a felhasználót, akinek a segítségével már be tudunk lépni, ahol az alábbi képernyő fogad:

Ha rányomunk az Authorize-ra, akkor átirányít a megadott callback URL-re, amit bizony már nekünk kell implementálnunk. Na itt kezdődik a mi alkalmazásunk, a kliens kódja. Itt többféle lehetőség áll előttünk. Az egyik ilyen lehetőség, hogy a kliensünk ugyanott lesz, ahol a szerver is, hiszen ez a saját API-nk, nemde? Viszont így megkérdőjelezzük kicsit, hogy miért is erőltetjük az OAuth-ot. Csináljunk hát egy egyszerű kis klienst valahol máshol:
composer create-project laravel/laravel some-oauth-client

Ha nem akartok kézzel újabb vhostot felvenni, akkor itt egy jó kis shell script a linuxosoknak, amivel könnyedén lehet újat felvenni.

Ha ezzel megvolnánk, akkor hozzunk létre benne egy pofonegyszerű végpontot (mint kiderült annyira nem is lesz pofonegyszerű):
Route::get('/callback', function (\Illuminate\Http\Request $request) {

    \Auth::login($request->input("code"));

    return redirect("/home");
})->middleware("guest");

Na most itt mi is történik? A passportunk átirányít ide, a query paraméterek közt az authorization code-al. Ezt átadjuk az aktuális guard implementációnknak, az bejelentkeztet bennünket és ezután átirányítunk a /home-ra. Ha pedig már be vagyunk jelentkezve, akkor a guest middleware segítségével rögtön a /home-ra leszünk átirányítva. Egyszerűnek tűnik, igaz? Na de a bibi az, hogy a Guard implementációt biza nekünk kell megírni. A lényeg, amit itt akarunk, hogy összegyógyítsuk a Laravelt és a tokenünk életciklusát.

Először is nézzük meg, hogy mi történik, ha létrehozunk egy /home route-ot és ráeresztjük az auth:web middleware-t és odanavigálunk.
Route::get("/home", function(\Illuminate\Contracts\Auth\Guard $auth) {
    return $auth->user();
})->middleware("auth:web");

Bizony ő bennünket a /login végpontra fog átirányítani, amivel két bajunk is van. Egyik, hogy 404-et dob, a másik pedig az, hogy nekünk az lenne a jó, ha az OAuth bejelentkező oldalára irányítana, vagyis valami hasonlóra:
http://passport.localhost.hu/oauth/authorize?client_id=3&redirect_uri=http%3A%2F%2Foauth.localhost.hu%2Fcallback&response_type=code&scope=

Ahonnan a Passportos laravel persze a saját /login végpontjára majd átirányít minket, de ez más kérdés. Hol tudjuk ezt meghekkölni?

Bizony, itt kapunk egy AuthenticationException-t, amit elkap a mi kis Handlerünk, az app\Exceptions\Handler.php-ben és ő irányít át bennünket. Az itteni unauthenticated metódus végén cseréljük le az eddigi return-t erre:
$query = http_build_query([
    'client_id' => config("oauth.client_id"),
    'redirect_uri' => config("oauth.redirect_uri"),
    'response_type' => 'code',
    'scope' => '',
]);
return redirect('http://passport.localhost.hu/oauth/authorize?'.$query);

Ez felépíti nekünk a query stringet és redirectál a megfelelő helyre. Persze ehhez az kell, hogy a konfigba is beleírjunk. Hozzunk létre egy config\oauth.php-t:
return [
    "token_url" => 'http://passport.localhost.hu/oauth/token',
    "client_id" => 'CLIENT_ID',
    "user_url" => 'http://passport.localhost.hu/api/me',
    "redirect_uri" => 'http://oauth.localhost.hu/callback',
    "client_secret" =>  'SECRET',
];

Ezzel a tartalommal. Azt mostmár elértük, hogy a megfelelő helyre leszünk irányítva, ha nem vagyunk bejelentkezve, de meg kéne írni azt a logikát, ami elhiteti a laravellel, hogy mi be vagyunk jelentkezve. Ehhez létre kell hozni egy Guardot, ami legyen pl. az app\Services\Auth\OauthGuard.php:  

Mielőtt bőszen elkezdenénk írni, be kell ezt regisztrálni, az AuthServiceProviderünkben:
public function boot()
{
    $this->registerPolicies();

    \Auth::extend('oauth', function ($app, $name, array $config) {
        return new OauthGuard($app->make("session"), $app->make("config"));
    });
}

Átadjuk neki a SessionManagert és a Configot is. Jelenleg sessionben fogunk tárolni adatokat, de az implementáció változhat persze. Most, hogy létrehoztunk egy új Guardot, be is lehet regisztrálni a config\auth.php-ben:
'guards' => [
    'web' => [
        'driver' => 'oauth',
        'provider' => 'users',
    ],

Ha ezzel megvoltunk, akkor hozzuk létre magát az osztályt:
class OauthGuard implements Guard {

    private $session;
    private $config;

    private $OAUTH_USER_SESSION_KEY = "oauth_user";

    private $ACCESS_TOKEN_SESSION_KEY = "accessToken";

    private $EXPIRE_DATE_SESSION_KEY = "expireDate";

    private $REFRESH_TOKEN_SESSION_KEY = "refreshToken";

    public function __construct(SessionManager $session, Repository $config) {
        $this->session = $session;
        $this->config = $config;
    }

Akkor nézzük eddig mi is történt. Kielégítjük a konstruktort, amit meghatároztunk fentebb a providerben, valamint felvettünk pár változót, hogy a sok stringben ne keveredjünk el. Igen, implementáltuk az Illuminate\Contracts\Auth\Guard interfészt, tehát azokat a metódusokat meg kell valósítanunk.
/**
 * Validate a user's credentials.
 *
 * @param  array $credentials
 * @return bool
 */
public function validate(array $credentials = [])
{
    throw new NotImplementedException();
}

Az egyik ilyennel viszont rögtön kivételt dobatunk, mivel mi nem fogunk foglalkozni jelszavakkal.
/**
 * Set the current user.
 *
 * @param  \Illuminate\Contracts\Auth\Authenticatable $user
 * @return void
 */
public function setUser(Authenticatable $user)
{
    $this->session->put($this->OAUTH_USER_SESSION_KEY, $user);
}

Ez ahhoz lesz majd szükséges, hogy beállítsuk a user-t valami perzisztens helyre, itt szimplán az adott felhasználó sessionjében elhelyezzük szerializálva. Na de nézzük csak azt a logint!
public function login($code) {
    $tokenResponse = $this->getTokenResponse($code);

    $http = new Client();
    $response = $http->get($this->config->get("oauth.user_url"), [
        "headers" => [
            "Authorization" => "Bearer ". $tokenResponse->access_token
        ]
    ]);

    $userArray = json_decode((string)$response->getBody(), true);
    $user = new \App\User($userArray);
    $this->setCredentials($tokenResponse->access_token, $tokenResponse->refresh_token, $tokenResponse->expires_in);
    $this->setUser($user);
}

Láthatjuk, hogy első körben kimegyünk és a callbackben kapott kódot kicseréljük tokenekre. Ezután meglátogatjuk a user URL-t, ami szimplán lekérdezi a userünk alap információit. Ezt passport oldalon kell megírni, nem bonyolult:
Route::get('/me', function (Request $request) {
    return $request->user();
})->middleware('auth:api');

Ez ugye visszaadja a usert, amit a korábbi metódussal be is állítunk. A setCredentials beállítja sessionben a tokeneket és a lejárati időt is. Ha valami cache storeban tároljuk és nem sessionben, akkor nem szükséges a lejárati időt beállítani külön, hanem használhatjuk a tokenek esetében. Nézzük csak mi is történik ebben a getTokenResponse-ban!
public function getTokenResponse($code)
{
    $http = new Client();
    $response = $http->post($this->config->get("oauth.token_url"), [
        'form_params' => [
            'grant_type' => 'authorization_code',
            'client_id' => $this->config->get("oauth.client_id"),
            'client_secret' => $this->config->get("oauth.client_secret"),
            'redirect_uri' => $this->config->get("oauth.redirect_uri"),
            'code' => $code,
        ],
    ]);
    $responeObject = json_decode((string)$response->getBody());
    return $responeObject;
}

A helyzet ugyanaz, elmegyünk egy POST kéréssel a token url-re az authorization_code-al és kapunk cserébe tokeneket, amiket stdClass formájában visszaadunk.
private function setCredentials($accessToken, $refreshToken, $expires) {
    $this->session->put($this->ACCESS_TOKEN_SESSION_KEY, $accessToken);
    $this->session->put($this->REFRESH_TOKEN_SESSION_KEY, $refreshToken);
    $this->session->put($this->EXPIRE_DATE_SESSION_KEY, Carbon::now()->addSeconds($expires));
}

Itt állítjuk be a sessionbe az egyes kulcsok alá a tokeneket és a lejárati időt Carbon segítségével. Akkor jöjjön az egyik legfontosabb metódus, a check. Ez hívódik meg mikor arra kíváncsi a middleware, hogy tényleg be vagyunk-e jelentkezve:
/**
 * Determine if the current user is authenticated.
 *
 * @return bool
 */
public function check()
{
    return ($this->hasAccessToken() && $this->hasUser());
}

Bizony, megnézzük, hogy a sessionben szerepel-e a tokent, valamint hogy szerepel-e a user. Az utóbbi elég egyszerű:
public function hasUser() {
    return $this->session->has($this->OAUTH_USER_SESSION_KEY);
}

public function hasAccessToken() {
    if ($this->session->has($this->ACCESS_TOKEN_SESSION_KEY)) {
        if (Carbon::now() > $this->session->get($this->EXPIRE_DATE_SESSION_KEY)) {
            return $this->refreshToken();
        } else
            return true;
    } else
        return false;
}

Ha a tokent vizsgáljuk, akkor picit más a helyzet. Ellenőrízzuk, hogy egyáltalán megtalálható-e a sessionben, ha nem, akkor nincs token.

Gondolom lassan eljutunk ide:



Ha mégis, akkor megvizsgáljuk, hogy lejárt-e az a token. Ha nem járt le, akkor minden frankó. Ha viszont lejárt, akkor megpróbáljuk frissíteni azt a kapott refresh-tokennel és visszatérni azzal, hogy sikerült-e vagy sem.
public function refreshToken() {
    $responseObject = $this->getRefreshTokenResponse();
    if ($responseObject->getStatusCode() !== 200) {
        return false;
    }
    $tokenResponse = json_decode((string)$responseObject->getBody());
    $this->setCredentials($tokenResponse->access_token, $tokenResponse->refresh_token, $tokenResponse->expires_in);
    return true;
}

A refreshTokent hasonlóan állítjuk elő, minimálisan módosul a POST kérés az Oauth felé. Ha 200-as válasszal tér vissza, akkor a loginhoz hasonlóan mindent szépen beállítunk, ha viszont nem akkor false-al térünk vissza, tehát a check is azt látja majd, hogy biza itt nincs token.
public function getRefreshTokenResponse()
{
    $http = new Client();
    $tokenResponse = $http->post($this->config->get("oauth.token_url"), [
        'form_params' => [
            'grant_type' => 'refresh_token',
            'client_id' => $this->config->get("oauth.client_id"),
            'client_secret' => $this->config->get("oauth.client_secret"),
            'redirect_uri' => $this->config->get("oauth.redirect_uri"),
            'refresh_token' => $this->session->get("refreshToken"),
        ],
        "exceptions" => false
    ]);
    return $tokenResponse;
}

Látható, hogy itt nem code-ot, hanem refresh_tokent adunk át és ennek az eredményével térünk vissza. Az egész folyamat kb. így néz ki:



Az interfész viszont megkövetel még pár metódust:
/**
 * Determine if the current user is a guest.
 *
 * @return bool
 */
public function guest()
{
    return !$this->check();
}

Ez igazából a fordítottja a check-nek, arra vagyunk kíváncsiak, hogy az aktuális felhasználó vendégként van-e jelen.
/**
 * Get the currently authenticated user.
 *
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 */
public function user()
{
    return $this->session->get($this->OAUTH_USER_SESSION_KEY);
}

Ez a metódus adja vissza a bejelentkezett felhasználót, ez utóbbi pedig annak az ID-ját:
/**
 * Get the ID for the currently authenticated user.
 *
 * @return int|null
 */
public function id()
{
    return $this->session->get($this->OAUTH_USER_SESSION_KEY)->id;
}

Na most eljutottunk oda, hogyha belépünk, akkor rendesen megkapjuk a tokenünket és egy middleware-el megoldható az authentikáció. Nézzük meg, hogy mit is tudunk kezdeni mindezzel:
/**
 * Returns the currently authenticated users token
 * @return string
 */
public function getToken() {
    return $this->session->get($this->ACCESS_TOKEN_SESSION_KEY);
}

Adjunk hozzá egy új metódust a Guardunkhoz, ezáltal könnyedén hozzáférünk az access_tokenünkhöz a későbbiekben. De vajon tényleg jól jelentkeztünk be? Tényleg minket lát a Laravel? Derítsük ki!
Route::get('/', function () {
})->middleware("guest", "auth:web");

Route::get("/home", function(\Illuminate\Contracts\Auth\Guard $auth) {
    return $auth->user();
})->middleware("auth:web");

Vegyük fel a két fenti route-ot. Az első ugye átirányít a /home-ra, ha be vagyunk jelentkezve. Mindekettő redirektál a passportos URL-re, amit korábban megadtunk, ha nem vagyunk bejelentkezve. A /home pedig nem csinál mást, mint a Guardunktól kikéri az épp bejelentkezett usert és kiírja JSON formátumban. Nézzük:



Tádám, működik! Tehát, hogy teljesen tiszta legyen:

Mikor odanavigálunk a kliensünk gyökerében a /-re. Akkor látja, hogy nem vagyunk bejelentkezve és átirányít minket a passportos URL-re. Ott belépünk, az pedig visszairányít minket az itteni /callback-re, a szükséges kóddal, amit a Guard fel is használ és lecseréli egy tokenre, valamint lekéri a userünk alap adatait és letárolja őket sessionben. Ezután redirektál minket a /home URL-re, ami ellenőrzi, hogy be vagyunk-e lépve. Ha igen, akkor JSON-ben kiírja az imént kapott userünket. Sima ügy, ugye?

A kód itt található.

Mivel nem terveztem, hogy implementációt is írok a klienshez, lévén vannak hasonlók, talán még jobbak is (max nem egyszerűbbek), ezért a scope-okat egy következő cikkben próbáljuk ki, szintén Passport segítségével!

Hozzászólások betöltése
2014-2017 © Letscode.hu. Minden jog fenntartva. Build verzió: 1.2.37

Mik azok a sütik?


As is common practice with almost all professional websites this site uses cookies, which are tiny files that are downloaded to your computer, to improve your experience. This page describes what information they gather, how we use it and why we sometimes need to store these cookies. We will also share how you can prevent these cookies from being stored however this may downgrade or 'break' certain elements of the sites functionality.

Áltálnos információkat nyújthat ez a Wikipedia cikk a HTTP sütikről...

Hogy használjuk a sütiket?


We use cookies for a variety of reasons detailed below. Unfortunately is most cases there are no industry standard options for disabling cookies without completely disabling the functionality and features they add to this site. It is recommended that you leave on all cookies if you are not sure whether you need them or not in case they are used to provide a service that you use.

Sütik kikapcsolása


You can prevent the setting of cookies by adjusting the settings on your browser (see your browser Help for how to do this). Be aware that disabling cookies will affect the functionality of this and many other websites that you visit. Disabling cookies will usually result in also disabling certain functionality and features of the this site. Therefore it is recommended that you do not disable cookies.

A sütik amiket mi használunk


This site offers newsletter or email subscription services and cookies may be used to remember if you are already registered and whether to show certain notifications which might only be valid to subscribed/unsubscribed users.

In order to provide you with a great experience on this site we provide the functionality to set your preferences for how this site runs when you use it. In order to remember your preferences we need to set cookies so that this information can be called whenever you interact with a page is affected by your preferences.


Harmadik féltől származó sütik


In some special cases we also use cookies provided by trusted third parties. The following section details which third party cookies you might encounter through this site.

This site uses Google Analytics which is one of the most widespread and trusted analytics solution on the web for helping us to understand how you use the site and ways that we can improve your experience. These cookies may track things such as how long you spend on the site and the pages that you visit so we can continue to produce engaging content.

For more information on Google Analytics cookies, see the official Google Analytics page.

The Google AdSense service we use to serve advertising uses a DoubleClick cookie to serve more relevant ads across the web and limit the number of times that a given ad is shown to you.

For more information on Google AdSense see the official Google AdSense privacy FAQ.

We also use social media buttons and/or plugins on this site that allow you to connect with your social network in various ways. For these to work the following social media sites including; Facebook, will set cookies through our site which may be used to enhance your profile on their site or contribute to the data they hold for various purposes outlined in their respective privacy policies.


More Information


Hopefully that has clarified things for you and as was previously mentioned if there is something that you aren't sure whether you need or not it's usually safer to leave cookies enabled in case it does interact with one of the features you use on our site. However if you are still looking for more information then you can contact us through one of our preferred contact methods.

Email: fejlesztes@letscode.hu

Bezárás