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",
}

}

Post to login

Y con los datos correctos, obtenemos nuestro token

Getting 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.