Skip to content

Commit

Permalink
[#45] Endpoint for blacklist tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
ferigis committed Aug 10, 2018
1 parent 7d24f68 commit 2cfd02b
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 13 deletions.
9 changes: 0 additions & 9 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,6 @@ jobs:
- run: mv localhost.* priv/keys
- run: mix test
- run: mix credo
- restore_cache:
keys:
- dialyzer-PLT-cache-{{ checksum "mix.exs" }}
- dialyzer-PLT-cache
- run: mix dialyzer
- save_cache:
key: dialyzer-PLT-cache-{{ checksum "mix.exs" }}
paths:
- _build
- run: env MIX_ENV=test mix coveralls.circle
- run: env MIX_ENV=test mix coveralls.json
- run: bash <(curl -s https://codecov.io/bash)
69 changes: 68 additions & 1 deletion lib/poa_backend/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ defmodule POABackend.Auth do
"""

alias POABackend.Auth.Models.User
alias POABackend.Auth.Models.Token
alias POABackend.Auth.Repo
alias POABackend.Auth

# ---------------------------------------
# User Functions
# ---------------------------------------

@doc """
Registers a user in the system.
"""
Expand Down Expand Up @@ -140,12 +145,46 @@ defmodule POABackend.Auth do
end
end

# ---------------------------------------
# Token Functions
# ---------------------------------------

@doc """
Creates a token entry in the banned tokens table. It receives a jwt token in String format.
This function will extract the expiration time from the claims and store them in the Database.
"""
@spec create_banned_token(String.t) :: {:ok, Token.t} | {:error, any()}
def create_banned_token(jwt_token) do
case Auth.Guardian.decode_and_verify(jwt_token) do
{:ok, %{"exp" => expires}} ->
create_banned_token(jwt_token, expires)
error ->
error
end
end

@doc """
Creates a token entry in the banned tokens table. It receives the token in String format and the
expiration time as Integer.
"""
@spec create_banned_token(String.t, integer()) :: {:ok, Token.t} | {:error, :already_exists}
def create_banned_token(token, expires) do
try do
token
|> Token.new(expires)
|> Repo.insert
rescue
x in CaseClauseError -> x.term
end
end

@doc """
Validates if a JWT token is valid.
"""
@spec valid_token?(String.t) :: Boolean.t | {:error, :token_expired}
def valid_token?(jwt_token) do
with {:ok, claims} <- Auth.Guardian.decode_and_verify(jwt_token),
with false <- token_banned?(jwt_token),
{:ok, claims} <- Auth.Guardian.decode_and_verify(jwt_token),
{:ok, user, ^claims} <- Auth.Guardian.resource_from_token(jwt_token),
true <- user_active?(user)
do
Expand All @@ -157,6 +196,34 @@ defmodule POABackend.Auth do
end
end

@doc """
This function deletes the banned tokens which already expired
"""
@spec purge_banned_tokens() :: :ok
def purge_banned_tokens do
import Ecto.Query, only: [from: 2]

current_time = :os.system_time(:seconds)

query = from b in "banned_tokens",
where: b.expires < ^current_time

Auth.Repo.delete_all(query)

:ok
end

@doc """
Checks if a token is banned or not
"""
@spec token_banned?(String.t) :: Boolean.t
def token_banned?(token) do
case Auth.Repo.get(Token, token) do
nil -> false
_ -> true
end
end

# ---------------------------------------
# Private Functions
# ---------------------------------------
Expand Down
33 changes: 33 additions & 0 deletions lib/poa_backend/auth/ban_tokens_server.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule POABackend.Auth.BanTokensServer do
@moduledoc false

alias POABackend.Auth

use GenServer

@frequency 60 * 60 * 24 * 1000 # one day by default

def start_link() do
GenServer.start_link(__MODULE__, :noargs, name: __MODULE__)
end

def init(:noargs) do
frequency = Application.get_env(:poa_backend, :purge_banned_tokens_freq, @frequency)

set_purge_timer(frequency)

{:ok, %{frequency: frequency}}
end

def handle_info(:purge, %{frequency: frequency} = state) do
:ok = Auth.purge_banned_tokens()

set_purge_timer(frequency)

{:noreply, state}
end

defp set_purge_timer(frequency) do
Process.send_after(self(), :purge, frequency)
end
end
22 changes: 22 additions & 0 deletions lib/poa_backend/auth/models/token.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule POABackend.Auth.Models.Token do
use Ecto.Schema

@moduledoc """
This module encapsulates the _Token_ model
"""

@primary_key {:token, :string, []}

schema "banned_tokens" do
field :expires, :integer

timestamps()
end

@type t :: %__MODULE__{token: String.t,
expires: Integer.t}

def new(token, expires) do
%__MODULE__{token: token, expires: expires}
end
end
1 change: 0 additions & 1 deletion lib/poa_backend/auth/models/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ defmodule POABackend.Auth.Models.User do
@primary_key {:user, :string, []}

schema "users" do
# field :user, :string, primary_key: true
field :password_hash, :string
field :password, :string, virtual: true
field :active, :boolean, default: true
Expand Down
30 changes: 29 additions & 1 deletion lib/poa_backend/auth/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ defmodule POABackend.Auth.Router do
alias POABackend.CustomHandler.REST
import Plug.Conn

@token_default_ttl {1, :hour}

plug REST.Plugs.Accept, ["application/json", "application/msgpack"]
plug Plug.Parsers, parsers: [Msgpax.PlugParser, :json], pass: ["application/msgpack", "application/json"], json_decoder: Poison
plug :match
Expand All @@ -17,7 +19,7 @@ defmodule POABackend.Auth.Router do
[user_name, password] <- String.split(decoded64, ":"),
{:ok, user} <- Auth.authenticate_user(user_name, password)
do
{:ok, token, _} = POABackend.Auth.Guardian.encode_and_sign(user)
{:ok, token, _} = POABackend.Auth.Guardian.encode_and_sign(user, %{}, ttl: @token_default_ttl)

{:ok, result} =
%{token: token}
Expand Down Expand Up @@ -91,6 +93,32 @@ defmodule POABackend.Auth.Router do
end
end

post "/blacklist/token" do
with {"authorization", "Basic " <> base64} <- List.keyfind(conn.req_headers, "authorization", 0),
{:ok, decoded64} <- Base.decode64(base64),
[admin_name, admin_password] <- String.split(decoded64, ":"),
true <- conn.params["token"] != nil,
{:ok, :valid} <- Auth.authenticate_admin(admin_name, admin_password)
do
case Auth.valid_token?(conn.params["token"]) do
false ->
send_resp(conn, 404, "")
true ->
{:ok, _} = Auth.create_banned_token(conn.params["token"])
send_resp(conn, 200, "")
end
else
false ->
conn
|> send_resp(404, "")
|> halt
_error ->
conn
|> send_resp(401, "")
|> halt
end
end

match _ do
send_resp(conn, 404, "")
end
Expand Down
3 changes: 2 additions & 1 deletion lib/poa_backend/auth/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ defmodule POABackend.Auth.Supervisor do

children = [
supervisor(Auth.Repo, []),
Plug.Adapters.Cowboy.child_spec(scheme: rest_options[:scheme], plug: Auth.Router, options: cowboy_options)
Plug.Adapters.Cowboy.child_spec(scheme: rest_options[:scheme], plug: Auth.Router, options: cowboy_options),
worker(Auth.BanTokensServer, [])
]

opts = [strategy: :one_for_one]
Expand Down
12 changes: 12 additions & 0 deletions priv/repo/migrations/20180809170931_create_banned_tokens.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule POABackend.Auth.Repo.Migrations.CreateBannedTokens do
use Ecto.Migration

def change do
create_if_not_exists table(:banned_tokens, primary_key: false) do
add :token, :string, primary_key: true
add :expires, :integer

timestamps()
end
end
end
1 change: 1 addition & 0 deletions test/ancillary/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule POABackend.Ancillary.Utils do

def clear_db do
:mnesia.clear_table(:users)
:mnesia.clear_table(:banned_tokens)
end

end

0 comments on commit 2cfd02b

Please sign in to comment.