Skip to content

Commit

Permalink
Merge pull request #80 from fleetyards/sso
Browse files Browse the repository at this point in the history
Sso
  • Loading branch information
kloenk authored Jul 20, 2023
2 parents 07dd83b + a789520 commit a0f0d9c
Show file tree
Hide file tree
Showing 39 changed files with 1,344 additions and 1,302 deletions.
1 change: 1 addition & 0 deletions apps/ex_fleet_yards/lib/ex_fleet_yards/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule ExFleetYards.Application do
# Start the Ecto repository
ExFleetYards.Vault,
ExFleetYards.Repo,
ExFleetYards.Token.Cache,
# Start the PubSub system
{Phoenix.PubSub, name: ExFleetYards.PubSub}
# Start a worker by calling: ExFleetYards.Worker.start_link(arg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule ExFleetYards.Oauth.ResourceOwners do
alias ExFleetYards.Repo.Account
alias ExFleetYards.Repo

@impl Borta.Oauth.ResourceOwners
@impl Boruta.Oauth.ResourceOwners
def get_by(username: username) do
case Account.get_user(username) do
%Account.User{} = user ->
Expand All @@ -17,7 +17,7 @@ defmodule ExFleetYards.Oauth.ResourceOwners do
end
end

@impl Borta.Oauth.ResourceOwners
@impl Boruta.Oauth.ResourceOwners
def get_by(sub: uuid) do
Repo.get(Account.User, uuid)
|> case do
Expand All @@ -30,7 +30,7 @@ defmodule ExFleetYards.Oauth.ResourceOwners do
end
end

@impl Borta.Oauth.ResourceOwners
@impl Boruta.Oauth.ResourceOwners
def check_password(resource_owner, password) do
user = Repo.get(Account.User, resource_owner.sub)

Expand Down Expand Up @@ -66,7 +66,8 @@ defmodule ExFleetYards.Oauth.ResourceOwners do
]
end

defp add_claims(resource_owner, user, [scope | scopes]) do
# Get all non handled scopes
defp add_claims(resource_owner, user, [_scope | scopes]) do
add_claims(resource_owner, user, scopes)
end

Expand Down
62 changes: 62 additions & 0 deletions apps/ex_fleet_yards/lib/ex_fleet_yards/plugs/api_authorization.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
defmodule ExFleetYards.Plugs.ApiAuthorization do
@moduledoc """
This module provides plugs for authorization and authentication.
## Plugs
### `require_authenticated/2`
Ensures that the user is authenticated by checking for a valid bearer token in
the `authorization` header.
It assigns the current token and user to the connection.
### `authorize/2`
Ensures that the user has the required OAuth2 scopes.
Takes a list of required scopes as a parameter.
"""

