javascript - Uso de OAuth2 en la aplicación web HTML5
oauth-2.0 (3)
La única forma de estar completamente seguro es no almacenar el lado del cliente de tokens de acceso. Cualquier persona con acceso (físico) a su navegador podría obtener su token.
1) Su evaluación de que no es una gran solución es precisa.
2) Utilizar los tiempos de caducidad sería lo mejor si está limitado solo al desarrollo del lado del cliente. No requeriría que sus usuarios volvieran a autenticarse con Oauth con tanta frecuencia, y garantizaría que el token no viviría para siempre. Aún no es el más seguro.
3) Obtener un token nuevo requeriría realizar el flujo de trabajo de Oauth para obtener un token nuevo. El client_id está vinculado a un dominio específico para que Oauth funcione.
El método más seguro para retener los tokens de Oauth sería una implementación del lado del servidor.
Actualmente estoy experimentando con OAuth2 para desarrollar una aplicación móvil construida completamente en JavaScript que se comunica con una API de CakePHP. Eche un vistazo al siguiente código para ver cómo se ve mi aplicación (tenga en cuenta que este es un experimento, de ahí el código desordenado y la falta de estructura en áreas, etc.)
var access_token,
refresh_token;
var App = {
init: function() {
$(document).ready(function(){
Users.checkAuthenticated();
});
}(),
splash: function() {
var contentLogin = ''<input id="Username" type="text"> <input id="Password" type="password"> <button id="login">Log in</button>'';
$(''#app'').html(contentLogin);
},
home: function() {
var contentHome = ''<h1>Welcome</h1> <a id="logout">Log out</a>'';
$(''#app'').html(contentHome);
}
};
var Users = {
init: function(){
$(document).ready(function() {
$(''#login'').live(''click'', function(e){
e.preventDefault();
Users.login();
});
$(''#logout'').live(''click'', function(e){
e.preventDefault();
Users.logout();
});
});
}(),
checkAuthenticated: function() {
access_token = window.localStorage.getItem(''access_token'');
if( access_token == null ) {
App.splash();
}
else {
Users.checkTokenValid(access_token);
}
},
checkTokenValid: function(access_token){
$.ajax({
type: ''GET'',
url: ''http://domain.com/api/oauth/userinfo'',
data: {
access_token: access_token
},
dataType: ''jsonp'',
success: function(data) {
console.log(''success'');
if( data.error ) {
refresh_token = window.localStorage.getItem(''refresh_token'');
if( refresh_token == null ) {
App.splash();
} else {
Users.refreshToken(refresh_token);
}
} else {
App.home();
}
},
error: function(a,b,c) {
console.log(''error'');
console.log(a,b,c);
refresh_token = window.localStorage.getItem(''refresh_token'');
if( refresh_token == null ) {
App.splash();
} else {
Users.refreshToken(refresh_token);
}
}
});
},
refreshToken: function(refreshToken){
$.ajax({
type: ''GET'',
url: ''http://domain.com/api/oauth/token'',
data: {
grant_type: ''refresh_token'',
refresh_token: refreshToken,
client_id: ''NTEzN2FjNzZlYzU4ZGM2''
},
dataType: ''jsonp'',
success: function(data) {
if( data.error ) {
alert(data.error);
} else {
window.localStorage.setItem(''access_token'', data.access_token);
window.localStorage.setItem(''refresh_token'', data.refresh_token);
access_token = window.localStorage.getItem(''access_token'');
refresh_token = window.localStorage.getItem(''refresh_token'');
App.home();
}
},
error: function(a,b,c) {
console.log(a,b,c);
}
});
},
login: function() {
$.ajax({
type: ''GET'',
url: ''http://domain.com/api/oauth/token'',
data: {
grant_type: ''password'',
username: $(''#Username'').val(),
password: $(''#Password'').val(),
client_id: ''NTEzN2FjNzZlYzU4ZGM2''
},
dataType: ''jsonp'',
success: function(data) {
if( data.error ) {
alert(data.error);
} else {
window.localStorage.setItem(''access_token'', data.access_token);
window.localStorage.setItem(''refresh_token'', data.refresh_token);
access_token = window.localStorage.getItem(''access_token'');
refresh_token = window.localStorage.getItem(''refresh_token'');
App.home();
}
},
error: function(a,b,c) {
console.log(a,b,c);
}
});
},
logout: function() {
localStorage.removeItem(''access_token'');
localStorage.removeItem(''refresh_token'');
access_token = window.localStorage.getItem(''access_token'');
refresh_token = window.localStorage.getItem(''refresh_token'');
App.splash();
}
};
Tengo varias preguntas relacionadas con mi implementación de OAuth:
1.) Al parecer, almacenar el access_token en localStorage es una mala práctica y, en su lugar, debería estar usando cookies. ¿Alguien puede explicar por qué? Como esto ya no es seguro o menos seguro, los datos de las cookies no se cifrarán.
ACTUALIZACIÓN: De acuerdo con esta pregunta: Almacenamiento Local vs Cookies almacenando los datos en localStorage SOLAMENTE disponible en el lado del cliente de todos modos y no hace ninguna solicitud HTTP a diferencia de las cookies, por lo que parece más seguro para mí, o al menos no parece tengo problemas en lo que puedo decir!
2.) En relación con la pregunta 1, el uso de una cookie para el tiempo de caducidad tampoco sería útil para mí, ya que si miras el código, se realiza una solicitud al inicio de la aplicación para obtener la información del usuario, que devolvería un error si había caducado en el extremo del servidor, y requiere una actualización_token. Así que no estoy seguro de los beneficios de tener tiempos de caducidad tanto en el cliente como en el servidor, cuando el servidor es lo que realmente importa.
3.) ¿Cómo obtengo un token de actualización, sin A, guardándolo con el access_token original para usarlo más adelante, y B) también almacenando un client_id? Me han dicho que esto es un problema de seguridad, pero ¿cómo puedo usarlos más adelante, pero protegerlos en una aplicación solo para JS? Nuevamente vea el código de arriba para ver cómo lo he implementado hasta ahora.
Para el enfoque de solo cliente puro, si tiene la oportunidad, intente utilizar "Flujo implícito" en lugar de "Flujo de propietario de recurso". No recibe el token de actualización como parte de la respuesta.
- Cuando la página de acceso del usuario, JavaScript busca el access_token en localStorage y verifica que caduque_in
- Si falta o caduca, la aplicación abre una nueva pestaña y redirige al usuario a la página de inicio de sesión, después de que el usuario de inicio de sesión exitoso es redirigido hacia atrás con token de acceso que se maneja solo en el lado del cliente y se conserva en el almacenamiento local con la página de redireccionamiento
- La página principal puede tener un mecanismo de sondeo en el token de acceso en el almacenamiento local y tan pronto como el usuario inicia sesión (redirige la página guarda el token al almacenamiento) en la página normalmente.
En el enfoque anterior, el token de acceso debe ser de larga duración (por ejemplo, 1 año). Si hay una preocupación con el token de vida larga, puedes usar el siguiente truco.
- Cuando la página de acceso del usuario, JavaScript busca el access_token en localStorage y verifica que caduque_in
- Si falta o expiró, la aplicación abre el iframe oculto e intenta iniciar sesión. Por lo general, el sitio web auth tiene una cookie de usuario y almacena una concesión en el sitio web del cliente, por lo tanto, el inicio de sesión ocurre automáticamente y el script dentro del iframe completará el token en el almacenamiento
- La página principal del cliente establece el mecanismo de sondeo en access_token y timeout. Si durante este breve período, access_token no se llena en el almacenamiento, significa que tenemos que abrir una nueva pestaña y establecer un flujo implícito normal en movimiento.
Parece que estás usando las credenciales de contraseña del propietario del recurso Flujo de OAuth 2.0, por ejemplo, enviando nombre de usuario / contraseña para volver a obtener un token de acceso y un token de actualización.
- El token de acceso PUEDE estar expuesto en javascript, los riesgos de que el token de acceso quede expuesto de alguna manera se mitigan por su corta vida útil.
- El token de actualización NO DEBE estar expuesto al javascript del lado del cliente. Se usa para obtener más tokens de acceso (como lo hace anteriormente), pero si un atacante pudiera obtener el token de actualización, podría obtener más tokens de acceso a voluntad hasta que el servidor OAuth revoque la autorización del cliente para el que se emitió el token de actualización .
Con ese fondo en mente, permítame responder a sus preguntas:
- Ya sea una cookie o un almacenamiento local, obtendrá persistencia local en las actualizaciones de página. Almacenar el token de acceso en el almacenamiento local le brinda un poco más de protección contra los ataques CSRF, ya que no se enviará automáticamente al servidor como lo hará una cookie. Su javascript del lado del cliente deberá sacarlo de la ubicación local y transmitirlo en cada solicitud. Estoy trabajando en una aplicación OAuth 2 y como es un enfoque de una sola página, no hago ninguna de las dos cosas; en cambio, solo lo guardo en la memoria.
- Estoy de acuerdo ... si está almacenando en una cookie es solo por la persistencia y no por la caducidad, el servidor responderá con un error cuando el token caduque. La única razón por la que puedo pensar que puede crear una cookie con vencimiento es para que pueda detectar si ha expirado SIN hacer primero una solicitud y esperando una respuesta de error. Por supuesto, podría hacer lo mismo con el almacenamiento local al guardar ese tiempo de caducidad conocido.
- Este es el quid de la cuestión en la que creo ... "¿Cómo obtengo un token de actualización, sin A, almacenándolo con el access_token original para usarlo más adelante, y B) también almacenando un client_id". Lamentablemente, no se puede ... Como se señaló en el comentario introductorio, tener el lado del cliente de token de actualización niega la seguridad proporcionada por la vida útil limitada del token de acceso . Lo que estoy haciendo en mi aplicación (donde no estoy usando ningún estado de sesión persistente del lado del servidor) es el siguiente:
- El usuario envía nombre de usuario y contraseña al servidor
- A continuación, el servidor reenvía el nombre de usuario y la contraseña al punto final OAuth, en su ejemplo anterior
http://domain.com/api/oauth/token
, y recibe el token de acceso y el token de actualización . - El servidor cifra el token de actualización y lo establece en una cookie (debe ser solo HTTP)
- El servidor responde con el token de acceso SOLAMENTE en texto claro (en una respuesta JSON) Y en la cookie HTTP solo encriptada
- JavaScript del lado del cliente ahora puede leer y usar el token de acceso (almacenar en el almacenamiento local o lo que sea
- Cuando el token de acceso caduca, el cliente envía una solicitud al servidor (no al servidor OAuth, sino al servidor que aloja la aplicación) para un nuevo token
- El servidor recibe la cookie HTTP única encriptada que creó, la descifra para obtener el token de actualización , solicita un token de acceso nuevo y finalmente devuelve el token de acceso nuevo en la respuesta.
Es cierto que esto infringe la restricción "JS-Only" que estaba buscando. Sin embargo, a) una vez más, usted NO debería tener un token de actualización en javascript yb) requiere una lógica mínima del lado del servidor al iniciar / cerrar sesión y no tiene un almacenamiento persistente en el servidor.
Nota sobre CSRF : Como se señala en los comentarios, esta solución no aborda la falsificación de solicitudes entre sitios ; consulte la Hoja de referencia de prevención de CSRF de OWASP para obtener más ideas sobre cómo abordar estas formas de ataques.
Otra alternativa es simplemente no solicitar el token de actualización en absoluto (no estoy seguro si esa es una opción con la implementación de OAuth 2 con la que está tratando, el token de actualización es opcional según la especificación ) y volver a autenticar continuamente cuando caduque.
¡Espero que ayude!