Implementace přihlašování pomocí OpenID

Je implementace OpenID těžká? Na praktickém příkladu si ukážeme, že přihlašování pomocí OpenID lze implementovat snadno. Nebudeme zabíhat do podrobností ani probírat implementační detaily; pouze ukážeme, že v té nejjednodušší podobě je mezi webem a webem s podporou OpenID rozdíl pouhých několika desítek minut.

Seriál: Moderní internetové autentizační metody (7 dílů)

  1. Moderní internetové autentizační metody 19.12.2008
  2. Porovnání moderních autentizačních metod 23.12.2008
  3. OpenID: Historie, terminologie a mechanismus autentizace 30.12.2008
  4. Implementace přihlašování pomocí OpenID 6.1.2009
  5. OpenID: Identity, aliasy a vlastní poskytovatel 13.1.2009
  6. Implementace přihlašování pomocí Live ID 20.1.2009
  7. Webové autentizační metody: Kuriozity a novinky 27.1.2009

Jedna z častých výhrad proti OpenID zní: „Já bych to třeba i nasadil, ale nechce se mi studovat hory manuálů a psát nějakou implementaci nějakého protokolu.“ U většiny protokolů nic takového dělat nemusíte, pokud vysloveně nechcete, namísto toho můžete použít hotové knihovny. U OpenID je tomu nejinak.

V následujícím textu odbočíme od šedivé teorie, kterou jsme se zabývali v předchozích dílech našeho seriálu o moderních autentizačních metodách, a ukážeme si jednoduchý příklad té nejrychlejší implementace OpenID přihlašování se standardní knihovnou od JanRain ze stránek OpenID Enabled.

Tradiční přihlášení

Představme si, že máme nějaký web, kam se mohou uživatelé registrovat a přihlašovat – diskusní fórum, blog, galerii fotografií, cokoli… Tradiční přihlášení bude vypadat třeba nějak takhle (login.php):

<?php
if (isset($_POST['login'])) {
  $jmeno = $_POST['jmeno'];
  $heslo = $_POST['heslo'];

  /**
   * Následuje ověření jména a hesla, např. proti databázi. Zde pro jednoduchost
   * zůstaneme u volání (blíže nedefinované) funkce "overeni()"
   */
  $spravne = overeni ($jmeno, $heslo);
  if ($spravne) {
    /**
     * Nějaké akce spojené se správným přihlášením - vytvoření session apod. Zde jen vypíšeme hlášení
     */
     print 'Prihlaseni v poradku, prihlasen jako '.$jmeno;
     die();
  } else {
     print 'Chyba';
     die();
  }
}

//dummy funkce overeni() pro testování - schválí kombinaci admin/nimda
function overeni($jmeno, $heslo) { return ($jmeno=='admin' && $heslo=='nimda'); }

?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="cs" xml:lang="cs">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Přihlášení</title>
  </head>
  <body>
  <form method="post" action="login.php">
  <fieldset><legend>Přihlášení jménem a heslem</legend>
  <p>
    <label for="jmeno">Jméno</label>
    <input type="text" id="jmeno" name="jmeno" />
  </p>
  <p>
    <label for="heslo">Heslo</label>
    <input type="password" id="heslo" name="heslo" />
  </p>
  <p><input type="submit" name="login" value="Přihlásit" /></p>
  </fieldset>

  </form>
  </body>
</html> 

Příklad je opravdu velmi zjednodušen, nijak v něm nejsou rozebírány detaily implementace – např. zapamatování přihlášeného uživatele přes session, nejsou řešené chybové stavy (jen vypisuje lakonické „chyba“), nejsou v něm ošetřené vstupy apod., protože to vše není v tuto chvíli podstatné. V praxi by nejspíš byl taky oddělen formulář od PHP skriptu. Pro tuto chvíli však stačí tenhle příklad, z něho vyjdeme.

Poznámka: Příklady z tohoto článku jsou umístěny na adrese http://misc.ma­ly.cz/openid/. Můžete si tedy rovnou zkoušet jejich chování „live“. Tento primitivní přihlašovací skript tedy najdete na adrese http://misc.ma­ly.cz/openid/lo­gin.php.

Připravujeme se na implementaci

Protože chceme implementovat přihlašování přes OpenID bez toho, abychom leželi mnoho dní v dokumentaci a pak týden psali a ladili vlastní implementaci, sáhneme po hotové OpenID knihovně. V době psaní článku je aktuální PHP verze 2.1.2 (dokumentace).

Stáhneme si ji (.zip – .tar.bz2) a rozbalíme. V archivu nalezneme kromě vlastní implementace OpenID (adresář Auth) i testovací skripty, dokumentaci, příklady a další soubory, kterými se v tuto chvíli nemusíme zabývat.

Požadavky knihovny

JanRain OpenID ke svému běhu potřebuje některé knihovny, jako jsou bcmath/gmp, cURL či podpora XML. Naštěstí je většina těchto knihoven standardně dostupná na většině instalací PHP, a pokud nejsou, tak je dokáže OpenID implementace obejít. To, jestli je vaše instalace vhodná či zda nastanou nějaké problémy, si můžete ověřit snadno: Nakopírujte soubor „detect.php“ z adresáře examples na váš server a spusťte ho. Skript otestuje prostředí a vypíše informace o tom, zda je vyhovující. (Ukázka – server plně dostačuje, doporučuje se instalace gmp pro zrychlení matematických operací.)

Nejčastějším problémem, se kterým jsem se setkal, byla neexistence zdroje náhodných čísel ve Windows. Naštěstí s tím knihovna počítá a nabízí řešení, spočívající v zapsání direktivy define('Auth_OpenID_RAND_SOURCE', null); před voláním OpenID knihoven.

Příprava prostředí

OpenID knihovna potřebuje prostor, kam si bude ukládat potřebné informace, např. komunikační klíče pro jednotlivé servery nebo informace o uživatelích. Máme na výběr z několika možností – buď databáze (MySQL, SQLite, pgSQL) nebo soubor na disku. My zůstaneme v našem příkladu u té jednodušší možnosti, tedy u souborů v adresáři na disku. Budeme k tomu potřebovat pouze adresář na serveru, do něhož bude smět skript zapisovat (adresář nesmí být zvenčí viditelný, takže jej buď umístěte mimo strom dokumentů, nebo alespoň zakažte přístup přes .htaccess).

Na serveru si vytvoříme adresář „cache“ a nastavíme mu patřičná práva pro zápis. Pokud bude viditelný zvenčí, umístíme do něj soubor .htaccess, který obsahuje obligátní:

order deny,allow
deny from all 

Na server zkopírujeme rovněž celý adresář Auth.

Test funkčnosti

V tuto chvíli můžeme takříkajíc „zkusit štěstí“, nahrát na server i adresář examples a zkusit si zavolat skript /examples/con­sumer/index.php. Pokud vám výše zmíněný test pomocí „detect.php“ neukázal žádné chyby, máte velkou pravděpodobnost, že se vám ukáže testovací formulář:

PHP OpenId Authentication Example

Pokud se neukázal, tak příčiny bývají nejčastěji tyto:

  • Chybně nastavená práva k adresáři examples/consu­mer/_php_consu­mer_test
  • Spuštění na Windows bez direktivy Auth_OpenID_RAN­D_SOURCE (nutno přidat do examples/consu­mer/common.php)
  • Špatně rozbalený adresář Auth (např. změněno při přenosu na „auth“)

Pokud není příčinou nic z nich, je třeba trochu hledat – naštěstí jsou ladicí výstupy celkem srozumitelné a člověka navedou.

Pokud se testovací formulář ukáže, zkuste si zadat své OpenID a nechat své přihlášení ověřit. Ověření by mělo proběhnout bez problémů. Problém se může vyskytnout, pokud používáte Firefox, máte nainstalované rozšíření NoScript a zapnutou CSRF ochranu – v takovém případě můžete u některých OpenID providerů dostat chybu Bad signature, i když je přihlášení v pořádku. Příčinou je drobná chyba v ukázkovém příkladu.

