Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OIDC #374

Merged
merged 16 commits into from
Jul 15, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PG_PASS=password
AUTHENTIK_SECRET_KEY=password
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ npm-debug.log
/.idea
/lib/media_server/release.ex
/rel/overlays/bin/
.env
2 changes: 1 addition & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@ config :logger, :console, format: "[$level] $message\n"
config :phoenix, :stacktrace_depth, 20

# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
config :phoenix, :plug_init_mode, :runtime
89 changes: 88 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
version: "3.4"

volumes:
database:
driver: local
redis:
driver: local

services:

midarr:
Expand All @@ -21,6 +29,14 @@ services:
- SETUP_ADMIN_NAME=admin
- SETUP_ADMIN_PASSWORD=passwordpassword

- OAUTH_ISSUER_URL=http://localhost:9000
- OAUTH_AUTHORIZE_URL=http://localhost:9000/application/o/authorize/
- OAUTH_TOKEN_URL=http://server-authentik:9000/application/o/token/
- OAUTH_CLIENT_ID=someClientId
- OAUTH_CLIENT_SECRET=someClientSecret
- OAUTH_REDIRECT_URI=http://localhost:4000/auth/authentik/callback
- OAUTH_USER_URL=http://server-authentik:9000/application/o/userinfo/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@onedr0p Example environment variables for configuring oauth.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good so far.

- RADARR_BASE_URL=http://radarr:7878
- RADARR_API_KEY=d031e8c9b9df4b2fab311d1c3b3fa2c5
- SONARR_BASE_URL=http://sonarr:8989
Expand Down Expand Up @@ -84,4 +100,75 @@ services:
- PUID=1000
- PGID=1000
healthcheck:
test: "exit 0"
test: "exit 0"

postgresql-authentik:
image: docker.io/library/postgres:12-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
start_period: 20s
interval: 30s
retries: 5
timeout: 5s
volumes:
- database:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
POSTGRES_DB: ${PG_DB:-authentik}
env_file:
- .env

redis-authentik:
image: docker.io/library/redis:alpine
command: --save 60 1 --loglevel warning
healthcheck:
test: [ "CMD-SHELL", "redis-cli ping | grep PONG" ]
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- redis:/data

server-authentik:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.3}
command: server
environment:
AUTHENTIK_REDIS__HOST: redis-authentik
AUTHENTIK_POSTGRESQL__HOST: postgresql-authentik
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
env_file:
- .env
ports:
- "${COMPOSE_PORT_HTTP:-9000}:9000"
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
depends_on:
- postgresql-authentik
- redis-authentik

worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.3}
command: worker
environment:
AUTHENTIK_REDIS__HOST: redis-authentik
AUTHENTIK_POSTGRESQL__HOST: postgresql-authentik
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
# `user: root` and the docker socket volume are optional.
# See more for the docker socket integration here:
# https://goauthentik.io/docs/outposts/integrations/docker
# Removing `user: root` also prevents the worker from fixing the permissions
# on the mounted folders, so when removing this make sure the folders have the correct UID/GID
# (1000:1000 by default)
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
env_file:
- .env
depends_on:
- postgresql-authentik
- redis-authentik
2 changes: 1 addition & 1 deletion lib/media_server_web/components/footer_component.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
) %>
</div>
<a href="https://github.com/midarrlabs" target="_blank" class="text-sm text-center text-zinc-500">
© 2023 Midarr Labs - v3.2.0-beta.1
© 2023 Midarr Labs - v3.2.0-beta.2
</a>
</div>
</div>
Expand Down
57 changes: 57 additions & 0 deletions lib/media_server_web/controllers/auth_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
defmodule MediaServerWeb.AuthController do
use MediaServerWeb, :controller

@doc """
This action is reached via `/auth/:provider` and redirects to the OAuth2 provider
based on the chosen strategy.
"""
def index(conn, %{"provider" => provider}) do
redirect conn, external: authorize_url!(provider)
end

def delete(conn, _params) do
conn
|> put_flash(:info, "You have been logged out!")
|> configure_session(drop: true)
|> redirect(to: "/")
end

@doc """
This action is reached via `/auth/:provider/callback` is the callback URL that
the OAuth2 provider will redirect the user back to with a `code` that will
be used to request an access token. The access token will then be used to
access protected resources on behalf of the user.
"""
def callback(conn, %{"provider" => provider, "code" => code}) do
# Exchange an auth code for an access token
client = get_token!(provider, code)

# Request the user's data with the access token
if user = MediaServer.Accounts.get_user_by_email(get_user!(provider, client).email) do

MediaServerWeb.UserAuth.log_in_user(conn, user)
end
Copy link
Member Author

@trueChazza trueChazza Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@onedr0p

Thoughts on this? Here it gets a user registered in the Midarr database that matches the email returned by Authentik, then logs them in.

What's your take on this, and the auth flow?

If a user returned by Authentik doesn't exist in Midarr - register them, then log them in?

Copy link
Contributor

@onedr0p onedr0p Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't auto-create users since the source of truth is the OIDC provider, meaning to say they would need to be added to the OIDC provider first and if they aren't an existing user they cannot login. Deny by default.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking oauth / OIDC provider would be an optional alternative, with Midarr still the source of truth for user auth. There would need to be some level of user mapping to support both options.

