Vai al contenuto
Home » Costruiamo un sistema di autenticazione con Sinatra e Warden. Parte 2

Costruiamo un sistema di autenticazione con Sinatra e Warden. Parte 2

Nella scorsa
puntata

abbiamo analizzato il modello User di una semplice applicazione basata su
Sinatra e
Warden per realizzare un frontend di
autenticazione basato su Active Directory o comunque su un server LDAP.

In questa puntata vedremo il controller e le view.

Il file app.rb

Il nostro file app.rb conterrà i controller della nostra applicazione sinatra.
Definirà tutte le rotte che potranno essere richiamate dall’utente ed invocherà
il modello nel modo opportuno.

Esendo un’applicazione di esempio, ho lasciato la configurazione di DataMapper
in bella vista per gli ambienti di esercizio e di staging. Una mossa più furba
è quella di leggere, in esercizio e in staging o comunque in ambienti
condivisi, il valore del pathname da linea di comando al momento dello startuo
del server, oppure lo si può mettere in una variabile d’ambiente. La variabile
d’ambiente lascia comunque la possibilità di poter essere letta.

require 'bundler'
Bundler.require

require 'tilt/haml'
require 'json'

configure(:development) do
  DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/db/codice_insicuro_auth.db")
end
configure(:test) do
  DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/db/codice_insicuro_auth_test.db")
end
configure(:production) do
  DataMapper.setup(:default, "postgres://test:test@localhost/codice_insicuro_auth")
end
configure(:staging) do
  DataMapper.setup(:default, "postgres://test:test@localhost/codice_insicuro_auth_staging")
end

require File.join(File.dirname(__FILE__), 'lib', 'warden.rb')
require File.join(File.dirname(__FILE__), 'models', 'user.rb')

Di seguito all’inizializzazione di DataMapper troviamo la dichiarazione della
classe CodiceInsicuroAuth che poi andremo ad eseguire.
Diciamo a Sinatra di abilitare le sessioni, necessarie per la memorizzazione
lato server delle informazioni di chi ha fatto login. Creiamo poi un secret che
sarà utilizzato per la generazione del cookie di sessione.

Anche in questo caso, possiamo usare un secret hardcoded nel caso di
un’applicazione di test, tuttavia se molti hanno accesso a quel sorgente può
essere opportuno passare il secret durante lo startup come parametro. In questo
caso si beneficierebbe anche di una variabilità della logica con cui sono
costruiti i token man mano che l’applicazione viene riavviata.

In questo caso, il mio secret è stato generato usando questa funzione nel mio
.zshrc che legge dei dati da urandom e me li mette in un formato
intelleggibile.

function mkpw() { head /dev/urandom | uuencode -m - | sed -n 2p | cut -c1-${1:-10} }
class CodiceInsicuroAuth < Sinatra::Base
  enable :sessions
  register Sinatra::Flash
  set :session_secret, "QdckPg1tGaloOD2nvix3XAzfy3x73rSKOu8lJgWsCiwp7udBBudWeFmT/qSO/pP90htBCIDSQHvo"

Si passa poi alla configurazione di Warden e la definizione delle sue strategie
di autenticazione. Quello che voglio realizzare è il seguente:

  • in sviluppo l’applicazione dovrà usare la password, in formato
    bcrypt
  • in staging e produzione, si dovrà utilizzare LDAP

Sostanzialmente ho scelto questo approccio perché lo sviluppo è sul mio
portatile e ci lavoro durante gli spostamenti da e verso casa.

La configurazione di Warden prevede, oltre alla definizione di cosa deve essere
serializzato nella sessione memorizzata server side, l’id dell’utente in questo
caso, la definizione delle strategie e cosa fare in cado si mancata
autenticazione.

  if self.development?

    use Warden::Manager do |config|
      config.serialize_into_session{|user| user.id }
      config.serialize_from_session{|id| User.get(id) }

      config.scope_defaults :default,
        strategies: [:password],
        action: 'auth/unauthenticated'
      config.failure_app = self
    end
  else
    use Warden::Manager do |config|
      config.serialize_into_session{|user| user.id }
      config.serialize_from_session{|id| User.get(id) }

      config.scope_defaults :default,
        strategies: [:ldap, :password],
        action: 'auth/unauthenticated'
      config.failure_app = self
    end

  end

  Warden::Manager.before_failure do |env,opts|
    env['REQUEST_METHOD'] = 'POST'
  end

Il mio helper lib/warden.rb

Sinatra non ha il concetto di helper, come invece
hanno Padrino o Rails.
Nel caso servisse, l’approccio che mi piace di più è quello di creare una
directory /lib nell’alberatura della mia applicazione Sinatra e creare lì
i miei helper che poi andrò ad includere nell’applicazione principale (il
file app.rb).

In questo caso, ho creato un helper per Warden per definire le strategie e
crearmi alcune routine per recuperare l’utente corrente.