Implementujeme OpenID

Knihovnu máme na serveru v adresáři Auth, ověřili jsme si, že funguje, takže můžeme přistoupit k vlastní implementaci. Nejprve přidáme do naší přihlašovací stránky další formulář pro přihlášení přes OpenID. Tentokrát si vystačíme s jediným textovým polem a přesměrujeme zpracování na jiný skript (jen kvůli přehlednosti, požadavek by mohl klidně zpracovávat stejný skript):

<!-- OpenID formulář -->
<form method="post" action="login2-openid.php">
<fieldset><legend>Přihlášení přes OpenID</legend>
<p>
  <label for="openid">OpenID</label>
  <input type="text" id="openid" name="openid" />
</p>
<p><input type="submit" name="login" value="Přihlásit" /></p>
</fieldset>
</form> 

Pokud nepotřebujeme od uživatelů nic víc než jejich uživatelské jméno (kvůli identifikaci) a (třeba) e-mail a jméno, které se jim má zobrazit, tak se obejdeme zcela bez registračního skriptu. O tyto údaje totiž můžeme požádat přímo OpenID providera a dostaneme je při úspěšném přihlášení. Můžeme pak vytvořit registraci „na pozadí“ (pokud ji z nějakého důvodu potřebujeme, např. když chceme, aby každý uživatel měl záznam v databázi a svoje ID), nebo ji nemusíme zakládat vůbec.

OpenId formulář

Discovery a přesměrování na providera

Dalším krokem je vytvoření skriptu, který provede požadované akce a přesměruje uživatele na stránky jeho poskytovatele OpenID (viz minulý článek o mechanismu autentizace, kroky 2–4). OpenID knihovna naštěstí většinu operací dělá za nás, takže skript login2-openid.php může být opravdu jednoduchý (čísla kroků pochází z popisu v minulém díle):

<?php
//Odkomentujte následující řádek, pokud má skript běžet na Windows
//define('Auth_OpenID_RAND_SOURCE', null);

require_once "./Auth/OpenID/Consumer.php";
require_once "./Auth/OpenID/FileStore.php";
require_once "./Auth/OpenID/SReg.php";

session_start();

$trust_root = 'http://misc.maly.cz/openid/';
$return_to = $trust_root . 'login2-openid-finish.php';

    //Cesta k úložišti
    $store_path = "./cache";
    $store = new Auth_OpenID_FileStore($store_path);
    $consumer = new Auth_OpenID_Consumer($store);

    $openid = $_POST['openid'];

    // Kroky 2 - Discovery a 3 (na pozadí)
    $auth_request = $consumer->begin($openid);

    if (!$auth_request) {
        die("Neni platne OpenID");
    }

    /**
     * Požádáme o údaje ze SimpleRegister
     * Povinně budeme požadovat přezdívku,
     * volitelně pak skutečné jméno a e-mail
     */
    $sreg_request = Auth_OpenID_SRegRequest::build(
                    array('nickname'),
                    array('fullname', 'email'));

    if ($sreg_request) {
        $auth_request->addExtension($sreg_request);
    }

    // Krok 4 - přesměrování na server OpenID poskytovatele
    if ($auth_request->shouldSendRedirect()) {
      //Krátký požadavek a požadavky OpenID 1 jsou přesměrovány přes HTTP redirekt
        $redirect_url = $auth_request->redirectURL($trust_root, $return_to);
        if (Auth_OpenID::isFailure($redirect_url)) {
            print("Chyba pri presmerovani: " . $redirect_url->message);
            die();
        } else {
            header("Location: ".$redirect_url);
            die();
        }
    } else {
        // Dlouhé OpenID 2 požadavky - vygenerujeme formulář a JavaScript jej odešle jako POST
        $form_id = 'openid_message';
        $form_html = $auth_request->htmlMarkup($trust_root, $return_to, false, array('id' => $form_id));

        if (Auth_OpenID::isFailure($form_html)) {
            print("Chyba pri presmerovani: " . $redirect_url->message);
            die();
        } else {
            print $form_html;
        }
    }
