diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/pleroma/user.ex | 46 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/authenticator.ex | 9 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/pleroma_authenticator.ex | 56 | ||||
| -rw-r--r-- | lib/pleroma/web/endpoint.ex | 17 | ||||
| -rw-r--r-- | lib/pleroma/web/oauth/oauth_controller.ex | 84 | ||||
| -rw-r--r-- | lib/pleroma/web/router.ex | 12 | ||||
| -rw-r--r-- | lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex | 14 | ||||
| -rw-r--r-- | lib/pleroma/web/templates/o_auth/o_auth/show.html.eex | 7 | 
8 files changed, 217 insertions, 28 deletions
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 1ce9882f6..381642281 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -41,6 +41,8 @@ defmodule Pleroma.User do      field(:email, :string)      field(:name, :string)      field(:nickname, :string) +    field(:auth_provider, :string) +    field(:auth_provider_uid, :string)      field(:password_hash, :string)      field(:password, :string, virtual: true)      field(:password_confirmation, :string, virtual: true) @@ -207,6 +209,36 @@ defmodule Pleroma.User do      update_and_set_cache(password_update_changeset(user, data))    end +  # TODO: FIXME (WIP): +  def oauth_register_changeset(struct, params \\ %{}) do +    info_change = User.Info.confirmation_changeset(%User.Info{}, :confirmed) + +    changeset = +      struct +      |> cast(params, [:email, :nickname, :name, :bio, :auth_provider, :auth_provider_uid]) +      |> validate_required([:auth_provider, :auth_provider_uid]) +      |> unique_constraint(:email) +      |> unique_constraint(:nickname) +      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames])) +      |> validate_format(:email, @email_regex) +      |> validate_length(:bio, max: 1000) +      |> put_change(:info, info_change) + +    if changeset.valid? do +      nickname = changeset.changes[:nickname] +      ap_id = (nickname && User.ap_id(%User{nickname: nickname})) || nil +      followers = User.ap_followers(%User{nickname: ap_id}) + +      changeset +      |> put_change(:ap_id, ap_id) +      |> unique_constraint(:ap_id) +      |> put_change(:following, [followers]) +      |> put_change(:follower_address, followers) +    else +      changeset +    end +  end +    def register_changeset(struct, params \\ %{}, opts \\ []) do      confirmation_status =        if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do @@ -506,13 +538,19 @@ defmodule Pleroma.User do        end    end +  def get_by_email(email), do: Repo.get_by(User, email: email) +    def get_by_nickname_or_email(nickname_or_email) do -    case user = Repo.get_by(User, nickname: nickname_or_email) do -      %User{} -> user -      nil -> Repo.get_by(User, email: nickname_or_email) -    end +    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)    end +  def get_by_auth_provider_uid(auth_provider, auth_provider_uid), +    do: +      Repo.get_by(User, +        auth_provider: to_string(auth_provider), +        auth_provider_uid: to_string(auth_provider_uid) +      ) +    def get_cached_user_info(user) do      key = "user_info:#{user.id}"      Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end) diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index 82267c595..fa439d562 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -12,8 +12,13 @@ defmodule Pleroma.Web.Auth.Authenticator do      )    end -  @callback get_user(Plug.Conn.t()) :: {:ok, User.t()} | {:error, any()} -  def get_user(plug), do: implementation().get_user(plug) +  @callback get_user(Plug.Conn.t(), Map.t()) :: {:ok, User.t()} | {:error, any()} +  def get_user(plug, params), do: implementation().get_user(plug, params) + +  @callback get_or_create_user_by_oauth(Plug.Conn.t(), Map.t()) :: +              {:ok, User.t()} | {:error, any()} +  def get_or_create_user_by_oauth(plug, params), +    do: implementation().get_or_create_user_by_oauth(plug, params)    @callback handle_error(Plug.Conn.t(), any()) :: any()    def handle_error(plug, error), do: implementation().handle_error(plug, error) diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index 333446bef..5583f41a9 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -8,9 +8,9 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do    @behaviour Pleroma.Web.Auth.Authenticator -  def get_user(%Plug.Conn{} = conn) do -    %{"authorization" => %{"name" => name, "password" => password}} = conn.params - +  def get_user(%Plug.Conn{} = _conn, %{ +        "authorization" => %{"name" => name, "password" => password} +      }) do      with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)},           {_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do        {:ok, user} @@ -20,6 +20,56 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do      end    end +  def get_user(%Plug.Conn{} = _conn, _params), do: {:error, :missing_credentials} + +  def get_or_create_user_by_oauth( +        %Plug.Conn{assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}}, +        _params +      ) do +    user = User.get_by_auth_provider_uid(provider, uid) + +    if user do +      {:ok, user} +    else +      info = auth.info +      email = info.email +      nickname = info.nickname + +      # TODO: FIXME: connect to existing (non-oauth) account (need a UI flow for that) / generate a random nickname? +      email = +        if email && User.get_by_email(email) do +          nil +        else +          email +        end + +      nickname = +        if nickname && User.get_by_nickname(nickname) do +          nil +        else +          nickname +        end + +      new_user = +        User.oauth_register_changeset( +          %User{}, +          %{ +            auth_provider: to_string(provider), +            auth_provider_uid: to_string(uid), +            name: info.name, +            bio: info.description, +            email: email, +            nickname: nickname +          } +        ) + +      Pleroma.Repo.insert(new_user) +    end +  end + +  def get_or_create_user_by_oauth(%Plug.Conn{} = _conn, _params), +    do: {:error, :missing_credentials} +    def handle_error(%Plug.Conn{} = _conn, error) do      error    end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index fa2d1cbe7..f92724d8b 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -51,11 +51,21 @@ defmodule Pleroma.Web.Endpoint do    plug(Plug.MethodOverride)    plug(Plug.Head) +  secure_cookies = Pleroma.Config.get([__MODULE__, :secure_cookie_flag]) +    cookie_name = -    if Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag), +    if secure_cookies,        do: "__Host-pleroma_key",        else: "pleroma_key" +  same_site = +    if Pleroma.Config.get([:auth, :oauth_consumer_enabled]) do +      # Note: "SameSite=Strict" prevents sign in with external OAuth provider (no cookies during callback request) +      "SameSite=Lax" +    else +      "SameSite=Strict" +    end +    # The session will be stored in the cookie and signed,    # this means its contents can be read but not tampered with.    # Set :encryption_salt if you would also like to encrypt it. @@ -65,9 +75,8 @@ defmodule Pleroma.Web.Endpoint do      key: cookie_name,      signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},      http_only: true, -    secure: -      Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag), -    extra: "SameSite=Strict" +    secure: secure_cookies, +    extra: same_site    )    plug(Pleroma.Web.Router) diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index ec70b7ccc..d39c4a713 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -15,11 +15,59 @@ defmodule Pleroma.Web.OAuth.OAuthController do    import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] +  if Pleroma.Config.get([:auth, :oauth_consumer_enabled]), do: plug(Ueberauth) +    plug(:fetch_session)    plug(:fetch_flash)    action_fallback(Pleroma.Web.OAuth.FallbackController) +  def request(conn, params) do +    message = +      if params["provider"] do +        "Unsupported OAuth provider: #{params["provider"]}." +      else +        "Bad OAuth request." +      end + +    conn +    |> put_flash(:error, message) +    |> redirect(to: "/") +  end + +  def callback(%{assigns: %{ueberauth_failure: failure}} = conn, %{"redirect_uri" => redirect_uri}) do +    messages = for e <- Map.get(failure, :errors, []), do: e.message +    message = Enum.join(messages, "; ") + +    conn +    |> put_flash(:error, "Failed to authenticate: #{message}.") +    |> redirect(external: redirect_uri(conn, redirect_uri)) +  end + +  def callback( +        conn, +        %{"client_id" => client_id, "redirect_uri" => redirect_uri} = params +      ) do +    with {:ok, user} <- Authenticator.get_or_create_user_by_oauth(conn, params) do +      do_create_authorization( +        conn, +        %{ +          "authorization" => %{ +            "client_id" => client_id, +            "redirect_uri" => redirect_uri, +            "scope" => oauth_scopes(params, nil) +          } +        }, +        user +      ) +    else +      _ -> +        conn +        |> put_flash(:error, "Failed to set up user account.") +        |> redirect(external: redirect_uri(conn, redirect_uri)) +    end +  end +    def authorize(conn, params) do      app = Repo.get_by(App, client_id: params["client_id"])      available_scopes = (app && app.scopes) || [] @@ -36,14 +84,21 @@ defmodule Pleroma.Web.OAuth.OAuthController do      })    end -  def create_authorization(conn, %{ -        "authorization" => -          %{ -            "client_id" => client_id, -            "redirect_uri" => redirect_uri -          } = auth_params -      }) do -    with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)}, +  def create_authorization(conn, params), do: do_create_authorization(conn, params, nil) + +  defp do_create_authorization( +         conn, +         %{ +           "authorization" => +             %{ +               "client_id" => client_id, +               "redirect_uri" => redirect_uri +             } = auth_params +         } = params, +         user +       ) do +    with {_, {:ok, %User{} = user}} <- +           {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn, params)},           %App{} = app <- Repo.get_by(App, client_id: client_id),           true <- redirect_uri in String.split(app.redirect_uris),           scopes <- oauth_scopes(auth_params, []), @@ -52,13 +107,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           {:missing_scopes, false} <- {:missing_scopes, scopes == []},           {:auth_active, true} <- {:auth_active, User.auth_active?(user)},           {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do -      redirect_uri = -        if redirect_uri == "." do -          # Special case: Local MastodonFE -          mastodon_api_url(conn, :login) -        else -          redirect_uri -        end +      redirect_uri = redirect_uri(conn, redirect_uri)        cond do          redirect_uri == "urn:ietf:wg:oauth:2.0:oob" -> @@ -214,4 +263,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do        nil      end    end + +  # Special case: Local MastodonFE +  defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login) + +  defp redirect_uri(_conn, redirect_uri), do: redirect_uri  end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c56e4a421..d490b9b58 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -5,6 +5,11 @@  defmodule Pleroma.Web.Router do    use Pleroma.Web, :router +  pipeline :browser do +    plug(:accepts, ["html"]) +    plug(:fetch_session) +  end +    pipeline :api do      plug(:accepts, ["json"])      plug(:fetch_session) @@ -197,6 +202,13 @@ defmodule Pleroma.Web.Router do      post("/authorize", OAuthController, :create_authorization)      post("/token", OAuthController, :token_exchange)      post("/revoke", OAuthController, :token_revoke) + +    scope [] do +      pipe_through(:browser) + +      get("/:provider", OAuthController, :request) +      get("/:provider/callback", OAuthController, :callback) +    end    end    scope "/api/v1", Pleroma.Web.MastodonAPI do diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex new file mode 100644 index 000000000..e7251bce8 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -0,0 +1,14 @@ +<h2>External OAuth Authorization</h2> +<%= form_for @conn, o_auth_path(@conn, :request, :twitter), [method: "get"], fn f -> %> +  <div class="scopes-input"> +  <%= label f, :scope, "Permissions" %> +  <div class="scopes"> +    <%= text_input f, :scope, value: Enum.join(@available_scopes, " ") %> +  </div> +  </div> + +  <%= hidden_input f, :client_id, value: @client_id %> +  <%= hidden_input f, :redirect_uri, value: @redirect_uri %> +  <%= hidden_input f, :state, value: @state%> +  <%= submit "Sign in with Twitter" %> +<% end %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 161333847..2fa7837fc 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -4,7 +4,9 @@  <%= if get_flash(@conn, :error) do %>  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>  <% end %> +  <h2>OAuth Authorization</h2> +  <%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>  <div class="input">    <%= label f, :name, "Name or email" %> @@ -33,3 +35,8 @@  <%= hidden_input f, :state, value: @state%>  <%= submit "Authorize" %>  <% end %> + +<%= if Pleroma.Config.get([:auth, :oauth_consumer_enabled]) do %> +  <br> +  <%= render @view_module, "consumer.html", assigns %> +<% end %>  | 
