···11+defmodule Sower.Accounts do
22+ use Ash.Domain
33+44+ resources do
55+ resource Sower.Accounts.User
66+ resource Sower.Accounts.UserToken
77+ end
88+end
+21
lib/sower/accounts/secrets.ex
···11+defmodule Sower.Accounts.Secrets do
22+ use AshAuthentication.Secret
33+44+ def secret_for([:authentication, :tokens, :signing_secret], Sower.Accounts.User, _) do
55+ case Application.fetch_env(:example, ExampleWeb.Endpoint) do
66+ {:ok, endpoint_config} ->
77+ Keyword.fetch(endpoint_config, :secret_key_base)
88+99+ :error ->
1010+ :error
1111+ end
1212+ end
1313+1414+ def secret_for([:authentication, :strategies, :oidc, :client_id], Sower.Accounts.User, _) do
1515+ Application.fetch_env(:sower, :oidc_client_id)
1616+ end
1717+1818+ def secret_for([:authentication, :strategies, :oidc, :client_secret], Sower.Accounts.User, _) do
1919+ Application.fetch_env(:sower, :oidc_client_secret)
2020+ end
2121+end
+11
lib/sower/accounts/token.ex
···11+defmodule Sower.Accounts.UserToken do
22+ use Ash.Resource,
33+ domain: Sower.Accounts,
44+ data_layer: AshPostgres.DataLayer,
55+ extensions: [AshAuthentication.TokenResource]
66+77+ postgres do
88+ table "user_tokens"
99+ repo Sower.Repo
1010+ end
1111+end
+68
lib/sower/accounts/user.ex
···11+defmodule Sower.Accounts.User do
22+ use Ash.Resource,
33+ domain: Sower.Accounts,
44+ data_layer: AshPostgres.DataLayer,
55+ extensions: [AshAuthentication]
66+77+ actions do
88+ defaults [:read]
99+1010+ create :register_with_oidc do
1111+ argument :user_info, :map, allow_nil?: false
1212+ argument :oauth_tokens, :map, allow_nil?: false
1313+ upsert? true
1414+ upsert_identity :unique_oidc_id
1515+1616+ change AshAuthentication.GenerateTokenChange
1717+1818+ change fn changeset, _ctx ->
1919+ user_info = Ash.Changeset.get_argument(changeset, :user_info)
2020+2121+ changeset
2222+ |> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"])
2323+ |> Ash.Changeset.change_attribute(:username, user_info["preferred_username"])
2424+ end
2525+ end
2626+ end
2727+2828+ attributes do
2929+ uuid_primary_key :id
3030+3131+ attribute :oidc_id, :uuid do
3232+ allow_nil? false
3333+ end
3434+3535+ attribute :username, :ci_string do
3636+ allow_nil? false
3737+ public? true
3838+ end
3939+ end
4040+4141+ authentication do
4242+ strategies do
4343+ oidc :oidc do
4444+ client_id Sower.Accounts.Secrets
4545+ base_url fn _, _ -> Application.fetch_env(:sower, :oidc_base_url) end
4646+ redirect_uri fn _, _ -> Application.fetch_env(:sower, :oidc_redirect_uri) end
4747+ client_secret Sower.Accounts.Secrets
4848+ # TODO: figure out why decoding fails with ES256
4949+ # id_token_signed_response_alg "ES256"
5050+ # authorization_params scope: "openid profile email"
5151+ end
5252+ end
5353+5454+ tokens do
5555+ token_resource Sower.Accounts.UserToken
5656+ signing_secret Sower.Accounts.Secrets
5757+ end
5858+ end
5959+6060+ postgres do
6161+ table "users"
6262+ repo Sower.Repo
6363+ end
6464+6565+ identities do
6666+ identity :unique_oidc_id, [:oidc_id]
6767+ end
6868+end
···21212222 def router do
2323 quote do
2424- use Phoenix.Router, helpers: false
2424+ use Phoenix.Router, helpers: true
25252626 # Import common connection and controller functions to use in pipelines
2727 import Plug.Conn
···11+defmodule SowerWeb.LiveUserAuth do
22+ @moduledoc """
33+ Helpers for authenticating users in LiveViews.
44+ """
55+66+ import Phoenix.Component
77+ use SowerWeb, :verified_routes
88+99+ def on_mount(:live_user_optional, _params, _session, socket) do
1010+ if socket.assigns[:current_user] do
1111+ {:cont, socket}
1212+ else
1313+ {:cont, assign(socket, :current_user, nil)}
1414+ end
1515+ end
1616+1717+ def on_mount(:live_user_required, _params, _session, socket) do
1818+ if socket.assigns[:current_user] do
1919+ {:cont, socket}
2020+ else
2121+ {:halt, Phoenix.LiveView.redirect(socket, to: ~p"/auth/user/oidc")}
2222+ end
2323+ end
2424+2525+ def on_mount(:live_no_user, _params, _session, socket) do
2626+ if socket.assigns[:current_user] do
2727+ {:halt, Phoenix.LiveView.redirect(socket, to: ~p"/")}
2828+ else
2929+ {:cont, assign(socket, :current_user, nil)}
3030+ end
3131+ end
3232+end
+20-6
lib/sower_web/router.ex
···11defmodule SowerWeb.Router do
22 use SowerWeb, :router
33 use Plug.ErrorHandler
44+ use AshAuthentication.Phoenix.Router
4556 pipeline :browser do
67 plug :accepts, ["html"]
···910 plug :put_root_layout, html: {SowerWeb.Layouts, :root}
1011 plug :protect_from_forgery
1112 plug :put_secure_browser_headers
1313+ plug :load_from_session
1214 end
13151416 pipeline :api do
1517 plug :accepts, ["json"]
1818+ plug :load_from_bearer
1619 end
17201821 scope "/", SowerWeb do
···20232124 get "/", PageController, :home
22252323- live "/seeds", SeedLive.Index, :index
2424- live "/seeds/:id", SeedLive.Show, :show
2525- live "/trees", TreeLive.Index, :index
2626- live "/trees/:id", TreeLive.Show, :show
2727- live "/inputs/repos", RepositoryLive.Index, :index
2828- live "/inputs/repos/:id", RepositoryLive.Show, :show
2626+ sign_in_route(register_path: "/register")
2727+ sign_out_route AuthController
2828+ auth_routes_for Sower.Accounts.User, to: AuthController
2929+3030+ ash_authentication_live_session :authentication_required,
3131+ on_mount: {SowerWeb.LiveUserAuth, :live_user_required} do
3232+ live "/seeds", SeedLive.Index, :index
3333+ live "/seeds/:id", SeedLive.Show, :show
3434+ live "/trees", TreeLive.Index, :index
3535+ live "/trees/:id", TreeLive.Show, :show
3636+ live "/inputs/repos", RepositoryLive.Index, :index
3737+ live "/inputs/repos/:id", RepositoryLive.Show, :show
3838+ end
3939+4040+ ash_authentication_live_session :authentication_optional,
4141+ on_mount: {SowerWeb.LiveUserAuth, :live_user_optional} do
4242+ end
2943 end
30443145 scope "/api" do