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.