Skip to content
Sentido Web

Sentido Web

  • Contacto
  • Sobre
  • Stop SOPA
Sentido Web
Sentido Web

login

Hapi.js + Vue.js llamadas autenticadas al servidor con JWT (login)
Javascript

Hapi.js + Vue.js llamadas autenticadas al servidor con JWT (login)

ByLuis September 1, 2019September 1, 2019

Hoy toca realizar conexiones con el servidor usando llamadas autenticadas. Para eso usaremos JWT (JSON Web Token), que viene a ser el envío de un token en cada llamada que necesita autenticación. El token está formado por tres cadenas codificadas en base64 y separadas por un punto (header.payload.signature). En el payload se envía toda la info que queramos, pero sin pasarse porque el token no debería ser muy largo y sobre todo sin enviar información sensible, porque el token no está encriptado, es por ello que la comunicación navegador/servidor debe ser mediante HTTPS. Si quieres una explicación más detallada, aquí te lo explican mejor.

En el payload vamos a guardar varios claims, entre ellos el username y un uuid que usaremos para validar que el usuario es correcto. JWT asegura que el token es válido, pero no viene mal añadir cierta seguridad. Cuando el usuario se loguea un nuevo uuid es generado, por lo que se comprobará si coinciden para el username enviado.

Ya tenemos la explicación teórica, ahora vamos con la parte de programación, primero la parte servidor y luego el frontend.

Lo primero que tenemos que hacer es añadir un nuevo plugin a hapi.js que se encargue de la autenticación, registrando un nuevo strategy a server.auth que use JWT. El plugin hapi-auth-jwt2 se encargará de toda la autenticación JWT y añadiremos una capa extra de validación comprobando que el username y el uuid coinciden.

/**
 * Auth controller
 *
 * It uses JWT
 */
const jwt2 = require( 'hapi-auth-jwt2' );
const User = require( '../models/users' );

const validate = async function( decoded ) {
	const user = await User.findByUUID( decoded.id );
	return { isValid: !! user && user.username === decoded.username };
};

exports.plugin = {
	name: 'auth',
	register: async function( server, options ) {
		await server.register( jwt2 );

		server.auth.strategy( 'jwt', 'jwt',
			{
				key: options.jwt.secret,
				validate: validate,
				verifyOptions: { algorithms: [ 'HS256' ] }, // pick a strong algorithm
			}
		);

		server.auth.default( 'jwt' );
	},
};

En el modelo user añadiremos 2 nuevos métodos estáticos, que permitirán encontrar usuarios por username/email y por uuid:

userSchema.static( 'findByUserOrEmail', async function( username, email ) {
	const result = await new Promise( ( resolve, reject ) => {
		this.model( 'User' )
			.findOne( {
				$or: [
					{ username: username },
					{ email: email },
				],
			} )
			.exec( ( error, data ) => {
				if ( error ) {
					reject( error );
				}
				resolve( data );
			} );
	} );
	return result;
} );

userSchema.static( 'findByUUID', async function( uuid ) {
	const result = await new Promise( ( resolve, reject ) => {
		this.model( 'User' )
			.findOne( {
				uuid,
			} )
			.exec( ( error, data ) => {
				if ( error ) {
					reject( error );
				}
				resolve( data );
			} );
	} );
	return result;
} );

En el manifest de Glue hay que añadir la configuración para CORS, ya que el token viajará usando la cabecera Authorization:

const manifest = {
	server: {
		port: Config.get( '/server/port' ),
		routes: {
			cors: {
				origin: [ '*' ],
				credentials: true,
				exposedHeaders: [ 'Authorization' ],
			},
		},
	},
};

Y en la configuración general del servidor hay que añadir una cadena que se usará para encriptar el signature del token JWT.

const config = {
	auth: {
		jwt: {
			secret: process.env.JWT_SECRET,
		},
	},
};

Y por último añadimos una nueva ruta para autenticar (login) el usuario, comprobamos que el usuario y la contraseña coinciden, y si es así, creamos un uuid que guardamos en la bd y generamos el JWT y lo enviamos en la cabecera Authorization:

/**
 * Authenticates an user
 */
server.route( {
	method: 'POST',
	path: '/user/auth',
	options: {
		tags: [ 'api', 'user', 'auth' ],
		description: 'Server authenticate user',
		notes: 'Server authenticate user',
		auth: false,
		validate: {
			payload: {
				username: Joi.string().alphanum().min( 3 ).max( 20 ).required(),
				password: Joi.string().min( 8 ).required(),
			},
		},
	},

	/**
	 * Route handler
	 *
	 * @param {object} request
	 * @param {object} h Hapi object
	 * @returns {object}
	 */
	handler: async( request, h ) => { // eslint-disable-line
		try {
			const user = await User.findByUserOrEmail( request.payload.username, request.payload.email );
			if ( ! user ) {
				return Boom.badData( 'User or password incorrect' );
			}
			const isValidPassword = await bcrypt.compare( request.payload.password, user.password );
			if ( ! isValidPassword ) {
				return Boom.badData( 'User or password incorrect' );
			}

			const claims = {
				id: uuid(),
				exp: new Date().getTime() + ( 180 * 24 * 60 * 60 * 1000 ), // 3 months
				username: user.username,
			};

			user.uuid = claims.id;
			user.save();

			const token = JWT.sign( claims, Config.get( '/auth' ).jwt.secret ); // synchronous

			return h.response( {
				response: true,
				message: 'Check Auth Header for your Token',
			} ).header( 'Authorization', token );
		} catch ( error ) {
			return Boom.badImplementation( 'Error', { error } );
		}
	},
} );

Vale, ya tenemos la parte del servidor, ahora la parte del frontend. Primero es añadir las rutas para /login, /account y /logout. He añadido un meta que indica si la ruta tiene que ser obligatoriamente autenticada, obligatoriamente no autenticada o como sea. Para ello, para cada ruta comprobará si el JWT token está almacenado o no y según el meta redirigirá a home o continuará:

import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/js/components/Home';
import Login from '@/js/components/Login';
import Account from '@/js/components/Account';
import Logout from '@/js/components/Logout';
import Storage from '@/js/utils/storage';

Vue.use( Router );

const router = new Router( {
	mode: 'history',
	routes: [
		{
			path: '/',
			name: 'Home',
			component: Home,
		},
		{
			path: '/login',
			name: 'Login',
			component: Login,
			meta: {
				auth: false,
			},
		},
		{
			path: '/account',
			name: 'Account',
			component: Account,
			meta: {
				auth: true,
			},
		},
		{
			path: '/logout',
			name: 'Logout',
			component: Logout,
			meta: {
				auth: true,
			},
		},
	],
} );

router.beforeEach( ( to, from, next ) => {
	if ( !! to.meta ) {
		const storage = new Storage();
		const jwtToken = storage.getJWTToken();
		// Can't be logged
		if ( to.meta.auth === false ) {
			if ( jwtToken ) {
				next( '/' );
			}
		// Must be logged
		} else if ( to.meta.auth === true ) {
			if ( ! jwtToken ) {
				next( '/' );
			}
		}
		next();
	} else {
		next();
	}
} );

export default router;

Para gestionar el almacenamiento en el navegador en vez de cookies vamos a usar sessionStorage y localStorage para guardar el token JWT. Como el formulario de login permite recordar la sesión, vamos a usar ambos storages. Si no se recuerda usaremos sessionStorage, que se borrará cuando se cierra el navegador, en caso contrario usaremos localStorage.

import Config from '@/js/config';

/**
 * API methods for sesion/local storage.
 * Depending on `this.session` it saves only on `sessionStorage` or also in `localStorage`
 *
 * @since v0.8.0
 */
class storage {
	/**
	 * Constructor
	 *
	 * @param {boolean} session If stored only in session
	 */
	constructor( session = false ) {
		this.session = session;
	}

	/**
	 * It saves the token in the session (and local storage is this.session === false),
	 *
	 * @param {strig} token JWT token
	 */
	setJWTToken( token ) {
		sessionStorage.setItem( Config.jwt.storageKey, token );
		if ( ! this.session ) {
			localStorage.setItem( Config.jwt.storageKey, token );
		}
	}

	/**
	 * It gets a value from session storage or in local if session = false
	 *
	 * @returns {string}
	 */
	getJWTToken() {
		const sessionValue = sessionStorage.getItem( Config.jwt.storageKey );
		if ( sessionValue ) {
			return sessionValue;
		}
		if ( ! this.session ) {
			const storedValue = localStorage.getItem( Config.jwt.storageKey );
			return storedValue;
		}
		return null;
	}

