Compare commits

..

2 Commits

Author SHA1 Message Date
bluepython508
eb62b82b33 Nixos module 2023-11-09 23:58:26 +00:00
bluepython508
6fb839824e Add login next url parameter for auth redirects 2023-11-09 17:03:20 +00:00
9 changed files with 116 additions and 21 deletions

View File

@@ -54,4 +54,8 @@ config :phoenix, :json_library, Jason
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
config :wax_, origin: "http://localhost:4000", rp_id: :auto
config :boruta, Boruta.Oauth, repo: SsoBsn.Repo, issuer: "http://localhost:4000/", contexts: [ resource_owners: SsoBsnWeb.ResourceOwners ]
import_config "#{config_env()}.exs" import_config "#{config_env()}.exs"

View File

@@ -26,7 +26,6 @@ config :sso_bsn, SsoBsnWeb.Endpoint,
tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
] ]
config :sso_bsn, :session_domain, "localhost"
# ## SSL Support # ## SSL Support
# #
@@ -77,5 +76,6 @@ config :phoenix, :plug_init_mode, :runtime
# Include HEEx debug annotations as HTML comments in rendered markup # Include HEEx debug annotations as HTML comments in rendered markup
config :phoenix_live_view, :debug_heex_annotations, true config :phoenix_live_view, :debug_heex_annotations, true
config :wax_, origin: "http://localhost:4000", rp_id: :auto config :wax_, origin: "http://localhost:4000"
config :boruta, Boruta.Oauth, repo: SsoBsn.Repo, issuer: "http://localhost:4000/", contexts: [ resource_owners: SsoBsnWeb.ResourceOwners ] config :boruta, Boruta.Oauth, issuer: "http://localhost:4000/"
config :sso_bsn, :session_domain, "localhost"

View File

@@ -16,7 +16,7 @@ import Config
# #
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
# script that automatically sets the env var above. # script that automatically sets the env var above.
if System.get_env("PHX_SERVER") do if System.get_env("SERVER") do
config :sso_bsn, SsoBsnWeb.Endpoint, server: true config :sso_bsn, SsoBsnWeb.Endpoint, server: true
end end
@@ -44,20 +44,20 @@ if config_env() == :prod do
You can generate one by calling: mix phx.gen.secret You can generate one by calling: mix phx.gen.secret
""" """
host = System.get_env("PHX_HOST") || "example.com" host = System.get_env("SSO_BSN_HOST") || raise "SSO_BSN_HOST must be set to the external host of the service"
port = String.to_integer(System.get_env("PORT") || "4000")
config :wax_, origin: "https://#{host}/"
config :boruta, Boruta.Oauth, issuer: "https://#{host}/"
config :sso_bsn, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") config :sso_bsn, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
config :sso_bsn, :session_domain, System.get_env("SESSION_DOMAIN") # A nil value will allow browser default of no subdomains, current domain
config :sso_bsn, SsoBsnWeb.Endpoint, config :sso_bsn, SsoBsnWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"], url: [host: host, port: 443, scheme: "https"],
http: [ http: [
# Enable IPv6 and bind on all interfaces. ip: {:local, System.get_env("BIND_UNIX")},
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. port: 0
# See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: port
], ],
secret_key_base: secret_key_base secret_key_base: secret_key_base

View File

@@ -14,8 +14,9 @@ mixRelease rec {
mixFodDeps = fetchMixDeps { mixFodDeps = fetchMixDeps {
pname = "mix-deps-${pname}"; pname = "mix-deps-${pname}";
inherit version src; inherit version src;
sha256 = "sha256-hQlMy0e1Kv2zBXYCiNHYlIO0N00wuIW8B+ZlsEMTSy0="; sha256 = "sha256-p74p7Dpi1xzddD+dygKF5cSLDATNKRXziKPNQgIhRPc=";
}; };
ELIXIR_MAKE_CACHE_DIR = "/tmp/.elixir-make-cache"; ELIXIR_MAKE_CACHE_DIR = "/tmp/.elixir-make-cache";
meta.mainProgram = "sso_bsn";
} }

View File

