Oauth2 strežnik in njegova izvedba v PHP (Laravel)


Sama implementacija Oauth2 strežnika ob pestri ponudbi odprtokodnih knjižnic, ki sam proces precej olajšajo, načeloma ne predstavlja težav. Če pa že ima razvijalec težave, pa so te po mojem mnenju bolj posledica nerazumevanja Oauth2 protokola ter njegovega delovanja.

Komunikacija pri oauth2 protokolu

  1. Odjemalec naslovi zahtevo na avtorizacijski strežnik (authorization server). V zahtevku mora biti podan ID odjemalca, URL za preusmeritev ter tip odgovora.
  2. https://oauth2server.com/auth?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI

  3. Strežnik prikaže obrazec, kjer uporabnik potrdi ali zavrne dostop do podatkov.
  4. Strežnik procesira obrazec glede na uporabnikov odgovor ter pošlje odgovor odjemalcu v obliki naslova URL. Ob pritrditvi odgovor ne vsebuje žetona za dostop, ampak le avtorizacijsko kodo, s katero lahko odjemalec zahteva žeton.
  5. https://registered-redirect-url.com/cb?code=AUTH_CODE_HERE

  6. Odjemalec z avtorizacijsko kodo zahteva žeton (zahteva POST). Pri tem mora podati še vrsto drugih parametrov (pazi na response_type/grant_type).
  7. POST https://api.oauth2server.com/token
    grant_type=authorization_code&
    
    code=AUTH_CODE_HERE&
    redirect_uri=REDIRECT_URI&
    client_id=CLIENT_ID&
    client_secret=CLIENT_SECRET
  8. Ko ga dobi, lahko z njim zahteva podatke na strežniku, ki vsebuje vire (resource server).
Diagram poteka komunikacije pri Oauth2
Diagram potek komunikacija (avtorizacijski strežnik in strežnik vira sta pogosto združena v enem strežniku).

Implementacija

Sam sem prejšnji teden prvič postavljal Oauth2 strežnik. Pri implementaciji sem uporabil Laravel in knjižnico lucadegasperi/laravel-oauth2-server (slednji temelji na league/oauth2-server), ki opravi večino dela – implementacije vmesnikov, ki jih zahteva knjižnica, na kateri temelji, filtrov, migracij. Potrebni koraki:

  1. Prenos knjižnice in njegove integracije v Laravel 5: wiki
  2. Nato je potrebno posodobiti konfiguracijsko datoteko config/oauth2.php (polje grant_types). Tudi o tem se navodila nahajajo na wiki-ju knjižnice.
  3. Preden odjemalca lahko sploh uporabljamo, ga je potrebno registrirati na avtorizacijskem strežniku (ime, id, skrivnost ter njegov URI naslov za preusmeritev) – v bazi se pišeta tabeli oauth_clients in oauth_client_endpoints. Preusmeritev je potrebno registrirati iz varnostnih razlogov.
  4. Definiramo krmilnik in poti za dostop do akcij, s katerimi:
    • pridobimo žeton za dostop do poti, ki zahtevajo oauth2 avtentikacijo,
    • prikažemo obrazec, v katerem uporabnik na zahtevo odjemalca potrdi ali zavrne dostop do svojih podatkov,
    • obdelamo omenjeni obrazec ter glede na odločitev uporabnika ustvarimo odgovor (kot parametri v url naslovu) ter preusmerimo na registrirani naslov URI odjemalca.
  5. Zavarujemo poti kjer se nahajajo podatki z oauth2 filtrom.

Primer registracije poti do krmilnika:

Route::controller('oauth', 'OauthController');

Primer krmilnika:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Guard;

use LucaDegasperi\OAuth2Server\Authorizer;

class OAuthController extends Controller
{
    protected $authorizer;

    public function __construct(Authorizer $authorizer)
    {
        $this->authorizer = $authorizer;

        $this->beforeFilter('auth', ['only' => ['getAuthorize', 'postAuthorize']]);
        $this->beforeFilter('csrf', ['only' => 'postAuthorize']);
        $this->beforeFilter('check-authorization-params', ['only' => ['getAuthorize', 'postAuthorize']]);
    }

    public function postAccessToken()
    {
         return response()->json($this->authorizer->issueAccessToken());
    }

    public function getAuthorize()
    {
        return view('oauth.authorize-form', $this->authorizer->getAuthCodeRequestParams());
    }

    public function postAuthorize(Request $request, Guard $auth)
    {
        $params['user_id'] = $auth->user()->id;

        $redirectUri = '';

        if ($request->has('approve')) {
            $redirectUri = $this->authorizer->issueAuthCode('user', $params['user_id'], $params);
        } else if ($request->has('deny')) {
            $redirectUri = $this->authorizer->authCodeRequestDeniedRedirectUri();
        }

        return redirect($redirectUri);
    }
}

Primer obrazca za uporabnikovo potrditev ali zavrnitev dostopa (podatki so poslani kot parametri v url-ju ter kot vsebina zahtevka v post):

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Aplikacija prosi za dostop do podatkov</title>
</head>
<body>

    <form action="/oauth/authorize?client_id=sQnOsjaZtZn951Ed&redirect_uri=http://localhost:8000/proxy&response_type=code" method="post">
        <input type="hidden" name="_token" value="{{ csrf_token() }}">
        <button type="submit" name="approve" value="1">Approve</button>
        <button type="submit" name="deny" value="1">Deny</button>
    </form>
</body>
</html>

(Podatki o odjemalcu, URI naslovu za preusmeritev ter tipu odgovora so zakodirani in jih je potrebno zamenjati z dejanskimi)

Pomembno je še poudariti, da v tem zapisu nisem obravnaval opcijska dovoljenja (scope), ki odjemalcu omejijo dostop do določenih virov. Ko zahtevamo avtorizacijsko kodo, moramo obstoječim podatkom pripeti še parameter scope, ki ima za vrednost z vejico ločene poljubne nize (ime dovoljen je lahko karkoli, npr. email, profil, pisanje, branje). Tako se bo ustvaril žeton, ki se bo ob dostopu do zavarovane končne točke od-šifriral in primerjal ali vsebuje zahtevana dovoljenja za dostop do te končne točke (končno točko zavarujemo s filtrom oauth2:scope1,scope2,..).