?> 

Ve skriptu jsou nejprve načteny potřebné soubory, tedy OpenID klient (consumer), FileStore (třída pro ukládání pracovních souborů na disk) a SReg (třída pro práci s rozšířením Simple Registration). Následuje nastavení dvou důležitých údajů, a to jednak adresy, pro niž je povolení požadováno (typicky URL serveru nebo jeho podčásti – v našem případě to je http://misc.ma­ly.cz/openid/), jednak návratové adresy (u nás to bude login2-openid-finish.php). Po nezbytných inicializacích úložiště a klienta je spuštěn proces dle kroků 2 a 3, a to voláním $consumer->begin($openid);

Výsledkem volání této funkce je objekt typu AuthRequest – samosebou pokud bylo volání úspěšné a OpenID identifikátor prošel zjišťovacím procesem (discovery). Tento objekt má metody, jimiž můžeme přidat požadavky na rozšíření (použili jsme rozšíření Simple Registration, budeme požadovat přezdívku a volitelně e-mail a plné jméno). Pak už jen přesměrujeme uživatele na stránky poskytovatele – OpenID 2 zavedl možnost přesměrování dlouhých požadavků jako POST, a i na to knihovna myslí, my se musíme jen správně rozhodnout, zda použít HTTP nebo zda odeslat formulář JavaScriptem.

Přihlašování přes MyOpenID

Opět pro jednoduchost nejsou ve skriptu ošetřeny chybové stavy, chyba je pouze vypsána a skript zastaven. Ošetření těchto stavů je u slušné aplikace samosebou nutné, ale zde je mimo záběr tohoto článku.

Návratový skript

Návratový skript (login2-openid-finish.php) je místo, kam se uživatel vrátí od OpenID providera. V tomto skriptu zbývá jen ověřit, zda celý proces dopadl dobře (opět jedním voláním jedné metody objektu Consumer) a podle výsledku s uživatelem naložit.

<?php
//Odkomentujte následující řádek, pokud má skript běžet na Windows
//define('Auth_OpenID_RAND_SOURCE', null);

require_once "./Auth/OpenID/Consumer.php";
require_once "./Auth/OpenID/FileStore.php";
require_once "./Auth/OpenID/SReg.php";

session_start();

$trust_root = 'http://misc.maly.cz/openid/';
$return_to = $trust_root . 'login2-openid-finish.php';

//Cesta k úložišti
$store_path = "./cache";
$store = new Auth_OpenID_FileStore($store_path);
$consumer = new Auth_OpenID_Consumer($store);

// Dokončení - krok 7
$response = $consumer->complete($return_to);

//Ověříme výsledek z $response->status
if ($response->status == Auth_OpenID_CANCEL) {
    $msg = 'Autentizace zrusena.';
} else if ($response->status == Auth_OpenID_FAILURE) {
    $msg = "Autentizace selhala: " . $response->message;
} else if ($response->status == Auth_OpenID_SUCCESS) {
    // Autentizace v poradku
    $openid = $response->getDisplayIdentifier();
    $esc_identity = htmlspecialchars($openid);

    $success = sprintf('V poradku overen jako ' .
                       '<a href="%s">%s</a>.',
                       $esc_identity, $esc_identity);

    if ($response->endpoint->canonicalID) {
        $escaped_canonicalID = htmlspecialchars($response->endpoint->canonicalID);
        $success .= '  (XRI CanonicalID: '.$escaped_canonicalID.') ';
    }

    //Získání informací z rozšíření Simple Registration
    $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse($response);
    $sreg = $sreg_resp->contents();
    if (@$sreg['email']) {
        $success .= "  Mail: '".htmlspecialchars($sreg['email']) . "'";
    }
    if (@$sreg['nickname']) {
        $success .= "  Prezdivka: '".htmlspecialchars($sreg['nickname']) . "'";
    }
    if (@$sreg['fullname']) {
        $success .= "  Jmeno: '".htmlspecialchars($sreg['fullname']) . "'";
    }
  //Přihlášení úspěšné... vypíšeme hlášku a přesměrujeme uživatele do
  //zabezpečené oblasti
  print($success);
  die();
}
//Přihlášení selhalo.
print($msg);die();
?> 

Skript začíná stejně jako předchozí – načtením potřebných souborů a inicializací objektu Consumer. Následuje volání $consumer->complete($re­turn_to), které zajistí všechny potřebné operace – převezme parametry, ověří podpis a vyhodnotí celý proces. Výsledkem je objekt Response, který ve své vlastnosti status obsahuje informaci o tom, zda byla autentizace úspěšná. Pokud ano, můžeme si z tohoto objektu převzít i informace, předané rozšířením Simple Registration, a ty si uložit pro další potřebu.

V tomto bodu máme tedy k dispozici informace o přihlášeném uživateli, máme jeho „uživatelské jméno“ (= OpenID identita) a máme další informace, potřebné např. pro vytvoření uživatelského účtu. Další postup, jako např. vytvoření session, uložení informací či pohyb po webu, je do značné míry podobný oné „klasické“ metodě (OpenID identifikátor místo jména apod.) a nespadá už do tématu tohoto článku.

Nový přihlašovací skript s podporou OpenID si můžete rovněž vyzkoušet na adrese http://misc.ma­ly.cz/openid/lo­gin2.php.

Zdrojové kódy použité v tomto článku si můžete stáhnout: OpenID demo (ZIP 4554 bytů).

Závěr

Ukázali jsme si nejjednodušší a nejrychlejší způsob implementace OpenID přihlašování na web. Článek prosím neberte jako hotové instantní řešení ani jako všeobsahující tutoriál, který vám pomůže s každým aspektem implementace OpenID. Berte ho spíš jako studijní materiál, jako základ či naprosté minimum, od něhož se můžete odpíchnout. Sami vidíte, že implementace přihlášení pomocí OpenID není nijak složitá ani mysteriózní.

Téměř povinný disclaimer: Pro OpenID platí stejná bezpečnostní pravidla jako pro jakoukoli jinou technologii: Vybírejte si důvěryhodné poskytovatele, co můžete, to si ověřte více způsoby, a když použijete cizí knihovnu, tak ji pravidelně aktualizujte! Pokud považujete cizí knihovnu v systému za výraznou bezpečnostní díru a chystáte se rovnou celé OpenID zatratit, tak ještě předtím uvažte, že OpenID je otevřená technologie se solidní dokumentací, takže si samosebou můžete napsat vlastní implementaci OpenID klienta.

Implementovali jste už někdy OpenID?

Začal programovat v roce 1984 s programovatelnou kalkulačkou. Pokračoval k BASICu, assembleru Z80, Forthu, Pascalu, Céčku, dalším assemblerům, před časem v PHP a teď by rád neprogramoval a radši se věnoval starým počítačům.

Komentáře: 24

Přehled komentářů

danaketh RE: Implementace přihlašování pomocí OpenID
Kuka OpenID server ?
Martin Malý Re: OpenID server ?
Kuka Re: OpenID server ?
pan_tau gmail openId
Martin Malý Re: gmail openId
Christof Re: gmail openId
Martin Malý Re: gmail openId
Martin Malý Re: gmail openId
pan_tau Re: gmail openId
Andrew Re: gmail openId
Martin Malý Re: gmail openId
Andrew Re: gmail openId
Martin Malý Re: gmail openId
burlog Re: gmail openId
v6ak Mail z OpenID: jen ze stejné domény
aprilchild RPXNow
Martin Malý Re: RPXNow
aprilchild Re: RPXNow
kluvi Navrh na clanek
kluvi Re: Navrh na clanek
Watchick Problém
bum Registrace z identifikatoru openid.org
haff dirty fix php 5.3.0
Zdroj: https://www.zdrojak.cz/?p=2909