Queste routine sono state create solo per rendere il codice di controller e
viste più leggibile. Framework più complessi basati su Warden come
Devise hanno già questi helper pronti, per
progetti più complessi dove magari serve utilizzare Facebook e Twitter come
fonti autoritative per l’autenticazione usare Devise può essere la soluzione
più semplice.

def warden
  env['warden']
end

def authenticated?
  warden.authenticated?
end

def current_user
  return warden.user if authenticated?
  return nil unless authenticated?
end

Il piccolo stratega della login

Per Warden, una strategia è la descrizione di cosa deve fare una volta che si
vede arrivare una coppia di credenziali.

Warden, nella gestione dell’autenticazione, chiamerà almeno 2 routine che
devono essere necessariamente definite in una strategia:

  • valid?
  • authenticate!

Nella documentazione sul repository github di warden, ci sono molti altri
metodi che possono essere definiti, tuttavia questi due sono quelli obbligatori
e servono per decidere rispettivamente quando una coppia di credenziali è da
considerarsi passibile di essere analizzata e cosa fare per decidere se quella
coppia è valida, ovvero se la password è corretta.

In entrambe le nostre strategie, definiremo il metodo valid? andando a
verificare che nei parametri passati alla richiesta HTTP via POST, ci siano i
parametri relativi alla username ed alla password.

Perché non via GET?

Allora, ci sono tante cose che possono far accapponare la pelle in campo ICT
security, ma nessuna batte username e password (magari in chiaro) passate via
HTTP GET. Se ti stai chiedendo eh che male c’è? vai a dare un’occhiata al
file access.log di apache.

Strategia per le password

La strategia per autenticare la password è molto semplice. Prima ricerco il mio
utente nel database e se lo trovo chiamo il metodo authenticate della
classe User.

Se il metodo del mio modello restituisce true allora invoco
success! che dice a Warden di prendere l’oggetto user, passato come
parametro, serializzarlo e metterne l’id in sessione.

Se l’autenticazione fallisce, sollevo un’eccezione dando un messaggio laconico
d’errore.

Warden::Strategies.add(:password) do
  def valid?
    params['user'] && params['user']['username'] && params['user']['password']
  end

  def authenticate!
    user = User.first(username: params['user']['username'])

    if user.nil?
      throw(:warden, message: "The username and password combination ")
    elsif user.authenticate(params['user']['password'])
      success!(user)
    else
      throw(:warden, message: "The username and password combination ")
    end
  end
end

Perché un messaggio generico di errore?

Se sei tra le persone che amano spiegare per filo e per segno ad un utente, che
magari non ha molta dimestichezza con l’informatica, il perché ed il per come
le sue credenziali non vanno bene e quindi mandi a video messaggi come la
password che hai messo è troppo lunga, ti ricordi: solo 6 caratteri?
o no
guarda, la username è sbagliata ma questa è la password del mese scorso
o
ehi, ti sei scordato la password, schiaccia qui e te la rimando via mail su,
allora sappi che stai sbagliando tutto.

Messaggi troppo dettagliati possono permettere ad un attaccante di capire come
funziona il tuo meccanismo di autenticazione nel dettaglio, di enumerare gli
utenti validi ed in alcuni casi di bypassare il controllo sulla password.

Per chi poi argomenta che molti utenti trovano questi messaggi difficili perché
magari non si ricordano la password, considera che:

  • la porta blindata di casa tua ha una chiave. Baratteresti quella chiave con
    un sistema che ti fa aprire la porta bussando 3 volte di seguito? Vuoi entrare?
    Usa la chiave.
  • se i tuoi utenti usano talmente poco il tuo sito da dimenticarsi la password,
    tu l’engagement dei tuoi clienti l’hai già perso e non sarà mettendo a
    repentaglio la sicurezza degli altri clienti affezionati che recupererai
    qualche manciata di click.

La strategia per LDAP

La strategia per LDAP è praticamente identica a quella per le password;
cambia solo il metodo del nostro modello User che deve essere invocato per
autenticare l’utente.

Warden::Strategies.add(:ldap) do
  def valid?
    params['user'] && params['user']['username'] && params['user']['password']
  end

  def authenticate!
    user = User.first_or_create(username: params['user']['username'])

    if user.nil?
      throw(:warden, message: "The username and password combination ")
    elsif user.ldap_authenticate(File.join(File.dirname(__FILE__), '..', 'config', 'ldap.yml'), params['user']['password'])
      success!(user)
    else
      throw(:warden, message: "The username and password combination ")
    end
  end
end

Off by one

In questo post abbiamo visto il cuore del nostro meccanismo di autenticazione.
Abbiamo definito le strategie di Warden e collegato il framework con il nostro
modello User.

Nella prossima puntata andremo a vedere nel dettaglio i controller e le viste
che daranno un tocco grafico alla nostra piccola applicazione web.

Tag:

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *