lunedì 4 marzo 2013

OAuth2: questo sconosciuto (per me)

Logo del progetto OAuth
Sebbene la specifica di OAuth2 sia ancora tutt'altro che priva di ambiguità, ci sono un sacco di servizi che già ne utilizzano svariate implementazioni (spesso arbitrarie). Tuttavia, se sui princìpi di funzionamento si trovano parecchi contributi esplicativi (ovviamente in lingua inglese), sulla implementazione al di fuori di contesti standardizzati c'è ben poco. Per non parlare di quando si cercano pezzi di codice funzionante, ad esempio, per applicazioni RESTful sotto Spring framework.
Recentemente mi è capitato di dover sviluppare una tale implementazione e, quindi, di dover combattere con la configurazione di OAuth2 per Spring e con la spesso astrusa orchestrazione delle varie request e redirects che la caratterizzano. Per di più mi sono trovato ad operare in un contesto di persistenza NoSQL (MongoDB). Non è stato per niente facile venirne fuori, il che mi ha fatto sentire spesso abbastanza scemo(frustrating). In questo ed in successivi post, mi riprometto pertanto di chiarire alcuni concetti ambigui di OAuth2 e di presentarvi un esempio funzionante from scratch di come implementare OAuth2 utilizzando Spring framework e Mongo Db. Vi mostrerò poi come ho utilizzato l'utility cURL per testare il tutto. Una serie di post molto interessanti sull'argomento (e da cui ho tratto spunto in più di una occasione) potete trovarli su questo blog http://techblog.hybris.com/. Il codice utilizzato, invece, potete trovarlo pubblicato nel mio spazio Github.

OAuth2: perché farsi del male?
La specifica di OAuth2, per quanto noiosa, andrebbe letta da cima a fondo. Come ogni documento architetturale ben fatto, questo paper (sebbene non sia perfetto) non solo chiarisce i dettagli del funzionamento del protocollo nei vari scenari possibili, ma soprattutto definisce in maniera univoca gli attori, i ruoli e le motivazioni che hanno spinto alla formalizzazione della specifica stessa. In questa sede non ci interessa entrare in tali dettagli, quanto piuttosto riassumerne i punti chiave per permettere a chi si trova nella condizione di dover implementare il protocollo in un contesto applicativo di comprenderne il funzionamento e di risolvere eventuali problemi che dovessero presentarsi.
Innanzitutto perché OAuth2? Diciamo subito che non esiste un'unica ragione. Nella specifica si punta il dito sulle limitazioni del tradizionale modello client-server quando un resource-owner (ossia una entità che possiede dei dati ad accesso controllato) voglia condividere ad applicazioni di terze parti l'accesso alle proprie risorse. Normalmente ciò implica la condivisione di una password, sebbene sia chiaro che questo non sia esattamente il metodo più sicuro di proteggere i dati. In secondo luogo poi, il resource-owner non ha alcun modo per limitare l'accesso ad un solo sottoinsieme di dati o restringerne la durata nel tempo. Inoltre, l'unico modo che ha di impedire che una certa applicazione acceda alle risorse è quello di cambiare la password, il che inibisce l'accesso a tutti i client. Infine, l'intero sistema risulta vulnerabile in quanto basta che una sola delle applicazioni client sia poco sicura per compromettere la sicurezza dei dati e di tutte le altre applicazioni. OAuth2 fa in modo di disaccoppiare il ruolo del client da quello del resource owner ed introduce fra i due un authorization server. Ciò fa in modo che il client ottenga delle credenziali di accesso ai dati che sono diverse da quelle offerte dal resource owner. Ciò che accade è che il client ottiene dall'authorization server un access token, cioè una stringa che contiene attributi che definiscono lo scope, la durata ed altre modalità di accesso ai dati. Una volta ottenuto il token, il client lo usa per accedere ai dati senza ulteriori filtri con le modalità definite dal token stesso.
Nella figura sottostante (presa direttamente dalla specifica) si mostra il flusso astratto del protocollo OAuth2.

     +--------+                               +---------------+
     |        |--(1)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(2)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(3)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(4)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(5)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(6)--- Protected Resource ---|               |
     +--------+                               +---------------+

                     Abstract Protocol Flow


Come si vede, sono previsti i seguenti step:

  1. Il client richiede l'autorizzazione dal resource owner;
  2. Il client riceve dal resource owner un authorization grant che rappresenta l'autorizzazione del resource owner. La specifica definisce quattro grant possibili: authorization code, implicit, resource owner password credentials e client credentials. Ci soffermeremo sulla natura di questi grants nelle singole implementazioni. Per il momento basti sapere che l'authorization grant dipende dalle modalità usate dal client per richiedere l'autorizzazione e dai tipi di autorizzazione supportati dall'authorization server.
  3. Il client usa l'authorization grant ottenuto nello step precedente per richiedere un access token all'authorization server.
  4. L'authorization server autentica il client, valida l'authorization grant e, se valido, rilascia un access token.
  5. Il client richiede la risorsa al resource server presentando l'access token.
  6. Il resource server valida il token e, se valido, serve le risorse richieste.
C'è da dire che questo è un flusso astratto nel senso che mette in evidenza tutti i ruoli previsti dalla specifica. Tuttavia, nei vari flussi possibili, come si vedrà nel seguito, tali ruoli possono essere accorpati in singoli attori o rimanere separati ed essere svolti da entità logicamente diverse. Ad esempio, si vedrà che il client utilizza l'authorization server come intermediario verso il resource owner per ottenere l'authorization grant.


Un solo protocollo, quattro flussi
Perché OAuth2, a fronte di un unico flusso astratto, prevede ben quattro possibili implementazioni per i flussi di utilizzo? Una possibile ragione sta nel fatto che oggi, più che mai in passato, esistono svariate tipologie di applicazioni e di client. Vi basti pensare al boom dei dispositivi mobile per farvi un'idea. Alcuni dei flussi previsti dalla specifica si addicono maggiormente ad applicazioni web server-side "classiche", altri vanno meglio nel caso si abbiano dei client mobile altri ancora si prestano ad utilizzi misti e mirabolanti. Ovviamente, nessuno obbliga gli sviluppatori e le aziende ad utilizzare implementazioni di OAuth2 già esistenti (come quella di Spring cui accennavo prima, o anche CXF della Apache Software Foundation). In linea di principio è possibile sviluppare una propria implementazione partendo direttamente dalle specifiche (dotandosi di una buona dose di caffeina, per non dire altro). Tuttavia una cosa simile la si farebbe a rischio e pericolo della organizzazione per cui si lavora e dei suoi utenti. Perché, e non dimentichiamolo mai, la ragione principale per cui è nato OAuth risiede nella sicurezza dei dati che tramite API si intendono esporre ai client. Una implementazione non standard della specifica (e quindi non certificata) potrebbe essere buggata, essere poco affidabile e facilmente forzata da utenti malintenzionati. Le implementazioni standard invece sono spesso soggette ad un'accurata analisi delle communities, continuamente testate e debuggate. Insomma, perché rischiare?
Vediamo quindi questi quattro flussi standard.
  1. Authorization Code Flow. Questo flusso è spesso indicato come il "tipico flusso OAuth2" ed è quello più indicato per le web applications server-side. In questo flusso, l'applicazione client viene redirezionata all'authorization endpoint del provider (ossia dell'attore che detiene le risorse). Qui avviene una autenticazione in due step. Nel primo step, l'utente, se non è già loggato (ossia se non dispone di un sessionId valido) deve fare un vero e proprio login con username e password. Nel secondo step, l'utente dovrà garantire l'accesso ai dati al particolare client.Questo è quanto avviene, ad esempio, quando create un nuovo account Twitter e vi viene chiesto di accedere ad i vostri contatti GMail per vedere quali fra questi dispone già di un account. Se non siete loggati a GMail, vi verrà chiesto di farlo e, successivamente, GMail vi chiederà qualcosa tipo: "volete permettere che l'applicazione Twitter acceda ai vostri dati?". Se accettate, opportunamente informati con appositi disclaimer e quindi pienamente consapevoli dei rischi, allora il flusso prosegue allo step successivo. Questo consiste in un redirect al client in cui il provider invia un codice (che in seguito potrà essere scambiato con un access token vero e proprio) ed un altro parametro di "stato", con funzioni di controllo sulla validità della sessione e sul cui utilizzo ci soffermeremo nel seguito. Nel prossimo step, il client invia il codice appena ricevuto e richiede al server un authorization token. A questa request, il provider risponde con l'authorization token vero e proprio, assieme ad altre informazioni come il periodo di validità del token espresso in ore ed un refresh token che serve a rinnovare un eventuale token scaduto senza dover rifare tutta la trafila.
  2. Flusso Implicito (Client-Side Flow). Questo flusso è più snello rispetto al precedente, ed è anche meno sicuro. Esso è però necessario laddove il client non abbia modo di poter conservare un token o un secret. Siamo cioè nel caso, ad esempio, di applicazioni che si riducono al codice JavaScript che viene interpretato in un browser. È evidente, in questo scenario, che il client non ha modo di poter conservare un segreto in maniera affidabile. Per questo tipo di applicazioni, tipicamente, non vi fidereste mai di rilasciare un token la cui validità è di giorni, quanto piuttosto di poche, pochissime ore! Questo perché voi, lato server, NON potrete mai sapere quanto bene sia stata realizzata l'applicazione dei client. Quanto sia resistente, ad esempio, ad attacchi tipo XSS (Cross-Site Scripting) che possano sottrarre il token. Non parliamo poi di dare al client addirittura la possibilità di fare il refresh di un token scaduto! In un tale contesto, il flusso precedente, con tutti i suoi scambi di codici e refresh token, diventa inutile e ridondante (oltre che potenzialmente pericoloso). Per questi client si adotta allora il flusso implicito. In cosa consiste? Dire "implicito" è come dire "sottinteso". Cosa esattamente è sottinteso in questo flusso rispetto al precedente? È presto detto: a differenza del precedente, in questo flusso il token viene fornito al client senza alcuna richiesta esplicita. Pertanto questa volta il client invia meno parametri al server (vedremo i dettagli in seguito) in quanto non è richiesta l'autenticazione del client. Anziché ritornare un authorization code, il server ritorna direttamente il token di accesso, nel fragment #hash dell'URI. A questo punto, il resto della logica avviene nel JavaScript all'interno del browser del client, che pertanto in questo flusso svolge il ruolo di user agent. Questo esegue una redirect al server senza inviare il fragment contenente il token. Il server, a questo punto, risponde con una pagina HTML5 in cui c'è il link ad una libreria JavaScript che consente allo user agent, cioè al browser, di estrarre l'access token dal fragment #hash. Il client adesso dispone del token per accedere alle risorse del provider.
  3. Resource Owner Password Flow. Su questo flusso è stato difficilissimo reperire dettagli implementativi. Per fare questa ricostruzione mi sono pertanto basato su poche fonti, tuttavia, all'atto pratico, l'implementazione sembra funzionare. Anche qui sono ben accette correzioni e contributi da parte di chi abbia una versione funzionante di questo flusso e desideri condividerla. Questo flusso è per parecchi versi molto semplice. Il client fornisce le sue credenziali e riceve in cambio un access token ed un refresh token. Si noti esplicitamente che questi sono forniti assieme. A questo punto qualcuno che ha seguito meglio storcerà il naso. Come mai facciamo tanto per implementare OAuth e non dare le credenziali all'applicazione client e qui invece le inseriamo? Questa è una giusta osservazione e infatti nella specifica si dice chiaramente che questo flusso è da implementare quando si abbia una applicazione client che è a tutti gli effetti trusted. L'esempio tipico è quello dell'applicazione ufficiale di Spotify che accede al vostro db musicale. In questo caso voi, gli utenti di Spotify, siete i resource owner e accedete ai vostri dati, ospitati da Spotify, mediante l'applicazione ufficiale di Spotify. È ovvio che in questo scenario l'applicazione è fidata ed è l'unica che accede e quindi può bene accettare le credenziali in luogo del token. Da questa osservazione si capisce anche come mai, congiuntamente all'access token, venga anche fornito il refresh token. Infatti, se così non fosse, alla scadenza dell'access token, delle due una: o sarebbe richiesto nuovamente di inserire le credenziali o il client in qualche modo deve ricordarsele, cioè memorizzarle da qualche parte, con notevoli implicazioni dal punto di vista della sicurezza. Ciò detto, da un punto di vista funzionale, il flusso è molto semplice. Il client chiederà subito le credenziali e l'utente dovrà inserirle direttamente nell'applicazione. A questo punto è necessario che l'utente sia certo che l'applicazione client sia davvero quella ufficiale. Deve cioè scaricarla dal sito ufficiale, da un app store fidato, avere un certificato o un hash o un qualunque altro metodo. L'importante è che sia certo di potersi fidare dell'applicazione client e di potervi inserire le credenziali. A questo punto, l'app client fa una request all'authorization server inviando (nello header della request o, in alternativa, nel body) client_id, client_secret (trattate come username e password), le credenziali vere e proprie dell'utente, un grant_type settato al valore 'password' e la dicitura "Authorization:Basic" che dice all'authorization server di eseguire un'autorizzazione solo sulla base delle credenziali del client. A questo punto l'auth server risponderà inviando access token e refresh token e il client non avrà più bisogno di inserire le credenziali per accedere alle risorse protette.
  4. Client Credentials Flow. Questo flusso è assai simile al precedente e ricalca il "Two-Legged Flow" presente nella specifica OAuth1. In sostanza il flusso consiste di una singola request e response come il precedente. Solo che in questo caso si inviano solo client_id e client_secret. L'authorization server risponderà direttamente fornendo l'access token ma non in refresh token. Ciò implica che, allo scadere dell'access token, il client dovrà semplicemente richiederne uno nuovo fornendo client_id e client_secret.

Per quanto riguarda la descrizione del funzionamento di OAuth2, è tutto qui. Dal prossimo post cominceremo a vedere una prima implementazione. Ovviamente se ci sono osservazioni, correzioni o richieste di ulteriori precisazioni, vi prego di farlo attraverso i commenti o, se preferite, contattandomi privatamente.
Alla prossima!

2 commenti:

  1. Gran bel lavoro!! veramente utile per districarsi nella massa di informazioni su oAuth2.

    RispondiElimina

I commenti di questo blog sono moderati. Pensa prima di scrivere...