@@ -23,5 +23,90 @@
inherit self; inherit self;
}; };
}); });
nixosModules = {
default = { lib, pkgs, config }: let
sso-bsn = self.packages.${pkgs.system}.default;
gen-secret = pkgs.writeShellScript "gen-secret" ''
mkdir -p $1
(umask 077; [ -f $1/$2 ] || ${pkgs.coreutils}/bin/head -c 128 /dev/urandom | ${pkgs.coreutils}/bin/base64 -w0 > $1/$2)
'';
secret = var: dir: file: ''
${gen-secret} "${dir}" "${file}"
${var}=$(${pkgs.coreutils}/bin/cat "${dir}/${file}")
'';
script = pkgs.writeShellScript "sso-bsn" ''
${secret "RELEASE_COOKIE" "/run/sso-bsn" "cookie"} ${lib.getExe sso-bsn} "$@"
'';
cfg = config.services.bluepython508.sso-bsn;
in {
options.services.bluepython508.sso-bsn = {
enable = lib.mkEnableOption "sso-bsn";
host = lib.mkOption { type = lib.types.string; };
session-domain = lib.mkOption { type = with lib.types; nullOr string; };
};
options.services.nginx.virtualHosts = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({ config }: {
options.sso.enable = lib.mkEnableOption "SSO BSN";
config.extraConfig = lib.mkIf config.sso.enable ''
auth_request /__auth_sso_validate;
proxy_set_header X-Auth-Username $auth_resp_username;
location = /__auth_sso_validate {
internal;
proxy_pass https://${cfg.host}/whoami;
proxy_pass_request_body off; # no need to send the POST body
proxy_set_header Content-Length "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
auth_request_set $auth_resp_username $upstream_http_x_auth_username;
}
error_page 401 = @error401;
location @error401 {
return 302 url=https://${cfg.host}/user/log_in?next=$http_host$request_uri;
}
'';
}));
};
config.environment.systemPackages = lib.mkIf cfg.enable [ script ];
config.systemd.services.sso-bsn = lib.mkIf cfg.enable {
description = "sso-bsn";
environment = {
SERVER = "true";
DATABASE_PATH = "/var/lib/sso-bsn/db.sqlite";
BIND_UNIX = "/run/sso-bsn/sock";
SSO_BSN_HOST = cfg.host;
SESSION_DOMAIN = cfg.session-domain;
};
script = ''
${secret "SECRET_KEY_BASE" "/var/lib/sso-bsn" "secret-key-base"}
SECRET_KEY_BASE="$SECRET_KEY_BASE" ${script} start
'';
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
ProtectHome = true;
PrivateUsers = true;
StateDirectory = "sso-bsn";
};
};
config.services.nginx.virtualHosts = lib.mkIf cfg.enable {
${cfg.host} = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://unix:/run/sso-bsn/sock:/";
recommendedProxySettings = true;
proxyWebsockets = true;
};
};
};
};
};
}; };
} }

View File

@@ -9,7 +9,6 @@ defmodule SsoBsnWeb.UserSessionController do
{:ok, user} -> {:ok, user} ->
conn conn
|> UserAuth.log_in_user(user) # TODO: pass through remember-me value? |> UserAuth.log_in_user(user) # TODO: pass through remember-me value?
|> redirect(to: ~p"/users/settings")
{:error, error} -> {:error, error} ->
dbg(error) dbg(error)
conn conn
@@ -32,4 +31,8 @@ defmodule SsoBsnWeb.UserSessionController do
username: user.username username: user.username
}) })
end end
def redirect_next(conn, %{ "next" => next }) do
conn |> redirect(external: next)
end
end end

View File

@@ -40,8 +40,8 @@ defmodule SsoBsnWeb.UserLoginLive do
""" """
end end
def mount(_params, _session, socket) do def mount(params, _session, socket) do
{:ok, socket |> assign(form: to_form(%{"username" => "", "remember_me" => false}), authenticating: false)} {:ok, socket |> assign(form: to_form(%{"username" => "", "remember_me" => false}), authenticating: false, next: params["next"])}
end end
def handle_event("login", %{"username" => username}, socket) do def handle_event("login", %{"username" => username}, socket) do
@@ -57,7 +57,7 @@ defmodule SsoBsnWeb.UserLoginLive do
case Accounts.authenticate_user(socket.assigns.challenge, params) do case Accounts.authenticate_user(socket.assigns.challenge, params) do
{:ok, user} -> {:ok, user} ->
login_token = Accounts.generate_user_login_token(user) login_token = Accounts.generate_user_login_token(user)
{:noreply, socket |> redirect(to: ~p"/users/log_in/#{login_token}")} {:noreply, socket |> redirect(to: if next = socket.assigns.next do ~p"/users/log_in/#{login_token}?next=#{next}" else ~p"/users/log_in/#{login_token}" end)}
{:error, error} -> {:error, error} ->
{:noreply, socket |> put_flash(:error, inspect(error))} {:noreply, socket |> put_flash(:error, inspect(error))}
end end

View File

@@ -21,6 +21,7 @@ defmodule SsoBsnWeb.Router do
pipe_through :browser pipe_through :browser
get "/", PageController, :home get "/", PageController, :home
get "/redirect", UserSessionController, :redirect_next
end end
# Other scopes may use custom stacks. # Other scopes may use custom stacks.
@@ -56,7 +57,6 @@ defmodule SsoBsnWeb.Router do
end end
get "/users/log_in/:token", UserSessionController, :login get "/users/log_in/:token", UserSessionController, :login
post "/users/log_in", UserSessionController, :create
end end
scope "/", SsoBsnWeb do scope "/", SsoBsnWeb do

View File

@@ -163,11 +163,11 @@ defmodule SsoBsnWeb.UserAuth do
end end
end end
def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do def on_mount(:redirect_if_user_is_authenticated, params, session, socket) do
socket = mount_current_user(socket, session) socket = mount_current_user(socket, session)
if socket.assigns.current_user do if socket.assigns.current_user do
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))} {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(params["next"]))}
else else
{:cont, socket} {:cont, socket}
end end
@@ -224,5 +224,7 @@ defmodule SsoBsnWeb.UserAuth do
defp maybe_store_return_to(conn), do: conn defp maybe_store_return_to(conn), do: conn
defp signed_in_path(_conn), do: ~p"/users/settings" defp signed_in_path(%Plug.Conn{ query_params: %{ "next" => next }}), do: ~p"/redirect?next=#{next}"
defp signed_in_path(next) when is_binary(next), do: ~p"/redirect?next=#{next}"
defp signed_in_path(_), do: ~p"/users/settings"
end end