Skip to content

Commit

Permalink
Merge pull request #11 from epochtalk/auth-prep
Browse files Browse the repository at this point in the history
Auth prep
  • Loading branch information
unenglishable authored Oct 25, 2022
2 parents 08e596a + e9b0dd0 commit 84d8a75
Show file tree
Hide file tree
Showing 49 changed files with 2,678 additions and 374 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ epochtalk_server-*.tar

# Ignore generated seed files
priv/repo/seeds/permissions.json

# Ignore secret config files
config/*.secret.exs
25 changes: 25 additions & 0 deletions .iex.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
IEx.configure(inspect: [charlists: :as_lists])
IEx.configure(inspect: [limit: :infinity])

alias EpochtalkServer.Repo

alias EpochtalkServer.Models.{
Ban,
BannedAddress,
Board,
BoardMapping,
BoardModerator,
Category,
Configuration,
Invitation,
MetadataBoard,
Permission,
Preference,
Profile,
Role,
RolePermission,
RoleUser,
User
}

reload = fn() -> r Enum.map(__ENV__.aliases, fn {_, module} -> module end) end
67 changes: 58 additions & 9 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,38 @@
import Config

config :epochtalk_server,
ecto_repos: [EpochtalkServer.Repo]
ecto_repos: [EpochtalkServer.Repo],
frontend_config: %{
frontend_url: "http://localhost:8000",
backend_url: "http://localhost:4000",
newbie_enabled: false,
login_required: false,
invite_only: false,
verify_registration: true,
post_max_length: 10_000,
max_image_size: 10_485_760,
max_avatar_size: 102_400,
mobile_break_width: 767,
ga_key: "UA-XXXXX-Y",
revision: nil,
website: %{
title: "Epochtalk Forums",
description: "Open source forum software",
keywords: "open source, free forum, forum software, forum",
logo: nil,
favicon: nil,
default_avatar: "/images/avatar.png",
default_avatar_shape: "circle"
},
portal: %{enabled: false, board_id: nil},
emailer: %{ses_mode: false, options: %{from_address: "[email protected]"}},
images: %{s3_mode: false, options: %{ local_host: "http://localhost:4000" }},
rate_limiting: %{}
}

# Configure Guardian
config :epochtalk_server, EpochtalkServer.Auth.Guardian,
issuer: "EpochtalkServer",
# TODO: configure this at runtime through env
secret_key: "Secret key. You can use `mix guardian.gen.secret` to get one"

# Configure Guardian.DB
Expand All @@ -22,13 +48,18 @@ config :guardian, Guardian.DB,
# schema_name: "guardian_tokens" # default
# token_types: ["refresh_token"] # store all token types if not set

# Configure GuardianRedis
# Configure GuardianRedis (for auth)
# (implementation of Guardian.DB storage in redis)
config :guardian_redis, :redis,
host: "127.0.0.1",
port: 6379,
pool_size: 10

# Configures redix (for sessions storage)
config :epochtalk_server, :redix,
host: "127.0.0.1",
name: :redix

# Configures the endpoint
config :epochtalk_server, EpochtalkServerWeb.Endpoint,
url: [host: "localhost"],
Expand All @@ -43,20 +74,38 @@ config :epochtalk_server, EpochtalkServerWeb.Endpoint,

# Configures the mailer
#
# By default it uses the "Local" adapter which stores the emails
# locally. You can see the emails in your browser, at "/dev/mailbox".
# By default "SMTP" adapter is being used.
#
# For Development `config/dev.exs` loads the "Local" adapter which allows preview
# of sent emails at the url `/dev/mailbox`. To test SMTP in Development mode,
# mailer configurations for adapter, credentials and other options can be
# overriden in `config/dev.secret.exs`
#
# For production it's recommended to configure a different adapter
# at the `config/runtime.exs`.
config :epochtalk_server, EpochtalkServer.Mailer, adapter: Swoosh.Adapters.Local
# For production configurations are fetched from system environment variables.
# Overrides for production are in `config/runtime.exs`.
config :epochtalk_server, EpochtalkServer.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: "smtp.example.com",
username: "username",
password: "password",
ssl: true,
tls: :if_available,
auth: :always,
port: 465,
retries: 2,
no_mx_lookups: false
# dkim: [
# s: "default", d: "domain.com",
# private_key: {:pem_plain, File.read!("priv/keys/domain.private")}
# ]

# Swoosh API client is needed for adapters other than SMTP.
config :swoosh, :api_client, false

# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
metadata: [:request_id, :remote_ip]

# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason
Expand Down
7 changes: 7 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ config :epochtalk_server, EpochtalkServerWeb.Endpoint,
secret_key_base: "9ORa6oGSN+xlXNedSn0gIKVc/6//naQqSiZsRJ8vNbcvHpPOTPMLgcn134WIH3Pd",
watchers: []

# Configure Local Mailer by default for dev mode (this can be overridden in dev.secret.exs)
config :epochtalk_server, EpochtalkServer.Mailer, adapter: Swoosh.Adapters.Local

# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
Expand Down Expand Up @@ -59,3 +62,7 @@ config :phoenix, :stacktrace_depth, 20

# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime

if File.exists?("config/dev.secret.exs") do
import_config "dev.secret.exs"
end
23 changes: 23 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,27 @@ if config_env() == :prod do
# config :swoosh, :api_client, Swoosh.ApiClient.Hackney
#
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
config :epochtalk_server, EpochtalkServer.Mailer,
relay: System.get_env("EMAILER_SMTP_RELAY") || "smtp.example.com",
username: System.get_env("EMAILER_SMTP_USERNAME") || "username",
password: System.get_env("EMAILER_SMTP_PASSWORD") || "password",
port: System.get_env("EMAILER_SMTP_PORT") || 465

# Configure Guardian for Runtime
config :epochtalk_server, EpochtalkServer.Auth.Guardian,
secret_key: System.get_env("GUARDIAN_SECRET_KEY") ||
raise """
environment variable GUARDIAN_SECRET_KEY is missing.
You can generate one by calling: mix guardian.gen.secret
"""

# Configure Guardian Redis
config :guardian_redis, :redis,
host: System.get_env("REDIS_HOST") || "127.0.0.1",
port: String.to_integer(System.get_env("REDIS_PORT") || "6379"),
pool_size: String.to_integer(System.get_env("REDIS_POOL_SIZE") || "10")

# Configure Redis for Session Storage
config :epochtalk_server, :redix,
host: System.get_env("REDIS_HOST") || "127.0.0.1"
end
17 changes: 17 additions & 0 deletions lib/epochtalk_server/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@ defmodule EpochtalkServer.Application do
@moduledoc false

use Application
@env Mix.env()

@impl true
def start(_type, _args) do
# Set Environment to config
# Work around for dialyzer rendering @env as :dev
Application.put_env(:epochtalk_server, :env, @env)
children = [
# Start Guardian Redis Redix connection
GuardianRedis.Redix,
# Start the server Redis connection
{Redix, host: redix_config()[:host], name: redix_config()[:name]},
# Start the Ecto repository
EpochtalkServer.Repo,
# Warm frontend_config variable (referenced by api controllers)
# This worker starts, does its thing and dies
{Task, &EpochtalkServer.Models.Configuration.warm_frontend_config/0},
# Start the Telemetry supervisor
EpochtalkServerWeb.Telemetry,
# Start the PubSub system
Expand All @@ -22,6 +31,11 @@ defmodule EpochtalkServer.Application do
# {EpochtalkServer.Worker, arg}
]

# don't run config_warmer during tests
children = if Application.get_env(:epochtalk_server, :env) == :test,
do: List.delete(children, {Task, &EpochtalkServer.Models.Configuration.warm_frontend_config/0}),
else: children

# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: EpochtalkServer.Supervisor]
Expand All @@ -35,4 +49,7 @@ defmodule EpochtalkServer.Application do
EpochtalkServerWeb.Endpoint.config_change(changed, removed)
:ok
end

# fetch redix config
defp redix_config(), do: Application.get_env(:epochtalk_server, :redix)
end
49 changes: 35 additions & 14 deletions lib/epochtalk_server/auth/guardian.ex
Original file line number Diff line number Diff line change
@@ -1,32 +1,53 @@
defmodule EpochtalkServer.Auth.Guardian do
use Guardian, otp_app: :epochtalk_server
alias EpochtalkServer.Models.User
alias EpochtalkServer.Models.Role

def subject_for_token(%{user_id: user_id}, _claims) do
def subject_for_token(%{user_id: user_id, session_id: session_id}, _claims) do
# You can use any value for the subject of your token but
# it should be useful in retrieving the resource later, see
# how it being used on `resource_from_claims/1` function.
# A unique `id` is a good subject, a non-unique email address
# is a poor subject.
sub = to_string(user_id)
sub = to_string(user_id) <> ":" <> session_id
{:ok, sub}
end
def subject_for_token(_, _) do
{:error, :reason_for_error}
end
def subject_for_token(_, _), do: {:error, :reason_for_error}

def resource_from_claims(%{"sub" => user_id}) do
def resource_from_claims(%{"sub" => sub}) do
# Here we'll look up our resource from the claims, the subject can be
# found in the `"sub"` key. In above `subject_for_token/2` we returned
# the resource id so here we'll rely on that to look it up.
resource = user_id
|> String.to_integer
|> User.by_id
{:ok, resource}
end
def resource_from_claims(_claims) do
{:error, :reason_for_error}

# extract user_id, session_id from subject
[user_id, session_id] = String.split(sub, ":")

# check if session is active in redis
Redix.command!(:redix, ["SISMEMBER", "user:#{user_id}:sessions", session_id])
|> case do
0 ->
# session is not active, return error
{:error, "No session with id #{session_id}"}
1 ->
# session is active, populate data
resource = %{
id: user_id,
session_id: session_id,
username: Redix.command!(:redix, ["HGET", "user:#{user_id}", "username"]),
avatar: Redix.command!(:redix, ["HGET", "user:#{user_id}", "avatar"]),
roles: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:roles"]) |> Role.by_lookup
}
# only append moderating, ban_expiration and malicious_score if present
moderating = Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:moderating"])
resource = if moderating && length(moderating) != 0,
do: Map.put(resource, :moderating, moderating), else: resource
resource = if (ban_expiration = Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "ban_expiration"])) != 0,
do: Map.put(resource, :ban_expiration, ban_expiration), else: resource
resource = if (malicious_score = Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "malicious_score"])) != 0,
do: Map.put(resource, :malicious_score, malicious_score), else: resource
{:ok, resource}
end
end
def resource_from_claims(_claims), do: {:error, :reason_for_error}

def after_encode_and_sign(resource, claims, token, _options) do
with {:ok, _} <- Guardian.DB.after_encode_and_sign(resource, claims["typ"], claims, token) do
Expand Down
Loading

0 comments on commit 84d8a75

Please sign in to comment.