[#45] Endpoint for blacklist tokens
This commit is contained in:
parent
7d24f6807f
commit
2cfd02b664
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
# ---------------------------------------
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -3,6 +3,7 @@ defmodule POABackend.Ancillary.Utils do
|
|||
|
||||
def clear_db do
|
||||
:mnesia.clear_table(:users)
|
||||
:mnesia.clear_table(:banned_tokens)
|
||||
end
|
||||
|
||||
end
|
|
@ -434,6 +434,161 @@ defmodule Auth.APITest do
|
|||
assert {404, :nobody} == result
|
||||
end
|
||||
|
||||
# ----------------------------------------
|
||||
# /blackmail/token Endpoint Tests
|
||||
# ----------------------------------------
|
||||
|
||||
test "Ban a token correctly [JSON]" do
|
||||
mime_type = "application/json"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@user <> ":" <> @password)}
|
||||
]
|
||||
|
||||
{200, %{"token" => jwt_token}} =
|
||||
%{:'agent-id' => "agentID"}
|
||||
|> Poison.encode!()
|
||||
|> post(@base_url <> "/session", headers)
|
||||
|
||||
assert Auth.valid_token?(jwt_token)
|
||||
|
||||
blacklist_url = @base_url <> "/blacklist/token"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@admin <> ":" <> @admin_pwd)}
|
||||
]
|
||||
|
||||
{200, :nobody} =
|
||||
%{:token => jwt_token}
|
||||
|> Poison.encode!()
|
||||
|> post(blacklist_url, headers)
|
||||
|
||||
refute Auth.valid_token?(jwt_token)
|
||||
end
|
||||
|
||||
test "Ban a token correctly [MSGPACK]" do
|
||||
mime_type = "application/msgpack"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@user <> ":" <> @password)}
|
||||
]
|
||||
|
||||
{200, %{"token" => jwt_token}} =
|
||||
%{:'agent-id' => "agentID"}
|
||||
|> Msgpax.pack!()
|
||||
|> post(@base_url <> "/session", headers)
|
||||
|
||||
assert Auth.valid_token?(jwt_token)
|
||||
|
||||
blacklist_url = @base_url <> "/blacklist/token"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@admin <> ":" <> @admin_pwd)}
|
||||
]
|
||||
|
||||
{200, :nobody} =
|
||||
%{:token => jwt_token}
|
||||
|> Msgpax.pack!()
|
||||
|> post(blacklist_url, headers)
|
||||
|
||||
refute Auth.valid_token?(jwt_token)
|
||||
end
|
||||
|
||||
test "Ban an invalid token [JSON]" do
|
||||
mime_type = "application/json"
|
||||
url = @base_url <> "/blacklist/token"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@admin <> ":" <> @admin_pwd)}
|
||||
]
|
||||
|
||||
result =
|
||||
%{:token => "badtoken"}
|
||||
|> Poison.encode!()
|
||||
|> post(url, headers)
|
||||
|
||||
assert result == {404, :nobody}
|
||||
end
|
||||
|
||||
test "Ban an invalid token [MSGPACK]" do
|
||||
mime_type = "application/msgpack"
|
||||
url = @base_url <> "/blacklist/token"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@admin <> ":" <> @admin_pwd)}
|
||||
]
|
||||
|
||||
result =
|
||||
%{:token => "badtoken"}
|
||||
|> Msgpax.pack!()
|
||||
|> post(url, headers)
|
||||
|
||||
assert result == {404, :nobody}
|
||||
end
|
||||
|
||||
test "Ban token with wrong Admin credentials [JSON]" do
|
||||
mime_type = "application/json"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@user <> ":" <> @password)}
|
||||
]
|
||||
|
||||
{200, %{"token" => jwt_token}} =
|
||||
%{:'agent-id' => "agentID"}
|
||||
|> Poison.encode!()
|
||||
|> post(@base_url <> "/session", headers)
|
||||
url = @base_url <> "/blacklist/token"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@admin <> ":" <> "wrongpassword")}
|
||||
]
|
||||
|
||||
result =
|
||||
%{:token => jwt_token}
|
||||
|> Poison.encode!()
|
||||
|> post(url, headers)
|
||||
|
||||
assert {401, :nobody} == result
|
||||
end
|
||||
|
||||
test "Ban token with wrong Admin credentials [MSGPACK]" do
|
||||
mime_type = "application/msgpack"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@user <> ":" <> @password)}
|
||||
]
|
||||
|
||||
{200, %{"token" => jwt_token}} =
|
||||
%{:'agent-id' => "agentID"}
|
||||
|> Msgpax.pack!()
|
||||
|> post(@base_url <> "/session", headers)
|
||||
url = @base_url <> "/blacklist/token"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@admin <> ":" <> "wrongpassword")}
|
||||
]
|
||||
|
||||
result =
|
||||
%{:token => jwt_token}
|
||||
|> Msgpax.pack!()
|
||||
|> post(url, headers)
|
||||
|
||||
assert {401, :nobody} == result
|
||||
end
|
||||
|
||||
test "Ban token without token field [JSON]" do
|
||||
mime_type = "application/json"
|
||||
url = @base_url <> "/blacklist/token"
|
||||
headers = [
|
||||
{"Content-Type", mime_type},
|
||||
{"authorization", "Basic " <> Base.encode64(@admin <> ":" <> @admin_pwd)}
|
||||
]
|
||||
|
||||
result = post("", url, headers)
|
||||
|
||||
assert {404, :nobody} == result
|
||||
end
|
||||
|
||||
# ----------------------------------------
|
||||
# Internal functions
|
||||
# ----------------------------------------
|
||||
|
|
|
@ -13,6 +13,10 @@ defmodule Auth.AuthTest do
|
|||
[]
|
||||
end
|
||||
|
||||
# ----------------------------------------
|
||||
# User Tests
|
||||
# ----------------------------------------
|
||||
|
||||
test "create a new user" do
|
||||
alias Comeonin.Bcrypt
|
||||
user_name = "ferigis"
|
||||
|
@ -90,4 +94,56 @@ defmodule Auth.AuthTest do
|
|||
|
||||
refute user_name == user_name2
|
||||
end
|
||||
|
||||
# ----------------------------------------
|
||||
# Token Tests
|
||||
# ----------------------------------------
|
||||
|
||||
test "create a banned token" do
|
||||
# first we need a user
|
||||
user_name = "ferigis"
|
||||
password = "mypassword"
|
||||
|
||||
{:ok, user} = Auth.create_user(user_name, password)
|
||||
|
||||
# generate a token
|
||||
{_, jwt_token, _} = Auth.Guardian.encode_and_sign(user)
|
||||
|
||||
{:ok, _token} = Auth.create_banned_token(jwt_token)
|
||||
{:error, :already_exists} = Auth.create_banned_token(jwt_token)
|
||||
end
|
||||
|
||||
test "create a banned token with a non JWT token" do
|
||||
{:error, %ArgumentError{}} = Auth.create_banned_token("wrongJWTToken")
|
||||
end
|
||||
|
||||
test "delete expired banned tokens" do
|
||||
current_time = :os.system_time(:seconds)
|
||||
|
||||
{:ok, token1} = Auth.create_banned_token("token1", current_time - 1000)
|
||||
{:ok, token2} = Auth.create_banned_token("token2", current_time + 1000)
|
||||
{:ok, token3} = Auth.create_banned_token("token3", current_time - 1000)
|
||||
{:ok, token4} = Auth.create_banned_token("token4", current_time - 1000)
|
||||
|
||||
all = Auth.Repo.all(Auth.Models.Token)
|
||||
|
||||
assert 4 == length(all)
|
||||
assert Enum.member?(all, token1)
|
||||
assert Enum.member?(all, token2)
|
||||
assert Enum.member?(all, token3)
|
||||
assert Enum.member?(all, token4)
|
||||
|
||||
# sending a message to the process which cleans the DB
|
||||
POABackend.Auth.BanTokensServer
|
||||
|> Process.whereis()
|
||||
|> send(:purge)
|
||||
|
||||
Process.sleep(1000) # wait a little until the server cleans the DB
|
||||
|
||||
all = Auth.Repo.all(Auth.Models.Token)
|
||||
|
||||
assert 1 == length(all)
|
||||
assert Enum.member?(all, token2)
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue