defmodule Supabase.GoTrue.Plug do @moduledoc """ Provides Plug-based authentication support for the Supabase GoTrue authentication in Elixir applications. This module offers a series of functions to manage user authentication through HTTP requests in Phoenix applications. It facilitates operations like logging in with a password, logging out users, fetching the current user from a session, and handling route protections based on authentication state. ## Configuration The module requires some application environment variables to be set: - `authentication_client`: The Supabase client used for authentication. - `signed_in_path`: The route to where conn should be redirected to after authentication - `not_authenticated_path `: The route to where conn should be redirect to if user isn't authenticated You can set up these config in your `config.exs`: ``` config :supabase_gotrue, signed_in_path: "/dashboard", not_authenticated_path: "/login", authentication_client: :my_supabase_potion_client_name ``` ## Authentication Flow It handles session management, cookie operations, and redirects based on user authentication status, providing a seamless integration for user sessions within Phoenix routes. """ import Plug.Conn import Phoenix.Controller alias Supabase.GoTrue alias Supabase.GoTrue.Admin alias Supabase.GoTrue.Session alias Supabase.GoTrue.User @session_cookie "_supabase_go_true_session_cookie" @session_cookie_options [sign: true, same_site: "Lax"] @client Application.compile_env!(:supabase_gotrue, :authentication_client) @signed_in_path Application.compile_env(:supabase_gotrue, :signed_in_path) @not_authenticated_path Application.compile_env(:supabase_gotrue, :not_authenticated_path, "/") @doc """ Logs in a user using a username and password. Stores the user token in the session and a cookie, if a `"remember_me"` key is present inside `params`. For more information on how Supabase login with email and password works, check `Supabase.GoTrue.sign_in_with_password/2` """ def log_in_with_password(conn, params \\ %{}) do with {:ok, session} <- GoTrue.sign_in_with_password(@client, params) do do_login(conn, session, params) end end def log_in_with_id_token(conn, params \\ %{}) do with {:ok, session} <- GoTrue.sign_in_with_id_token(@client, params) do do_login(conn, session, params) end end def log_in_with_oauth(conn, params \\ %{}) do with {:ok, session} <- GoTrue.sign_in_with_oauth(@client, params) do do_login(conn, session, params) end end def log_in_with_sso(conn, params \\ %{}) do with {:ok, session} <- GoTrue.sign_in_with_sso(@client, params) do do_login(conn, session, params) end end def log_in_with_otp(conn, params \\ %{}) do with {:ok, session} <- GoTrue.sign_in_with_otp(@client, params) do do_login(conn, session, params) end end defp do_login(conn, session, params) do user_return_to = get_session(conn, :user_return_to) :ok = Supabase.Client.update_access_token(@client, session.access_token) conn |> renew_session() |> put_token_in_session(session.access_token) |> maybe_write_session_cookie(session, params) |> redirect(to: user_return_to || @signed_in_path) end defp renew_session(conn) do conn |> configure_session(renew: true) |> clear_session() end defp maybe_write_session_cookie(conn, %Session{} = session, params) do case params do %{"remember_me" => "true"} -> token = session.access_token opts = Keyword.put(@session_cookie_options, :max_age, session.expires_in) put_resp_cookie(conn, @session_cookie, token, opts) _ -> conn end end @doc """ Logs out the user from the application, clearing session data """ def log_out_user(%Plug.Conn{} = conn, scope) do user_token = get_session(conn, :user_token) session = %Session{access_token: user_token} user_token && Admin.sign_out(@client, session, scope) live_socket_id = get_session(conn, :live_socket_id) endpoint = Application.get_env(:supabase_gotrue, :endpoint) if live_socket_id && endpoint do endpoint.broadcast(live_socket_id, "disconnect", %{}) end conn |> renew_session() |> redirect(to: @not_authenticated_path ) end @doc """ Retrieves the current user from the session or a signed cookie, assigning it to the connection's assigns. Can be easily used as a plug, for example inside a Phoenix web app pipeline in your `YourAppWeb.Router`, you can do something like: ``` import Supabase.GoTrue.Plug pipeline :browser do plug :fetch_session # comes from Plug.Conn plug :fetch_current_user # rest of plug chain... end ``` """ def fetch_current_user(conn, _opts) do {user_token, conn} = ensure_user_token(conn) user = user_token && fetch_user_from_session_token(user_token) assign(conn, :current_user, user) end defp fetch_user_from_session_token(user_token) do case GoTrue.get_user(@client, %Session{access_token: user_token}) do {:ok, %User{} = user} -> user _ -> nil end end defp ensure_user_token(conn) do if user_token = get_session(conn, :user_token) do {user_token, conn} else conn = fetch_cookies(conn, signed: [@session_cookie]) if user_token = conn.cookies[@session_cookie] do {user_token, put_token_in_session(conn, user_token)} else {nil, conn} end end end @doc """ Redirects an user to the configured `signed_in_path` if it is authenticated, if not, just halts the connection. Generaly you wan to use it inside your scopes routes inside `YourAppWeb.Router`: ``` scope "/" do pipe_trough [:browser, :redirect_if_user_is_authenticated] get "/login", LoginController, :login end ``` """ def redirect_if_user_is_authenticated(conn, _opts) do if conn.assigns[:current_user] do conn |> redirect(to: @signed_in_path) |> halt() else conn end end @doc """ Ensures an user is authenticated before executing the rest of Plugs chain. Generaly you wan to use it inside your scopes routes inside `YourAppWeb.Router`: ``` scope "/" do pipe_trough [:browser, :require_authenticated_user] get "/super-secret", SuperSecretController, :secret end ``` """ def require_authenticated_user(conn, _opts) do if conn.assigns[:current_user] do conn else conn |> maybe_store_return_to() |> redirect(to: @signed_in_path) |> halt() end end defp maybe_store_return_to(%{method: "GET"} = conn) do put_session(conn, :user_return_to, current_path(conn)) end defp maybe_store_return_to(conn), do: conn def put_token_in_session(conn, token) do base64_token = Base.url_encode64(token) conn |> put_session(:user_token, token) |> put_session(:live_socket_id, "users_session:#{base64_token}") end end