Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion apps/authenticator/lib/sessions.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
defmodule Authenticator.Sessions do
@moduledoc false
@moduledoc """
An sesssion is a representation of an succeded authentication by a subject.

The session in this application stores most of the info needed to check if
an access_token still active or not. This can be done by checking two infos:
- The Session status has to be `active`;
- The session expiration cannot be higher than actual date (UTC);
"""

use Authenticator.Domain, schema_model: Authenticator.Sessions.Schemas.Session

Expand Down
9 changes: 9 additions & 0 deletions apps/authenticator/lib/sign_in/application_attempts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Authenticator.SignIn.ApplicationAttempts do
@moduledoc """
A application attemp is a representation of an valid or not sign in attempt by the application;

This is used in order to check if the application should be temporarilly blocked or not.
"""

use Authenticator.Domain, schema_model: Authenticator.SignIn.Schemas.ApplicationAttempt
end
45 changes: 34 additions & 11 deletions apps/authenticator/lib/sign_in/commands/client_credentials.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ defmodule Authenticator.SignIn.Commands.ClientCredentials do
require Logger

alias Authenticator.Ports.ResourceManager, as: Port
alias Authenticator.Sessions
alias Authenticator.{Repo, Sessions}
alias Authenticator.Sessions.Tokens.{AccessToken, ClientAssertion, RefreshToken}
alias Authenticator.SignIn.ApplicationAttempts
alias Authenticator.SignIn.Inputs.ClientCredentials, as: Input
alias Ecto.Multi
alias ResourceManager.Permissions.Scopes

@behaviour Authenticator.SignIn.Commands.Behaviour
Expand All @@ -39,11 +41,11 @@ defmodule Authenticator.SignIn.Commands.ClientCredentials do
with {:app, {:ok, app}} <- {:app, Port.get_identity(%{client_id: client_id})},
{:flow_enabled?, true} <- {:flow_enabled?, "client_credentials" in app.grant_flows},
{:app_active?, true} <- {:app_active?, app.status == "active"},
{:secret_matches?, true} <- {:secret_matches?, secret_matches?(app, input)},
{:valid_protocol?, true} <- {:valid_protocol?, app.protocol == "openid-connect"},
{:secret_matches?, true} <- {:secret_matches?, secret_matches?(app, input)},
{:ok, access_token, claims} <- generate_access_token(app, scope),
{:ok, refresh_token, _} <- generate_refresh_token(app, claims),
{:ok, _session} <- generate_session(claims) do
{:ok, _session} <- generate_and_save(input, claims) do
{:ok, parse_response(access_token, refresh_token, claims)}
else
{:app, {:error, :not_found}} ->
Expand All @@ -58,18 +60,15 @@ defmodule Authenticator.SignIn.Commands.ClientCredentials do
Logger.info("Client application #{client_id} is not active")
{:error, :unauthenticated}

{:secret_matches?, false} ->
Logger.info("Client application #{client_id} credential didn't matches")
{:error, :unauthenticated}

{:confidential?, false} ->
Logger.info("Client application #{client_id} is not confidential")
{:error, :unauthenticated}

{:valid_protocol?, false} ->
Logger.info("Client application #{client_id} protocol is not openid-connect")
{:error, :unauthenticated}

{:secret_matches?, false} ->
Logger.info("Client application #{client_id} credential didn't matches")
generate_attempt(input, false)
{:error, :unauthenticated}

error ->
Logger.error("Failed to run command becuase of unknow error", error: inspect(error))
error
Expand Down Expand Up @@ -155,6 +154,30 @@ defmodule Authenticator.SignIn.Commands.ClientCredentials do
end
end

defp generate_and_save(input, claims) do
Multi.new()
|> Multi.run(:save_attempt, fn _repo, _changes -> generate_attempt(input, true) end)
|> Multi.run(:generate, fn _repo, _changes -> generate_session(claims) end)
|> Repo.transaction()
|> case do
{:ok, %{generate: session}} ->
Logger.info("Succeeds in creating session", id: session.id)
{:ok, session}

{:error, step, reason, _changes} ->
Logger.error("Failed to create session in step #{inspect(step)}", reason: reason)
{:error, reason}
end
end

defp generate_attempt(%{client_id: client_id, ip_address: ip_address}, success?) do
ApplicationAttempts.create(%{
client_id: client_id,
was_successful: success?,
ip_address: ip_address
})
end

defp generate_session(%{"jti" => jti, "sub" => sub, "exp" => exp} = claims) do
Sessions.create(%{
jti: jti,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ defmodule Authenticator.SignIn.Inputs.ClientCredentials do
@possible_grant_type ~w(client_credentials)
@acceptable_assertion_types ~w(urn:ietf:params:oauth:client-assertion-type:jwt-bearer)

@required [:client_id, :grant_type, :scope]
@required [:client_id, :grant_type, :ip_address, :scope]
@optional [:client_secret, :client_assertion, :client_assertion_type]
embedded_schema do
field :client_id, Ecto.UUID
field :grant_type, :string
field :scope, :string
field :ip_address, :string

# Application credentials
field :client_secret, :string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ defmodule Authenticator.SignIn.Inputs.ResourceOwner do
@possible_grant_type ~w(password)
@acceptable_assertion_types ~w(urn:ietf:params:oauth:client-assertion-type:jwt-bearer)

@required [:username, :password, :client_id, :scope, :grant_type]
@required [:username, :password, :client_id, :ip_address, :scope, :grant_type]
@optional [:client_secret, :client_assertion, :client_assertion_type]
embedded_schema do
field :username, :string
field :password, :string
field :grant_type, :string
field :scope, :string
field :client_id, :string
field :ip_address, :string

# Application credentials
field :client_secret, :string
Expand Down
31 changes: 29 additions & 2 deletions apps/authenticator/lib/sign_in/commands/resource_owner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ defmodule Authenticator.SignIn.Commands.ResourceOwner do

alias Authenticator.Crypto.Commands.{FakeVerifyHash, VerifyHash}
alias Authenticator.Ports.ResourceManager, as: Port
alias Authenticator.Sessions
alias Authenticator.{Repo, Sessions}
alias Authenticator.Sessions.Tokens.{AccessToken, ClientAssertion, RefreshToken}
alias Authenticator.SignIn.Inputs.ResourceOwner, as: Input
alias Authenticator.SignIn.UserAttempts
alias Ecto.Multi
alias ResourceManager.Permissions.Scopes

@behaviour Authenticator.SignIn.Commands.Behaviour
Expand Down Expand Up @@ -51,7 +53,7 @@ defmodule Authenticator.SignIn.Commands.ResourceOwner do
{:pass_matches?, true} <- {:pass_matches?, VerifyHash.execute(user, input.password)},
{:ok, access_token, claims} <- generate_access_token(user, app, scope),
{:ok, refresh_token, _} <- generate_refresh_token(app, claims),
{:ok, _session} <- generate_session(claims) do
{:ok, _session} <- generate_and_save(input, claims) do
{:ok, parse_response(access_token, refresh_token, claims)}
else
{:app, {:error, :not_found}} ->
Expand Down Expand Up @@ -96,6 +98,7 @@ defmodule Authenticator.SignIn.Commands.ResourceOwner do

{:pass_matches?, false} ->
Logger.info("User #{username} password do not match any credential")
generate_attempt(input, false)
{:error, :unauthenticated}

error ->
Expand Down Expand Up @@ -185,6 +188,30 @@ defmodule Authenticator.SignIn.Commands.ResourceOwner do
end
end

defp generate_and_save(input, claims) do
Multi.new()
|> Multi.run(:save_attempt, fn _repo, _changes -> generate_attempt(input, true) end)
|> Multi.run(:generate, fn _repo, _changes -> generate_session(claims) end)
|> Repo.transaction()
|> case do
{:ok, %{generate: session}} ->
Logger.info("Succeeds in creating session", id: session.id)
{:ok, session}

{:error, step, reason, _changes} ->
Logger.error("Failed to create session in step #{inspect(step)}", reason: reason)
{:error, reason}
end
end

defp generate_attempt(%{username: username, ip_address: ip_address}, success?) do
UserAttempts.create(%{
username: username,
was_successful: success?,
ip_address: ip_address
})
end

defp generate_session(%{"jti" => jti, "sub" => sub, "exp" => exp} = claims) do
Sessions.create(%{
jti: jti,
Expand Down
50 changes: 50 additions & 0 deletions apps/authenticator/lib/sign_in/schemas/application_attempt.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defmodule Authenticator.SignIn.Schemas.ApplicationAttempt do
@moduledoc """
Application login attempts.

Every time a application sign in on API we save the login attempt in order
to create some rules to detect and prevant attacks.
"""

use Authenticator.Schema

import Ecto.Changeset

@typedoc "Application attempt schema fields"
@type t :: %__MODULE__{
id: binary(),
client_id: String.t(),
was_successful: boolean(),
ip_address: String.t(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}

@required_fields [:client_id, :was_successful, :ip_address]
schema "application_sign_in_attempt" do
field :client_id, :string
field :was_successful, :boolean
field :ip_address, :string

timestamps()
end

@doc false
def changeset_create(params) when is_map(params) do
%__MODULE__{}
|> cast(params, @required_fields)
|> validate_required(@required_fields)
end

@doc false
def changeset_update(%__MODULE__{} = model, params) when is_map(params),
do: cast(model, params, @required_fields)

#################
# Custom filters
#################

defp custom_query(query, {:ids, ids}), do: where(query, [c], c.id in ^ids)
defp custom_query(query, {:created_after, date}), do: where(query, [c], c.inserted_at > ^date)
defp custom_query(query, {:created_before, date}), do: where(query, [c], c.inserted_at < ^date)
end
50 changes: 50 additions & 0 deletions apps/authenticator/lib/sign_in/schemas/user_attempt.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defmodule Authenticator.SignIn.Schemas.UserAttempt do
@moduledoc """
User login attempts.

Every time a user sign in on API we save the login attempt in order
to create some rules to detect and prevant attacks.
"""

use Authenticator.Schema

import Ecto.Changeset

@typedoc "User attempt schema fields"
@type t :: %__MODULE__{
id: binary(),
username: String.t(),
was_successful: boolean(),
ip_address: String.t(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}

@required_fields [:username, :was_successful, :ip_address]
schema "user_sign_in_attempt" do
field :username, :string
field :was_successful, :boolean
field :ip_address, :string

timestamps()
end

@doc false
def changeset_create(params) when is_map(params) do
%__MODULE__{}
|> cast(params, @required_fields)
|> validate_required(@required_fields)
end

@doc false
def changeset_update(%__MODULE__{} = model, params) when is_map(params),
do: cast(model, params, @required_fields)

#################
# Custom filters
#################

defp custom_query(query, {:ids, ids}), do: where(query, [c], c.id in ^ids)
defp custom_query(query, {:created_after, date}), do: where(query, [c], c.inserted_at > ^date)
defp custom_query(query, {:created_before, date}), do: where(query, [c], c.inserted_at < ^date)
end
9 changes: 9 additions & 0 deletions apps/authenticator/lib/sign_in/user_attempts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Authenticator.SignIn.UserAttempts do
@moduledoc """
A user attemp is a representation of an valid or not sign in attempt by the user;

This is used in order to check if the user should be temporarilly blocked or not.
"""

use Authenticator.Domain, schema_model: Authenticator.SignIn.Schemas.UserAttempt
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Authenticator.Repo.Migrations.CreateUserSignInAttemptTable do
use Ecto.Migration

def change do
create_if_not_exists table(:user_sign_in_attempt, primary_key: false) do
add :id, :uuid, primary_key: true
add :username, :string, null: false
add :was_successful, :boolean, null: false
add :ip_address, :string, null: false

timestamps()
end

create_if_not_exists index(:user_sign_in_attempt, [:username, :was_successful, :ip_address])
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Authenticator.Repo.Migrations.CreateApplicationSignInAttemptTable do
use Ecto.Migration

def change do
create_if_not_exists table(:application_sign_in_attempt, primary_key: false) do
add :id, :uuid, primary_key: true
add :client_id, :string, null: false
add :was_successful, :boolean, null: false
add :ip_address, :string, null: false

timestamps()
end

create_if_not_exists index(:application_sign_in_attempt, [:client_id, :was_successful, :ip_address])
end
end
Loading