@doc """
Ensures that the user is authenticated by checking for a valid bearer token in
the `authorization` header.
It assigns the current token and user to the connection.
"""
@callback require_authenticated(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t()

@doc """
Ensures that the user has the required OAuth2 scopes.
Takes a list of required scopes as a parameter.
"""
@callback authorize(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t()

@doc """
Ensures that the user is authenticated by checking for a valid bearer token in
the `authorization` header.
It assigns the current token and user to the connection.
"""
def require_authenticated(conn, scopes) do
impl().require_authenticated(conn, scopes)
end

@doc """
Ensures that the user has the required OAuth2 scopes.
Takes a list of required scopes as a parameter.
"""
def authorize(conn, scopes) do
impl().authorize(conn, scopes)
end

defp impl,
do:
ExFleetYards.Config.get(
:ex_fleet_yards,
:authorization_module,
ExFleetYards.Plugs.AuthorizationBoruta
)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
defmodule ExFleetYards.Plugs.AuthorizationBoruta do
@moduledoc """
This module provides plugs for authorization and authentication.
## Plugs
### `require_authenticated/2`
Ensures that the user is authenticated by checking for a valid bearer token in
the `authorization` header.
It assigns the current token and user to the connection.
### `authorize/2`
Ensures that the user has the required OAuth2 scopes.
Takes a list of required scopes as a parameter.
"""

import Plug.Conn

alias ExFleetYards.Repo.Account

alias Boruta.Oauth.Authorization
alias Boruta.Oauth.Scope

@doc """
Ensures that the user is authenticated by checking for a valid bearer token in
the `authorization` header.
It assigns the current token and user to the connection.
"""
@spec require_authenticated(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t()
def require_authenticated(conn, _opts) do
with [authorization_header] <- get_req_header(conn, "authorization"),
[_auth_header, bearer] <- Regex.run(~r/^Bearer\s+(.+)$/, authorization_header),
{:ok, token} <- Authorization.AccessToken.authorize(value: bearer) do
conn
|> assign(:current_token, token)
|> assign(:current_user, Account.get_user_by_sub(token.sub))
else
_ ->
conn
|> put_status(:unauthorized)
|> Phoenix.Controller.put_view(ExFleetYardsApi.ErrorJson)
|> Phoenix.Controller.render("401.json")
|> halt()
end
end

@doc """
Ensures that the user has the required OAuth2 scopes.
Takes a list of required scopes as a parameter.
"""
@spec authorize(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t()
def authorize(conn, [_h | _t] = required_scopes) do
current_scopes = Scope.split(conn.assigns[:current_token].scope)

case Enum.empty?(required_scopes -- current_scopes) do
true ->
conn

false ->
conn
|> put_status(:forbidden)
|> Phoenix.Controller.put_view(ExFleetYardsApi.ErrorView)
|> Phoenix.Controller.render("403.json",
message: "You do not have the required scopes to access this resource"
)
|> halt()
end
end
end
67 changes: 5 additions & 62 deletions apps/ex_fleet_yards/lib/ex_fleet_yards/repo/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule ExFleetYards.Repo.Account do
"""

alias ExFleetYards.Repo
alias ExFleetYards.Repo.Account.{User, UserToken}
alias ExFleetYards.Repo.Account.User
import Ecto.Query

## Database getters
Expand Down Expand Up @@ -87,23 +87,6 @@ defmodule ExFleetYards.Repo.Account do
Repo.delete(user)
end

@spec confirm_user_by_token(String.t()) ::
{:ok, User.t()}
| {:error, Ecto.Changeset.t() | :not_found}
def confirm_user_by_token(token) do
with %UserToken{} = token <- get_user_by_token(token, "confirm"),
{:ok, user} <- confirm_user(token.user),
{:ok, _} <- Repo.delete(token) do
{:ok, user}
else
nil ->
{:error, :not_found}

{:error, _} = error ->
error
end
end

def confirm_user(user, time \\ NaiveDateTime.utc_now())

def confirm_user(id, time) when is_binary(id) do
Expand All @@ -117,50 +100,10 @@ defmodule ExFleetYards.Repo.Account do
|> Repo.update()
end

def get_api_token(user, scopes) do
{token, user_token} = UserToken.build_api_token(user, scopes)
Repo.insert!(user_token)
token
end

def get_auth_token(user) do
{token, user_token} = UserToken.build_auth_token(user)
Repo.insert!(user_token)

User.login_changeset(user)
|> Repo.update()

token
end

def create_confirm_token(user) do
{token, user_token} = UserToken.create_confirm_token(user)
Repo.insert!(user_token)
token
end

def delete_token(token, context \\ "api") do
Repo.delete_all(
from(t in UserToken,
where: t.token == ^token and t.context == ^context
)
)
end

def send_confirm_token(user) do
token = create_confirm_token(user)
send_confirm_token(user, token)
end

def send_confirm_token(user, token) do
end

def get_user_by_token(token, context \\ "api") do
UserToken.verify_hashed_token(token, context)
|> case do
{:ok, query} -> Repo.one(query)
:error -> nil
end
def update_last_signin!(%User{} = user, conn) do
user
|> User.login_changeset(conn)
|> Repo.update!()
end

defp user_query_set_confirmed(query, true) do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
defmodule ExFleetYards.Repo.Account.TokenRevocation do
@moduledoc """
Revoked token
"""
use TypedEctoSchema

import Ecto.Changeset
import Ecto.Query

alias ExFleetYards.Repo
alias ExFleetYards.Repo.Account

@primary_key {:id, Ecto.UUID, []}

typed_schema "user_token_revocations" do
belongs_to :user, Account.User, type: Ecto.UUID
field :jti, :string
field :iat, :integer
field :exp, :integer

timestamps(updated_at: false)
end

@doc """
Revoke a token.
This should not be called, but use ExFleetYards.Token.revoke_token/1
"""
def revoke_token(attrs) do
revoke_token_changeset(attrs)
|> Repo.insert()
end

@doc """
Revoke all old tokens of a user.
This should not be called, but use ExFleetYards.Token.revoke_user/1
"""
def revoke_user(user_id) when is_binary(user_id) do
%__MODULE__{}
|> cast(
%{
user_id: user_id,
iat: Joken.current_time(),
exp: Joken.current_time() + ExFleetYards.Token.revoke_exp()
},
[:user_id, :iat, :exp]
)
|> Repo.insert()
end

@doc """
Revoke a specific token (changeset)
"""
def revoke_token_changeset(token \\ %__MODULE__{}, attrs) do
token
|> cast(attrs, [:user_id, :jti, :exp])
|> validate_required([:exp, :jti])
|> unique_constraint(:jti)
end

def verify_token_query(%{"sub" => user_id, "iat" => iat, "jti" => jti}) do
from t in __MODULE__, where: t.jti == ^jti or (t.user_id == ^user_id and t.iat >= ^iat)
end

@doc """
Returns true if the token is not revoked
"""
def verify_token(token) do
!Repo.exists?(verify_token_query(token))
end
end
12 changes: 11 additions & 1 deletion apps/ex_fleet_yards/lib/ex_fleet_yards/repo/account/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ defmodule ExFleetYards.Repo.Account.User do
field :sign_in_count, :integer
field :current_sign_in_at, :naive_datetime
field :last_sign_in_at, :naive_datetime
field :last_sign_in_ip, :string
field :current_sign_in_ip, :string
field :confirmation_token, :string, redact: true
field :confirmed_at, :naive_datetime
Expand Down Expand Up @@ -200,6 +201,15 @@ defmodule ExFleetYards.Repo.Account.User do

def login_changeset(user) do
user
|> change(last_sign_in_at: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second))
|> change(sign_in_count: user.sign_in_count + 1)
|> change(last_sign_in_at: user.current_sign_in_at)
|> change(last_sign_in_ip: user.current_sign_in_ip)
|> change(current_sign_in_at: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second))
end

def login_changeset(user, conn) do
user
|> login_changeset()
|> change(current_sign_in_ip: to_string(:inet_parse.ntoa(conn.remote_ip)))
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ defmodule ExFleetYards.Repo.Account.User.Totp do
|> Repo.insert(returning: [:id])
end

def user_query(user_id, active \\ true)

def user_query(user_id, nil) when is_binary(user_id) do
from(t in __MODULE__, where: t.user_id == ^user_id)
end

def user_query(user_id, active \\ true) when is_binary(user_id) and is_boolean(active) do
def user_query(user_id, active) when is_binary(user_id) and is_boolean(active) do
user_query(user_id, nil)
|> where(active: ^active)
end
Expand Down Expand Up @@ -101,7 +103,7 @@ defmodule ExFleetYards.Repo.Account.User.Totp do

def use_code(%__MODULE__{totp_secret: secret, last_used: last_used} = totp, code) do
with true <- NimbleTOTP.valid?(secret, code, since: last_used),
{:ok, totp} <- update_last_used(totp) do
{:ok, _totp} <- update_last_used(totp) do
:ok
else
_ ->
Expand Down
Loading

0 comments on commit a0f0d9c

Please sign in to comment.