Autenticación API con Phoenix y Guardian
Guardian es una biblioteca para usar autenticación en aplicaciones hechas con Phoenix. Con Guardian podemos crear JSON Web Tokens que se pueden usar para autenticar al usuario ya sea usando EmberJS, Angular, iOS, Android, etc.
Guardian no solo sirve para generar tokens, si no también para limitar quienes ingresan a una URL o no.
En este tutorial crearemos un proyecto en donde generaremos los tokens para los usuarios, pude ser el inicio para crear tus API Rest con Phoenix.
Creando el proyecto
Primero vamos a crear un proyecto Phoenix, como nuestra aplicación será solamente para generar APIs no es necesario el manejo de assets estáticos.
mix phoenix.new hello_guardian --no-brunch
Escribe Y y dale enter para que instale las dependencias
...
* creating hello_guardian/web/templates/page/index.html.eex
* creating hello_guardian/web/views/layout_view.ex
* creating hello_guardian/web/views/page_view.ex
Fetch and install dependencies? [Yn]
Una vez hecho esto, ya tendremos nuestra aplicación de Phoenix.
Creando los modelos y base de datos
Lo primero que haremos será configurar nuestra base de datos y crear el modelo de usuario para hacer las pruebas
# Configure your database
config :hello_guardian, HelloGuardian.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "hello_guardian_dev",
hostname: "localhost",
pool_size: 10
Ya que lo hayas configurado, crearemos un modelo de usuario sencillo, que nos servirá para poner en práctica el login con tokens.
En la carpeta web/models/
crea el archivo user.ex
.
defmodule HelloGuardian.User do
use HelloGuardian.Web, :model
schema "users" do
field :name, :string
field :email, :string
field :password, :string
field :password_conf, :string, virtual: true
timestamps
end
@required_fields ~w(email password password_conf)
@optional_fields ~w(name)
def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
|> unique_constraint(:email)
end
end
Un modelo simple, hemos creado el schema “user” que será el nombre de nuestra tabla en la base de datos. el campo password_conf
es virtual ya que necesitamos que podamos hacer una validación de la contraseña pero no queremos guardar este dato en la base de datos, por
lo que solo existirá en nuestra aplicación. También hemos hecho que el email sea único, de tal forma que no se repita en nuestra base de
datos, esto servirá para que sea usado como nombre de usuario.
Es tiempo de crear la migración para crear la tabla en la base de datos.
mix ecto.gen.migration create_user
Abre el archivo que generó la migración para crear nuestra tabla y escribe lo siguente
defmodule HelloGuardian.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :name, :string
add :email, :string
add :password, :string
timestamps
end
create unique_index(:users, [:email])
end
end
Ahora vamos a crear un usuario de prueba en nuestra base de datos, en el archivo priv/repo/seeds.exs
crea el siguiente código
HelloGuardian.Repo.insert!(%User{
name: "Giovanni",
email: "test@example.com",
password: "12345678"
})
Ya tenemos nuestras primeras configuraciones listas, vamos a crear la base de datos, a migrarlo y a insertar los datos.
$ mix ecto.create
The database for HelloGuardian.Repo has been created.
$ mix ecto.migrate
13:18:40.201 [info] == Running HelloGuardian.Repo.Migrations.CreateUser.change/0 forward
13:18:40.202 [info] create table users
13:18:40.234 [info] create index users_email_index
13:18:40.241 [info] == Migrated in 0.3s
Ahora corre mix run priv/repo/seeds.exs
en la consola, con esto habremos llenado nuestra base de datos.
Listo, ya tenemos creada nuestra tabla y hemos agregado un usuario de prueba. Como te darás cuenta, el campo de password se encuentra en texto plano, esto obviamente no lo haremos cuando vayamos a crear una aplicación ya para producción, así que vamos a cambiar nuestro código para que la contraseña esté hasheada.
Hasheando el campo de contraseña
Para que nuestra contraseña esté hasheada, vamos a necesitar una biblioteca llamada comeonin.
Agrega la biblioteca a tus dependencias en mix.exs
{:comeonin, "~> 3.0"}
Corre mix do deps.get, compile
Ahora ya tenemos comeonin instalado, ahora es necesario actualizar nuestro código para que guarde la contraseña hasheada. Borra el registro que creamos, y actualiza el arcivo seeds.exs
alias HelloGuardian.User
import Comeonin.Bcrypt, only: [hashpwsalt: 1]
HelloGuardian.Repo.insert!(%User{
name: "Giovanni",
email: "test@example.com",
password: hashpwsalt("12345678"),
password_conf: "12345678",
})
Como puedes ver importamos la función hashpwsalt, esto nos sirve para hashear la contraseña. Si corremos ahora este script mix run priv/repo/seeds.exs
y vemos el campo de contraseña en
nuestra base de datos, te podrás dar cuenta que el campo contraseña se encuentra hasheado.
Instalando y configurando Guardian
Ahora es tiempo de instalar y configurar Guardian, que es la biblioteca que usaremos para crear los tokens que usaremos ya sea en conjunto con el frontend o en alguna aplicación móvil.
Primero agregamos guardian a nuestras dependencias
defp deps do
[# ...
{:guardian, "~> 0.10.0"}
..
]
end
Corre mix deps.get
para obterlo. Antes de poder usar Guardian, es necesario hacer unas configuraciones, en el archivo config.exs
agrega
lo siguiente
config :guardian, Guardian,
allowed_algos: ["HS512"], # optional
verify_module: Guardian.JWT, # optional
issuer: "HelloGuardian",
ttl: { 30, :days },
verify_issuer: true, # optional
secret_key: "jkjjsisisi*jsj0(=0",
serializer: HelloGuardian.GuardianSerializer
Con esto, configuramos aspectos básicos para generar nuestro token, usamos un algoritmo HS512, también la duración que permitirá estar activo el token, en nuestro caso es 30 días. La secret key debes poner una cadena que sea difícil de adivinar, por ahora con poner cualquiera está bien, en producción querrás poner estas configuraciones fuera del source control.
También necesitamos crear un serializer, que nos ayudará an codificar y decodificar nuestro usuario y token.
Crea el archivo lib/hello_guardian/guardian_serializer.ex
defmodule HelloGuardian.GuardianSerializer do
@behaviour Guardian.Serializer
alias HelloGuardian.Repo
alias HelloGuardian.User
def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
def for_token(_), do: { :error, "Unknown resource type" }
def from_token("User: " <> id), do: { :ok, Repo.get(User, id) }
def from_token(_), do: { :error, "Unknown resource type" }
end
Ya con Guardian configurado, es hora de crear nuestro controlador para general el token.
Creando el controlador de login
En la carpeta de controllers, crea el archivo login_controller.ex
defmodule HelloGuardian.LoginController do
use HelloGuardian.Web, :controller
alias HelloGuardian.User
import Comeonin.Bcrypt, only: [checkpw: 2]
def login(conn, %{"user" => user_params}) do
%{"email" => email, "password" => password} = user_params
if user = validate_credentials(email, password) do
# Get the token from Guardian
new_conn = Guardian.Plug.api_sign_in(conn, user)
jwt = Guardian.Plug.current_token(new_conn)
claims = Guardian.Plug.claims(new_conn)
new_conn
|> json(%{token: jwt)
else
conn
|> put_status(400)
|> json(%{error: "Correo o contraseña incorrecta"})
end
end
defp validate_credentials(email, password) do
if user = User.get_by_email(email) do
(checkpw(password, user.password)) && user || nil
else
nil
end
end
end
Lo que hicimos aquí es sencillo, creamos el alias hacia nuestro modelo de usuario para poder usarlo en nuestro código,
igualmente importamos la función checkpw/2
que nos ayuda a verificar si el password que enviamos es el mismo
que el que tenemos en la base de datos.
Para ayudarnos a saber si el usuario y contraseña son válidos, creamos la función privada validate_credentials/2
que toma el correo
y contraseña, primero hacemos la búsqueda por email con la función get_by_email
, que aún no hemos creado en el modelo, si el
usuario existe, entonces checamos si el password es igual al guardado, si el usuario no es nil, si
pasa todas esas validaciones entonces nos valida la credencial.
Una vez que hemos validado la credencial, entonces es hora de obtener el token, la parte importante en esto es
new_conn = Guardian.Plug.api_sign_in(conn, user)
jwt = Guardian.Plug.current_token(new_conn)
claims = Guardian.Plug.claims(new_conn)
new_conn
|> json(%{token: jwt})
Al usar la función Guardian.Plug.current_token(new_conn)
podemos optener el token que devolvemos en el json.
Y como cada controlador necesita su vista, crea el archivo login_view.ex
defmodule HelloGuardian.LoginView do
use HelloGuardian.Web, :view
end
Obteniendo el usuario por email
Como dije anteriormente, nos hace falta crear la función get_by_email/1
, en tu modelo de usuario, crea la función
alias HelloGuardian.User
def get_by_email(email) do
query = from user in User,
where: user.email == ^email
query
|> HelloGuardian.Repo.one
end
Simplemente hacemos una consulta en los usuarios con el correo que pasemos, si encuentra uno o más, enviamos el primero, que sería el único ya que no debe haber dos o más usuarios con el mismo email.
Configurando el router
Ya casi terminamos, antes de obtener el token, es necesario decirle a nuestro router a qué path debemos ir, pero antes, hay que configurar nuestro pipeline de Api, ya que estamos haciendo un API Rest
pipeline :api do
plug :accepts, ["json"]
plug Guardian.Plug.VerifyHeader
plug Guardian.Plug.LoadResource
end
Simplemente verificamos el encabezado y cargamos los recursos de Guardian,
Ahora en nuestro scope de api, vamos a poner el post a nuestra URL de login
scope "/api", HelloGuardian do
pipe_through :api
post "/login", LoginController, :login
end
Listo, ya tenemos terminado nuestro generador de tokens
Probando nuestro login
Ahora es tiempo de probar nuestra aplicación.
Para probarlo, yo uso Postman, que es una aplicación de Google que nos sirve para hacer requests
Primero corremos la aplicación mix phoenix.server
Ahora usamos Postman para hacer el request, la URL que usaremos será http://localhost:4000/api/login
y en el body
del request usamos un json con los datos del usuario
{"user":
{
"password":"12345678",
"email":"test@example.com",
}
}
Y con los datos correctos, obtenemos nuestro token
Ahora este token podemos usarlo en el header de Autorización, de tal forma que no enviamos el usuario y contraseña en cada request, para poder usar ese token y que solo puedan entrar los usuarios logueados es necesario hacer algunos ajustes en los controladores, que también se hacen con Guardian, pero eso sería para otro post.
Ya con esto terminamos, si tienes alguna duda o comentario con respecto al tutorial, puedes escribirme.