Una de las cosas más divertidas que me encontré para hacer durante el desarrollo de ttTodoMVC fue sin duda pensar en un esquema de autenticación stateless contra el servidor, pese a que mi primer idea fue enviar continuamente el nombre de usuario y contraseña por cada petición, luego de muy poca investigación entendí que no era lo optimo en lo más minimo, por lo que comencé a buscar alternativas hasta que me encontré con:
http://www.thebuzzmedia.com/designing-a-secure-rest-api-without-oauth-authentication/
que explica como funciona la autenticación en servicios tan utilizados como AWS (Amazon Web Services) como tal me resultó más que interesante y decidí implementar este método.
El Método
La forma de autenticación no es más que el viejo y conocido esquema de publicKey/privateKey donde existe una clave publica que puede ser conocida por cualquiera (y por tanto enviada en todas las peticiones, resistiendo spoofing) y una clave privada que una vez creada solo es conocida por los dos puntos de la comunicación y JAMAS es enviada durante la comunicación entre ambos puntos, ni siquiera aún cifrada. Entonces para confirmar la autenticidad de cada petición solo es necesario que las peticiones sean «firmadas» digitalmente. De que manera firmamos la petición? Simple tomamos todos los parámetros (o incluso todos los nombres de los parámetros) utilizando nuestra clave privada, para esto utilizamos el protocolo de cifrado SHA1 HMAC que nos permite cifrar un conjunto de datos con una clave privada.
Por lo que construimos un conjunto de datos a partir de nuestros parámetros y los ciframos utilizando nuestra clave privada para obtener nuestra firma electronica y adjuntamos tanto nuestra clave pública como nuestra firma al conjunto de parámetros enviados en cada petición, y lo enviamos todo.
Luego, del lado del servidor simplemente tomamos el listado de parámetros construimos nuevamente nuestro conjunto de datos y buscamos en la base de datos la clave privada a partir de la clave pública, y generamos nuevamente la firma para el conjunto de datos enviados, finalmente comprobamos la igualdad entre ambas firmas y si hay coincidencia, entonces la petición es autentica!.
Para resumir
Del lado del cliente:
- Tomamos todos los parámetros (y nombres) a enviar, y construimos un conjunto de datos
- Firmamos el conjunto de datos utilizando una encriptación SHA1 HMAC y la clave privada
- Adjuntamos la clave pública y la firma a los parámetros de la petición
- Enviamos la petición
Del lado del Servidor:
- Tomamos todos los parámetros (menos la public key y la signature (o firma)) y construimos un conjunto de datos
- Con la Public Key buscamos la Private Key en la base de datos
- Generamos nuevamente la firma (signature) utilizando SHA1 HMAC y la private Key
- Comparamos ambas firmas, si coinciden, la petición ha sido autenticada!
al describirla así puede sonar simple pero a la hora de la implementación, a veces son necesarias algunas estrategias a tener en cuenta.
veamos como implementé esto dentro de ttTodoMVC.
The SHA1 algorithm
Si hablamos de una implementación SHA1 sobre JavaScript no hablamos de otra librería más que de:
http://caligatio.github.com/jsSHA/
que implementa toda la familia de protocolos SHA, incluyendo SHA1 en
https://github.com/Caligatio/jsSHA/blob/master/src/sha1.js
aunque no es AMD compatible, pero facilmente puede hacerse compatible, simplemente agregando al final:
root = this;
if (typeof exports !== ‘undefined’) {
exports = jsSHA;
} else {
root.jsSHA = jsSHA;
}
y encerrandolo en una función wrapper para que this esté asociado a la window del browser.
del lado del servidor simplemente agregamos
module.exports= jsSHA;
y esto lo hace 100% compatible con Common.js
Luego, para usarlo es simplemente necesario crear un Objeto jsSHA de la siguiente manera
shaObj = new jsSHA(data,»ASCII»);
signature = shaObj.getHMAC($(privateKey, «ASCII»,»HEX»)
y esto nos da la firma digital SHA1 HMAC por private Key,
Con SHA1 tanto del lado del servidor como del cliente, estamos listos para la implementación
Client Side
Backbone.js ofrece muy pocos puntos para definir estrategias de acceso a un servicio RESTful, en casos simples, totalmente fuera del MVC propuesto por Backbone.js, como el login solo tratamos contra una url enviando una petición ajax con jQuery como en:
https://github.com/picanteverde/ttTodoMVC/blob/RESTFulAuth/public/app/modules/login.js
donde podemos ver simplemente generamos un conjunto de datos a firmar utilizando la publicKey como parte de los parametros y generamos un parámetro randomico para simplemente generar ruido en cualquier intento de ubicar nuestra private key
var rnd = Math.random()*1000,
sign = «publicKey=» + $(«#login-username»).val() + «rnd=»+rnd,
shaObj = new jsSHA(sign,»ASCII»);
en este caso utilizo por cuestiones de simplificación solo el nombre de usuario como publicKey, aunque tambien podría haber sido utilizado un Hash SHA1 del mismo nombre de usuario
Acto seguido generamos nuestra firma electronica SHA1 HMAC y la adjuntamos al conjunto de parámetros enviados a la API
$.ajax({
url: «/api/auth»,
type: «POST»,
dataType: «json»,
data: {
publicKey: $(«#login-username»).val(),
rnd: rnd,
signature: shaObj.getHMAC($(«#login-password»).val(), «ASCII»,»HEX»)
},
y enviamos la petición.
ahora a la hora de trabajar con Modelos y Colleciones de Backbone.js es necesario sobre escribir Backbone.Sync, aunque el mismo podría ser escrito de mejor manera esto es lo que tengo hasta ahora:
// Ensure that we have the appropriate request data.
if (!options.data && model && (method == ‘create’ || method == ‘update’ || method == ‘delete’)) {
params.contentType = ‘application/json’;
var data = model.toJSON();
if(publicKey && privateKey){
var key, sign =»», shaObj;
data.publicKey = publicKey;
for(key in data){
if(data.hasOwnProperty(key)){
sign += key + «=» + data[key];
}
}
shaObj = new jsSHA(sign, «ASCII»);
sign = shaObj.getHMAC(privateKey,»ASCII»,»HEX»);
data.signature = sign;
}
params.data = JSON.stringify(data);
}
agregamos delete para enviar un payload en el verbo «delete» de http y de estar definidos public y private key simplemente generamos la signature y agregamos ambos parámetros a la petición
tambien como pueden ver mas adelante hacemos lo mismo con al verbo «read»
if(method==’read’ && publicKey && privateKey){
var rnd = Math.random()*1000,
sign = «publicKey=» + publicKey + «rnd=»+rnd,
shaObj = new jsSHA(sign,»ASCII»);
if(!params.data){
params.data ={};
}
params.data.publicKey = publicKey;
params.data.rnd = rnd;
params.data.signature = shaObj.getHMAC(privateKey, «ASCII», «HEX»);
}
de esta forma firmamos todos los verbos.
Server Side
en el server side es mucho, mucho, más fácil gracias a la idea de middleware traída a expresss.js por connect.js
de esta forma simplemente podemos deifinir una función que se encargue de autenticar nuestra petición
var authorize = function(req, res, next){
var container = null, key, sign,shaObj;
if(req.body.publicKey && req.body.signature){
container = req.body;
}
if(req.query.publicKey && req.query.signature){
container = req.query;
}
if(!container){
res.status(401);
res.json({«error»:»Authentication required!»});
}else{
db.getUser(container.publicKey,function(err, user){
if(!user){
res.status(401);
res.json({«error»:»Authentication required, invalid publicKey»});
}else{
sign = «»;
for(key in container){
if(container.hasOwnProperty(key) && key !== «signature»){
sign += key + «=» +container[key];
}
}
shaObj = new jsSHA(sign, «ASCII»);
sign = shaObj.getHMAC(user.password,»ASCII»,»HEX»);
if(sign === container.signature){
next();
}else{
res.status(401);
res.json({«error»:»Authentication required, Authentication failed!»});
};
}
});
de esta forma via middle ware podemos autenticar cada petición simplemente añadiendo la función como middleware para cada petición que sea necesario autenticar
app.post(«/api/auth»,[authorize], function(req, res, next){
res.json({«auth»:true, «username»: req.body.username});
});
de esta forma podemos autenticar cada petición del lado del servidor sin tener que requerir el password por cada petición solo una signature.
Peróooooooooooooooooooooon por el largo post pero no había forma de explicarlo todo sin ser tan extenso.
Saludoooos