	/**
	 * Removes JWT token from session and local storage
	 */
	removeJWTToken() {
		sessionStorage.removeItem( Config.jwt.storageKey );
		localStorage.removeItem( Config.jwt.storageKey );
	}
}

export default storage;

También hemos creado una librería para tratar las llamadas a la API. Hay dos métodos, uno para autenticar el usuario (login) que mirará la cabecera Authorization, y otro método que obtiene los datos del usuario actual realizando una llamada autenticada enviando el JWT token en la cabecera Authorization:

import Config from '@/js/config';
import Storage from '@/js/utils/storage';

/**
 * API backend methods
 *
 * @since 0.8.0
 */
class apiFetch {
	/**
	 * Authenticate an user, if ok, JWT token is sent by the server in Authorization header
	 *
	 * @param {string} username User name
	 * @param {string} password Password
	 *
	 * @returns {Promise}
	 */
	auth( username, password ) {
		return fetch( Config.api.user.auth, {
			method: 'POST',
			body: JSON.stringify(
				{ username, password }
			),
			mode: 'cors',
		} ).then( response => {
			const auth = response.headers.get( 'Authorization' );
			if ( auth ) {
				return {
					response: true,
					token: auth,
				};
			}
			return {
				response: false,
				message: 'Username or password not valid',
			};
		} );
	}

	getUser( username ) {
		return fetch( `${ Config.api.user.get }${ username }`, {
			method: 'GET',
			headers: {
				Authorization: new Storage().getJWTToken(),
			},
		} ).then( response => response.json() );
	}
}

export default apiFetch;

Ahora solo faltan los controladores para login, account y logout. Login realizar la llamada al servidor y si se obtiene el JWT se guarda:

<template>
	<section class="section">
		<div class="container">
			<h1 class="title">
				Login
			</h1>
			<div
				v-if="error"
				class="columns is-centered has-margin-bottom-2"
			>
				<b-notification
					class="column is-7-tablet is-6-desktop is-5-widescreen"
					type="is-danger"
					has-icon
					aria-close-label="Close notification"
					role="alert"
					size="is-small	"
				>
					{{ error }}
				</b-notification>
			</div>
		</div>
		<div class="container">
			<div class="columns is-centered">
				<div class="column is-5-tablet is-4-desktop is-3-widescreen has-background-light login-form">
					<b-field label="Username">
						<b-input
							v-model="username"
							value=""
							maxlength="30"
							icon="account-circle-outline"
						/>
					</b-field>
					<b-field label="Password">
						<b-input
							v-model="password"
							value=""
							type="password"
							icon="lock-outline"
						/>
					</b-field>
					<div class="field">
						<b-checkbox v-model="remember">
							Remember me
						</b-checkbox>
					</div>
					<div class="has-text-right">
						<b-button
							type="is-primary"
							@click="submit"
						>
							Log in
						</b-button>
					</div>
				</div>
			</div>
		</div>
	</section>
</template>

<script>
import ApiFectch from '@/js/utils/api';
import Storage from '@/js/utils/storage';

export default {
	name: 'Login',
	// Form data and error messages if login fails
	data() {
		return {
			username: '',
			password: '',
			remember: false,
			error: '',
		};
	},
	methods: {
		// It logs in, using the backend API for authenticate the user data.
		// If user logs in, it saves the JWT token in the browser. If not, shows error message.
		submit: function() {
			const api = new ApiFectch();
			api.auth( this.username, this.password )
				.then( response => {
					const storage = new Storage( ! this.remember );
					if ( !! response.response && !! response.token ) {
						storage.setJWTToken( response.token );
						this.error = false;
						// `go` refreshes the page, so user data is updated
						this.$router.go( '/' );
					} else {
						storage.removeJWTToken();
						this.error = response.message;
					}
				} );
		},
	},
};
</script>

<style lang="scss" scoped>
	.login-form {
		border-radius: 4px;
	}
</style>

Logout borra los datos JWT del navegador y redirige a home:

<template>
	<section class="hero is-medium is-primary is-bold">
		<div class="hero-body">
			<div class="container has-text-centered">
				<h1 class="title">
					{{ message }}
				</h1>
				<h2 class="subtitle">
					Miss you 💛
				</h2>
			</div>
		</div>
	</section>
</template>

<script>
import User from '@/js/utils/user';

export default {
	name: 'Logout',
	// Dummy data
	data() {
		return {
			message: 'Bye',
		};
	},
	// After being created it logs out and go to home
	created() {
		new User().logout();
		// `go` instead of `push` because refreshing the page updates the user data
		// Maybe using vuex is a better way to do it, or not...
		this.$router.go( '/' );
	},
};
</script>

Y Account recupera los datos del usuario una vez que ha creado el controlador:

<template>
	<div id="account">
		<section class="hero is-primary is-bold">
			<div class="hero-body">
				<div class="has-text-centered">
					<h1 class="title">
						{{ message }}
					</h1>
					<h2 class="subtitle">
						Your data
					</h2>
				</div>
			</div>
		</section>
		<section class="section">
			<div class="container">
				<div class="tile is-ancestor">
					<div class="tile is-parent">
						<p class="tile is-child notification">
							Some content
						</p>
					</div>
					<div class="tile is-8 is-parent">
						<div class="tile is-child notification is-info">
							<ul id="data">
								<li
									v-for="( value, key ) in user"
									:key="key"
								>
									{{ key }} : {{ value }}
								</li>
							</ul>
						</div>
					</div>
				</div>
			</div>
		</section>
	</div>
</template>

<script>
// Dummy component

import ApiFectch from '@/js/utils/api';
import User from '@/js/utils/user';

export default {
	name: 'Account',
	data() {
		return {
			message: 'Account',
			user: {},
		};
	},
	created() {
		const user = new User().getCurrentUser();
		new ApiFectch().getUser( user.username )
			.then( response => this.user = response );
	},
};
</script>

Recuerda que tienes todo el código aquí

Javascript | PHP | Quicklinks

Building a Sleek AJAX Email Signup Form

ByLuis August 30, 2010

In this tutorial, we’ll learn how to create a slick email signup form. This form will harness the full power of AJAX, meaning that we will submit data to the server, validate it, and receive a response – all without a single page refresh! Along the way, you should pick up some useful tips related to PHP and JavaScript, as well as general programming …

Post original

PHP

Login en Facebook con PHP y CURL

ByLuis June 17, 2010

¿Para que usar CURL si se puede usar la API de Facebook? sencillo, porque la API no te permite obtener todos los datos que se pueden leer cuando navegas por Facebook. Por ello, en alguna ocasión, nos podemos encontrar con la necesidad de loguearnos en Facebook y “navegar” usando CURL:

echo 'loading 
'; $login_email = 'my@sexy.mail'; $login_pass = 'my_sexy_password'; $fp = fopen("fb_cookies.txt", w); fclose($fp); echo 'still loading
'; if (fb_login($login_email,$login_pass)){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://login.facebook.com/login.php?login_attempt=1'); curl_setopt($ch, CURLOPT_POSTFIELDS,'charset_test=%E2%82%AC%2C%C2%B4%2C%E2%82%AC%2C%C2%B4%2C%E6%B0%B4%2C%D0%94%2C%D0%84&locale=en_US&email='.urlencode($login_email).'&pass='.urlencode($login_pass).'&pass_placeholder=&charset_test=%E2%82%AC%2C%C2%B4%2C%E2%82%AC%2C%C2%B4%2C%E6%B0%B4%2C%D0%94%2C%D0%84'); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_COOKIEJAR, str_replace('\\','/',dirname(__FILE__)).'/fb_cookies.txt'); curl_setopt($ch, CURLOPT_COOKIEFILE, str_replace('\\','/',dirname(__FILE__)).'/fb_cookies.txt'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6 (.NET CLR 3.5.30729)"); $html = curl_exec($ch); curl_close($ch); echo $html; } function fb_login($login_email, $login_pass){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://login.facebook.com/login.php?login_attempt=1'); curl_setopt($ch, CURLOPT_POSTFIELDS,'charset_test=%E2%82%AC%2C%C2%B4%2C%E2%82%AC%2C%C2%B4%2C%E6%B0%B4%2C%D0%94%2C%D0%84&locale=en_US&email='.urlencode($login_email).'&pass='.urlencode($login_pass).'&pass_placeholder=&charset_test=%E2%82%AC%2C%C2%B4%2C%E2%82%AC%2C%C2%B4%2C%E6%B0%B4%2C%D0%94%2C%D0%84'); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_COOKIEJAR, str_replace('\\','/',dirname(__FILE__)).'/fb_cookies.txt'); curl_setopt($ch, CURLOPT_COOKIEFILE, str_replace('\\','/',dirname(__FILE__)).'/fb_cookies.txt'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6 (.NET CLR 3.5.30729)"); $html = curl_exec($ch); $err = 0; $err = curl_errno($ch); curl_close($ch); if ($err != 0){ echo 'error='.$err."\n"; return(false); } else { echo 'fetching..'; return(true); } }