Copy link
Member Author

@trueChazza trueChazza Jul 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now just to keep moving forward - I'll go with the logic as above (user must exist in Midarr and OIDC provider to login).

We can continue to improve the flow in future iterations 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant to comment and say pretty much that. 😅


conn
|> redirect(to: "/")
end

defp authorize_url!("authentik"), do: Authentik.authorize_url!
defp authorize_url!(_), do: raise "No matching provider available"

defp get_token!("authentik", code), do: Authentik.get_token!(code: code)
defp get_token!(_, _), do: raise "No matching provider available"

defp get_user!("authentik", client) do

token = Map.get(client, :token) |> Map.get(:access_token) |> Jason.decode! |> Map.get("access_token")

%{body: user} = OAuth2.Client.get!(client, System.get_env("OAUTH_USER_URL"), [
{"authorization", "Bearer #{ token }"}
])

decoded_user = Jason.decode!(user)

%{name: decoded_user["name"], email: decoded_user["email"]}
end
end
42 changes: 42 additions & 0 deletions lib/media_server_web/oauth/authentik.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule Authentik do
@moduledoc """
An OAuth2 strategy for Authentik.
"""
use OAuth2.Strategy

alias OAuth2.Strategy.AuthCode

defp config do
[
strategy: Authentik,
site: System.get_env("OAUTH_ISSUER_URL"),
authorize_url: System.get_env("OAUTH_AUTHORIZE_URL"),
token_url: System.get_env("OAUTH_TOKEN_URL"),
client_id: System.get_env("OAUTH_CLIENT_ID"),
client_secret: System.get_env("OAUTH_CLIENT_SECRET"),
redirect_uri: System.get_env("OAUTH_REDIRECT_URI")
]
end

def client do
OAuth2.Client.new(config())
end

def authorize_url!(params \\ []) do

Check warning on line 25 in lib/media_server_web/oauth/authentik.ex

View workflow job for this annotation

GitHub Actions / OTP 24 / Elixir 1.14.3

variable "params" is unused (if the variable is not meant to be used, prefix it with an underscore)
OAuth2.Client.authorize_url!(client())
end

def get_token!(params \\ [], headers \\ []) do

Check warning on line 29 in lib/media_server_web/oauth/authentik.ex

View workflow job for this annotation

GitHub Actions / OTP 24 / Elixir 1.14.3

variable "headers" is unused (if the variable is not meant to be used, prefix it with an underscore)
OAuth2.Client.get_token!(client(), params)
end

def authorize_url(client, params) do
AuthCode.authorize_url(client, params)
end

def get_token(client, params, headers) do
client
|> put_header("Accept", "application/json")
|> AuthCode.get_token(params, headers)
end
end
8 changes: 8 additions & 0 deletions lib/media_server_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ defmodule MediaServerWeb.Router do
post "/login", UserSessionController, :create
end

scope "/auth", MediaServerWeb do
pipe_through :browser

get "/:provider", AuthController, :index
get "/:provider/callback", AuthController, :callback
delete "/logout", AuthController, :delete
end

scope "/", MediaServerWeb do
pipe_through [:browser, :require_authenticated_user]

Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ defmodule MediaServer.MixProject do
{:cors_plug, "~> 3.0"},
{:scrivener, "~> 2.0"},
{:extitles, "~> 0.1.0"},
{:exstream, "~> 0.20.0"}
{:exstream, "~> 0.20.0"},
{:oauth2, "~> 2.0"}
]
end

Expand Down
4 changes: 4 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@
"httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"},
"openid_connect": {:hex, :openid_connect, "0.2.2", "c05055363330deab39ffd89e609db6b37752f255a93802006d83b45596189c0b", [:mix], [{:httpoison, "~> 1.2", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "735769b6d592124b58edd0582554ce638524c0214cd783d8903d33357d74cc13"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.7.6", "61f0625af7c1d1923d582470446de29b008c0e07ae33d7a3859ede247ddaf59a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "f6b4be7780402bb060cbc6e83f1b6d3f5673b674ba73cc4a7dd47db0322dfb88"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"},
Expand All @@ -58,6 +61,7 @@
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"tesla": {:hex, :tesla, "1.7.0", "a62dda2f80d4f8a925eb7b8c5b78c461e0eb996672719fe1a63b26321a5f8b4e", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2e64f01ebfdb026209b47bc651a0e65203fcff4ae79c11efb73c4852b00dc313"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"websock": {:hex, :websock, "0.5.2", "b3c08511d8d79ed2c2f589ff430bd1fe799bb389686dafce86d28801783d8351", [:mix], [], "hexpm", "925f5de22fca6813dfa980fb62fd542ec43a2d1a1f83d2caec907483fe66ff05"},
"websock_adapter": {:hex, :websock_adapter, "0.5.3", "4908718e42e4a548fc20e00e70848620a92f11f7a6add8cf0886c4232267498d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "cbe5b814c1f86b6ea002b52dd99f345aeecf1a1a6964e209d208fb404d930d3d"},
Expand Down
Loading