Código fuente en Pastie

Vía / Stack Overflow

PHP

reduxauth: librería de login para CodeIgniter

ByLuis July 7, 2008

Redux Authedtication es una librería creada para CodeIgniter que nos permite realizar autenticación en nuestro sistema de forma ligera, sencilla y completa en funcionalidades. Una gran elección para nuestros proyectos debido a las posibilidades que ofrece al desarrollador.

Entre las características nos encontramos con:

  • Registro
  • Login y logout
  • Recordar contraseña
  • Centrado en seguridad
  • Grupos de usuarios
  • Sistema de baneo

The Redux Authentication System

Desarrollo web

Login mediante cookies

ByLuis February 13, 2008

Este post quizás es algo teórico, por lo que serviría para cualquier lenguaje (PHP, Java, ASP…), pero no habrá código para poder utilizar.

Se trata de hacer login en una aplicación mediante una cookie que estará almacenada en el navegador. Esto es lo que se suele hacer cuando queremos recordar al usuario y así que no tenga que logarse cada vez que entra o se pierde la sesión del navegador. Como se trata de una cookie le podemos poner la fecha de caducidad que queramos por lo que el usuario permanecerá logado durante ese tiempo.

Los datos en la cookie no pueden ir en claro, ya que cualquiera podría obtener esos datos si accedieran a nuestro navegador, por lo que hay que encriptarlos.

Supongamos que tenemos una tabla de usuarios que tiene como mínimo 3 datos: alias, contraseña y código. El alias y la contraseña están claro para que se usan y el código lo veremos más adelante. Contraseña y código serán ambos datos encriptados con MD5.

El proceso de login normal (mediante formulario) sería el siguiente: recibimos usuario y contraseña mediante POST y hacemos consulta a la BD para obtener los datos del usuario mediante el alias. Una vez obtenidos los datos del usuario, comprobamos si la contraseña de la BD y la contraseña recibida y encriptada con MD5 son la misma (nunca hay que recuperar los datos igualando el usuario y la contraseña, ya que si no se hace correctamente puede haber problemas de seguridad, por lo que es mejor obtener primero los datos y luego mediante código comparar los datos). Si las contraseñas coinciden daremos acceso a la aplicación, pero antes almacenaremos en el campo código un valor aleatorio y encriptado con MD5 que usaremos después.

Para realizar el login mediante cookies tendremos que guardar en la cookie el alias y la contraseña. Lo único que encriptaremos será la contraseña, el usuario también se podría encriptar, pero obtener los datos del usuario usando el alias encriptado es menos eficiente, tendría que realizar un MD5 por cada registro de la tabla de usuarios. También se podría almacenar en otro campo el alias encriptado, pero serían datos redundantes y tampoco es muy correcto.

La contraseña no se guardaría únicamente encriptada, ya que el MD5 se puede romper mediante fuerza bruta, por lo que lo que almacenaremos será una unión encriptada de una función simple entre la contraseña y el código. El ejemplo más sencillo para la función es la concatenación, por lo que la contraseña almacenada en la cookie quedaría algo así:

md5(contraseña+codigo)

que para ver un ejemplo más claro, si nuestra contraseña fuera ‘password‘, quedaría algo así:

md5( md5('password') + md5( rand() ) )

ya que la contrasñena en la BD se guarda encriptada y el código es un valor aleatorio encriptado.

Supongo que con mucha paciencia también se puede romper este código, pero realmente, si te preocupa que alguien pueda romper un MD5 de una cadena de 64 caracteres (32+32), mejor no hagas el recordar password y usa SSL.

Una vez obtenida la contraseña de la cookie, realizamos la operación con la contraseña obtenida en el consulta a la BD y si coinciden pues damos acceso a la aplicación.

Cada vez que se haga login se renueva el código y actualiza la cookie, y cada vez que se haga logout o se equivoque en el login se actualiza el código y se borra la cookie.

Esta es la forma en que lo haría yo, pero si alguien tiene dudas sobre la eficacia del método, por favor que lo comente.

© 2025 Sentido Web - WordPress Theme by Kadence WP

  • Contacto
  • Sobre
  • Stop SOPA