diff options
| -rw-r--r-- | config/config.exs | 18 | ||||
| -rw-r--r-- | config/dev.exs | 1 | ||||
| -rw-r--r-- | lib/pleroma/registration.ex | 57 | ||||
| -rw-r--r-- | lib/pleroma/user.ex | 18 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/authenticator.ex | 21 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/ldap_authenticator.ex | 15 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/pleroma_authenticator.ex | 66 | ||||
| -rw-r--r-- | lib/pleroma/web/endpoint.ex | 17 | ||||
| -rw-r--r-- | lib/pleroma/web/oauth/oauth_controller.ex | 232 | ||||
| -rw-r--r-- | lib/pleroma/web/router.ex | 15 | ||||
| -rw-r--r-- | lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex | 13 | ||||
| -rw-r--r-- | lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex | 15 | ||||
| -rw-r--r-- | lib/pleroma/web/templates/o_auth/o_auth/register.html.eex | 48 | ||||
| -rw-r--r-- | lib/pleroma/web/templates/o_auth/o_auth/show.html.eex | 20 | ||||
| -rw-r--r-- | mix.exs | 12 | ||||
| -rw-r--r-- | mix.lock | 7 | ||||
| -rw-r--r-- | priv/repo/migrations/20190315101315_create_registrations.exs | 18 | 
17 files changed, 534 insertions, 59 deletions
diff --git a/config/config.exs b/config/config.exs index 0df38d75a..2dc87a50c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -378,6 +378,24 @@ config :pleroma, :ldap,    base: System.get_env("LDAP_BASE") || "dc=example,dc=com",    uid: System.get_env("LDAP_UID") || "cn" +oauth_consumer_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "") + +ueberauth_providers = +  for strategy <- oauth_consumer_strategies do +    strategy_module_name = "Elixir.Ueberauth.Strategy.#{String.capitalize(strategy)}" +    strategy_module = String.to_atom(strategy_module_name) +    {String.to_atom(strategy), {strategy_module, [callback_params: ["state"]]}} +  end + +config :ueberauth, +       Ueberauth, +       base_path: "/oauth", +       providers: ueberauth_providers + +config :pleroma, :auth, +  oauth_consumer_strategies: oauth_consumer_strategies, +  oauth_consumer_enabled: oauth_consumer_strategies != [] +  # Import environment specific config. This must remain at the bottom  # of this file so it overrides the configuration defined above.  import_config "#{Mix.env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index f77bb9976..a7eb4b644 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -12,7 +12,6 @@ config :pleroma, Pleroma.Web.Endpoint,      protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]    ],    protocol: "http", -  secure_cookie_flag: false,    debug_errors: true,    code_reloader: true,    check_origin: false, diff --git a/lib/pleroma/registration.ex b/lib/pleroma/registration.ex new file mode 100644 index 000000000..21fd1fc3f --- /dev/null +++ b/lib/pleroma/registration.ex @@ -0,0 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Registration do +  use Ecto.Schema + +  import Ecto.Changeset + +  alias Pleroma.Registration +  alias Pleroma.Repo +  alias Pleroma.User + +  @primary_key {:id, Pleroma.FlakeId, autogenerate: true} + +  schema "registrations" do +    belongs_to(:user, User, type: Pleroma.FlakeId) +    field(:provider, :string) +    field(:uid, :string) +    field(:info, :map, default: %{}) + +    timestamps() +  end + +  def nickname(registration, default \\ nil), +    do: Map.get(registration.info, "nickname", default) + +  def email(registration, default \\ nil), +    do: Map.get(registration.info, "email", default) + +  def name(registration, default \\ nil), +    do: Map.get(registration.info, "name", default) + +  def description(registration, default \\ nil), +    do: Map.get(registration.info, "description", default) + +  def changeset(registration, params \\ %{}) do +    registration +    |> cast(params, [:user_id, :provider, :uid, :info]) +    |> validate_required([:provider, :uid]) +    |> foreign_key_constraint(:user_id) +    |> unique_constraint(:uid, name: :registrations_provider_uid_index) +  end + +  def bind_to_user(registration, user) do +    registration +    |> changeset(%{user_id: (user && user.id) || nil}) +    |> Repo.update() +  end + +  def get_by_provider_uid(provider, uid) do +    Repo.get_by(Registration, +      provider: to_string(provider), +      uid: to_string(uid) +    ) +  end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 728b00a56..8fd0c5772 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -13,6 +13,7 @@ defmodule Pleroma.User do    alias Pleroma.Formatter    alias Pleroma.Notification    alias Pleroma.Object +  alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web @@ -55,6 +56,7 @@ defmodule Pleroma.User do      field(:bookmarks, {:array, :string}, default: [])      field(:last_refreshed_at, :naive_datetime_usec)      has_many(:notifications, Notification) +    has_many(:registrations, Registration)      embeds_one(:info, Pleroma.User.Info)      timestamps() @@ -216,7 +218,7 @@ defmodule Pleroma.User do      changeset =        struct        |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation]) -      |> validate_required([:email, :name, :nickname, :password, :password_confirmation]) +      |> validate_required([:name, :nickname, :password, :password_confirmation])        |> validate_confirmation(:password)        |> unique_constraint(:email)        |> unique_constraint(:nickname) @@ -227,6 +229,13 @@ defmodule Pleroma.User do        |> validate_length(:name, min: 1, max: 100)        |> put_change(:info, info_change) +    changeset = +      if opts[:external] do +        changeset +      else +        validate_required(changeset, [:email]) +      end +      if changeset.valid? do        hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])        ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]}) @@ -505,11 +514,10 @@ 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_cached_user_info(user) do diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index 82267c595..bb87b323c 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.Auth.Authenticator do    alias Pleroma.User +  alias Pleroma.Registration    def implementation do      Pleroma.Config.get( @@ -12,8 +13,18 @@ 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 create_from_registration(Plug.Conn.t(), Map.t(), Registration.t()) :: +              {:ok, User.t()} | {:error, any()} +  def create_from_registration(plug, params, registration), +    do: implementation().create_from_registration(plug, params, registration) + +  @callback get_registration(Plug.Conn.t(), Map.t()) :: +              {:ok, Registration.t()} | {:error, any()} +  def get_registration(plug, params), +    do: implementation().get_registration(plug, params)    @callback handle_error(Plug.Conn.t(), any()) :: any()    def handle_error(plug, error), do: implementation().handle_error(plug, error) @@ -22,4 +33,10 @@ defmodule Pleroma.Web.Auth.Authenticator do    def auth_template do      implementation().auth_template() || Pleroma.Config.get(:auth_template, "show.html")    end + +  @callback oauth_consumer_template() :: String.t() | nil +  def oauth_consumer_template do +    implementation().oauth_consumer_template() || +      Pleroma.Config.get(:oauth_consumer_template, "consumer.html") +  end  end diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index 88217aab8..8b6d5a77f 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -8,14 +8,19 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do    require Logger    @behaviour Pleroma.Web.Auth.Authenticator +  @base Pleroma.Web.Auth.PleromaAuthenticator    @connection_timeout 10_000    @search_timeout 10_000 -  def get_user(%Plug.Conn{} = conn) do +  defdelegate get_registration(conn, params), to: @base + +  defdelegate create_from_registration(conn, params, registration), to: @base + +  def get_user(%Plug.Conn{} = conn, params) do      if Pleroma.Config.get([:ldap, :enabled]) do        {name, password} = -        case conn.params do +        case params do            %{"authorization" => %{"name" => name, "password" => password}} ->              {name, password} @@ -29,14 +34,14 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do          {:error, {:ldap_connection_error, _}} ->            # When LDAP is unavailable, try default authenticator -          Pleroma.Web.Auth.PleromaAuthenticator.get_user(conn) +          @base.get_user(conn, params)          error ->            error        end      else        # Fall back to default authenticator -      Pleroma.Web.Auth.PleromaAuthenticator.get_user(conn) +      @base.get_user(conn, params)      end    end @@ -46,6 +51,8 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do    def auth_template, do: nil +  def oauth_consumer_template, do: nil +    defp ldap_user(name, password) do      ldap = Pleroma.Config.get(:ldap, [])      host = Keyword.get(ldap, :host, "localhost") diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index 94a19ad49..8b190f97f 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -5,12 +5,14 @@  defmodule Pleroma.Web.Auth.PleromaAuthenticator do    alias Comeonin.Pbkdf2    alias Pleroma.User +  alias Pleroma.Registration +  alias Pleroma.Repo    @behaviour Pleroma.Web.Auth.Authenticator -  def get_user(%Plug.Conn{} = conn) do +  def get_user(%Plug.Conn{} = _conn, params) do      {name, password} = -      case conn.params do +      case params do          %{"authorization" => %{"name" => name, "password" => password}} ->            {name, password} @@ -27,9 +29,69 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do      end    end +  def get_registration( +        %Plug.Conn{assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}}, +        _params +      ) do +    registration = Registration.get_by_provider_uid(provider, uid) + +    if registration do +      {:ok, registration} +    else +      info = auth.info + +      Registration.changeset(%Registration{}, %{ +        provider: to_string(provider), +        uid: to_string(uid), +        info: %{ +          "nickname" => info.nickname, +          "email" => info.email, +          "name" => info.name, +          "description" => info.description +        } +      }) +      |> Repo.insert() +    end +  end + +  def get_registration(%Plug.Conn{} = _conn, _params), do: {:error, :missing_credentials} + +  def create_from_registration(_conn, params, registration) do +    nickname = value([params["nickname"], Registration.nickname(registration)]) +    email = value([params["email"], Registration.email(registration)]) +    name = value([params["name"], Registration.name(registration)]) || nickname +    bio = value([params["bio"], Registration.description(registration)]) + +    random_password = :crypto.strong_rand_bytes(64) |> Base.encode64() + +    with {:ok, new_user} <- +           User.register_changeset( +             %User{}, +             %{ +               email: email, +               nickname: nickname, +               name: name, +               bio: bio, +               password: random_password, +               password_confirmation: random_password +             }, +             external: true, +             confirmed: true +           ) +           |> Repo.insert(), +         {:ok, _} <- +           Registration.changeset(registration, %{user_id: new_user.id}) |> Repo.update() do +      {:ok, new_user} +    end +  end + +  defp value(list), do: Enum.find(list, &(to_string(&1) != "")) +    def handle_error(%Plug.Conn{} = _conn, error) do      error    end    def auth_template, do: nil + +  def oauth_consumer_template, do: nil  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 ebb3dd253..e54e196aa 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do    alias Pleroma.Repo    alias Pleroma.User +  alias Pleroma.Registration    alias Pleroma.Web.Auth.Authenticator    alias Pleroma.Web.OAuth.App    alias Pleroma.Web.OAuth.Authorization @@ -14,6 +15,8 @@ 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) @@ -35,29 +38,17 @@ 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)}, -         %App{} = app <- Repo.get_by(App, client_id: client_id), -         true <- redirect_uri in String.split(app.redirect_uris), -         scopes <- oauth_scopes(auth_params, []), -         {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes}, -         # Note: `scope` param is intentionally not optional in this context -         {: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 +  def create_authorization( +        conn, +        %{ +          "authorization" => %{"redirect_uri" => redirect_uri} = auth_params +        } = params, +        opts \\ [] +      ) do +    with {:ok, auth} <- +           (opts[:auth] && {:ok, opts[:auth]}) || +             do_create_authorization(conn, params, opts[:user]) do +      redirect_uri = redirect_uri(conn, redirect_uri)        cond do          redirect_uri == "urn:ietf:wg:oauth:2.0:oob" -> @@ -133,7 +124,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do          conn,          %{"grant_type" => "password"} = params        ) do -    with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)}, +    with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn, params)},           %App{} = app <- get_app_from_request(conn, params),           {:auth_active, true} <- {:auth_active, User.auth_active?(user)},           scopes <- oauth_scopes(params, app.scopes), @@ -189,6 +180,189 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end +  def prepare_request(conn, %{"provider" => provider} = params) do +    scope = +      oauth_scopes(params, []) +      |> Enum.join(" ") + +    state = +      params +      |> Map.delete("scopes") +      |> Map.put("scope", scope) +      |> Poison.encode!() + +    params = +      params +      |> Map.drop(~w(scope scopes client_id redirect_uri)) +      |> Map.put("state", state) + +    redirect(conn, to: o_auth_path(conn, :request, provider, params)) +  end + +  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, params) do +    params = callback_params(params) +    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, params["redirect_uri"])) +  end + +  def callback(conn, params) do +    params = callback_params(params) + +    with {:ok, registration} <- Authenticator.get_registration(conn, params) do +      user = Repo.preload(registration, :user).user + +      auth_params = %{ +        "client_id" => params["client_id"], +        "redirect_uri" => params["redirect_uri"], +        "scopes" => oauth_scopes(params, nil) +      } + +      if user do +        create_authorization( +          conn, +          %{"authorization" => auth_params}, +          user: user +        ) +      else +        registration_params = +          Map.merge(auth_params, %{ +            "nickname" => Registration.nickname(registration), +            "email" => Registration.email(registration) +          }) + +        conn +        |> put_session(:registration_id, registration.id) +        |> redirect(to: o_auth_path(conn, :registration_details, registration_params)) +      end +    else +      _ -> +        conn +        |> put_flash(:error, "Failed to set up user account.") +        |> redirect(external: redirect_uri(conn, params["redirect_uri"])) +    end +  end + +  defp callback_params(%{"state" => state} = params) do +    Map.merge(params, Poison.decode!(state)) +  end + +  def registration_details(conn, params) do +    render(conn, "register.html", %{ +      client_id: params["client_id"], +      redirect_uri: params["redirect_uri"], +      scopes: oauth_scopes(params, []), +      nickname: params["nickname"], +      email: params["email"] +    }) +  end + +  def register(conn, %{"op" => "connect"} = params) do +    create_authorization_params = %{ +      "authorization" => Map.merge(params, %{"name" => params["auth_name"]}) +    } + +    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), +         %Registration{} = registration <- Repo.get(Registration, registration_id), +         {:ok, auth} <- do_create_authorization(conn, create_authorization_params), +         %User{} = user <- Repo.preload(auth, :user).user, +         {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do +      conn +      |> put_session_registration_id(nil) +      |> create_authorization( +        create_authorization_params, +        auth: auth +      ) +    else +      _ -> +        conn +        |> put_flash(:error, "Unknown error, please try again.") +        |> redirect(to: o_auth_path(conn, :registration_details, params)) +    end +  end + +  def register(conn, params) do +    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), +         %Registration{} = registration <- Repo.get(Registration, registration_id), +         {:ok, user} <- Authenticator.create_from_registration(conn, params, registration) do +      conn +      |> put_session_registration_id(nil) +      |> create_authorization( +        %{ +          "authorization" => %{ +            "client_id" => params["client_id"], +            "redirect_uri" => params["redirect_uri"], +            "scopes" => oauth_scopes(params, nil) +          } +        }, +        user: user +      ) +    else +      {:error, changeset} -> +        message = +          Enum.map(changeset.errors, fn {field, {error, _}} -> +            "#{field} #{error}" +          end) +          |> Enum.join("; ") + +        message = +          String.replace( +            message, +            "ap_id has already been taken", +            "nickname has already been taken" +          ) + +        conn +        |> put_flash(:error, "Error: #{message}.") +        |> redirect(to: o_auth_path(conn, :registration_details, params)) + +      _ -> +        conn +        |> put_flash(:error, "Unknown error, please try again.") +        |> redirect(to: o_auth_path(conn, :registration_details, params)) +    end +  end + +  defp do_create_authorization( +         conn, +         %{ +           "authorization" => +             %{ +               "client_id" => client_id, +               "redirect_uri" => redirect_uri +             } = auth_params +         } = params, +         user \\ nil +       ) 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, []), +         {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes}, +         # Note: `scope` param is intentionally not optional in this context +         {:missing_scopes, false} <- {:missing_scopes, scopes == []}, +         {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do +      Authorization.create_authorization(app, user, scopes) +    end +  end +    # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be    # decoding it.  Investigate sometime.    defp fix_padding(token) do @@ -221,4 +395,14 @@ 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 + +  defp get_session_registration_id(conn), do: get_session(conn, :registration_id) + +  defp put_session_registration_id(conn, registration_id), +    do: put_session(conn, :registration_id, registration_id)  end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 9ccb4e535..8a157e223 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) @@ -204,6 +209,16 @@ defmodule Pleroma.Web.Router do      post("/authorize", OAuthController, :create_authorization)      post("/token", OAuthController, :token_exchange)      post("/revoke", OAuthController, :token_revoke) +    get("/registration_details", OAuthController, :registration_details) + +    scope [] do +      pipe_through(:browser) + +      get("/prepare_request", OAuthController, :prepare_request) +      get("/:provider", OAuthController, :request) +      get("/:provider/callback", OAuthController, :callback) +      post("/register", OAuthController, :register) +    end    end    scope "/api/v1", Pleroma.Web.MastodonAPI do diff --git a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex new file mode 100644 index 000000000..4b8fb5dae --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex @@ -0,0 +1,13 @@ +<div class="scopes-input"> +  <%= label @form, :scope, "Permissions" %> + +  <div class="scopes"> +    <%= for scope <- @available_scopes do %> +      <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> +      <div class="scope"> +        <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: assigns[:scope_param] || "scope[]" %> +        <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> +      </div> +    <% end %> +  </div> +</div> 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..002f014e6 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -0,0 +1,15 @@ +<br> +<br> +<h2>Sign in with external provider</h2> + +<%= form_for @conn, o_auth_path(@conn, :prepare_request), [method: "get"], fn f -> %> +  <%= render @view_module, "_scopes.html", Map.put(assigns, :form, f) %> + +  <%= hidden_input f, :client_id, value: @client_id %> +  <%= hidden_input f, :redirect_uri, value: @redirect_uri %> +  <%= hidden_input f, :state, value: @state %> + +    <%= for strategy <- Pleroma.Config.get([:auth, :oauth_consumer_strategies], []) do %> +      <%= submit "Sign in with #{String.capitalize(strategy)}", name: "provider", value: strategy %> +    <% end %> +<% end %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex new file mode 100644 index 000000000..f4547170c --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex @@ -0,0 +1,48 @@ +<%= if get_flash(@conn, :info) do %> +  <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> +<% end %> +<%= if get_flash(@conn, :error) do %> +  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> +<% end %> + +<h2>Registration Details</h2> + +<p>If you'd like to register a new account, +<br> +please provide the details below.</p> +<br> + +<%= form_for @conn, o_auth_path(@conn, :register), [], fn f -> %> + +<div class="input"> +  <%= label f, :nickname, "Nickname" %> +  <%= text_input f, :nickname, value: @nickname %> +</div> +<div class="input"> +  <%= label f, :email, "Email" %> +  <%= text_input f, :email, value: @email %> +</div> + +<%= submit "Proceed as new user", name: "op", value: "register" %> + +<br> +<br> +<br> +<p>Alternatively, sign in to connect to existing account.</p> + +<div class="input"> +  <%= label f, :auth_name, "Name or email" %> +  <%= text_input f, :auth_name %> +</div> +<div class="input"> +  <%= label f, :password, "Password" %> +  <%= password_input f, :password %> +</div> + +<%= submit "Proceed as existing user", name: "op", value: "connect" %> + +<%= hidden_input f, :client_id, value: @client_id %> +<%= hidden_input f, :redirect_uri, value: @redirect_uri %> +<%= hidden_input f, :scope, value: Enum.join(@scopes, " ") %> + +<% 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..e6cf1db45 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" %> @@ -14,18 +16,8 @@    <%= label f, :password, "Password" %>    <%= password_input f, :password %>  </div> -<div class="scopes-input"> -<%= label f, :scope, "Permissions" %> -  <div class="scopes"> -    <%= for scope <- @available_scopes do %> -      <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> -      <div class="scope"> -        <%= checkbox f, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> -        <%= label f, :"scope_#{scope}", String.capitalize(scope) %> -      </div> -    <% end %> -  </div> -</div> + +<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f, scope_param: "authorization[scope][]"}) %>  <%= hidden_input f, :client_id, value: @client_id %>  <%= hidden_input f, :response_type, value: @response_type %> @@ -33,3 +25,7 @@  <%= hidden_input f, :state, value: @state%>  <%= submit "Authorize" %>  <% end %> + +<%= if Pleroma.Config.get([:auth, :oauth_consumer_enabled]) do %> +  <%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %> +<% end %> @@ -54,6 +54,12 @@ defmodule Pleroma.Mixfile do    #    # Type `mix help deps` for examples and options.    defp deps do +    oauth_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "") + +    oauth_deps = +      for s <- oauth_strategies, +          do: {String.to_atom("ueberauth_#{s}"), ">= 0.0.0"} +      [        {:phoenix, "~> 1.4.1"},        {:plug_cowboy, "~> 2.0"}, @@ -70,7 +76,8 @@ defmodule Pleroma.Mixfile do        {:phoenix_html, "~> 2.10"},        {:calendar, "~> 0.17.4"},        {:cachex, "~> 3.0.2"}, -      {:httpoison, "~> 1.2.0"}, +      {:httpoison, "~> 1.2.0", override: true}, +      {:poison, "~> 3.0", override: true},        {:tesla, "~> 1.2"},        {:jason, "~> 1.0"},        {:mogrify, "~> 0.6.1"}, @@ -91,11 +98,12 @@ defmodule Pleroma.Mixfile do        {:floki, "~> 0.20.0"},        {:ex_syslogger, github: "slashmili/ex_syslogger", tag: "1.4.0"},        {:timex, "~> 3.5"}, +      {:ueberauth, "~> 0.4"},        {:auto_linker,         git: "https://git.pleroma.social/pleroma/auto_linker.git",         ref: "94193ca5f97c1f9fdf3d1469653e2d46fac34bcd"},        {:pleroma_job_queue, "~> 0.2.0"} -    ] +    ] ++ oauth_deps    end    # Aliases are shortcuts or tasks specific to the current project. @@ -4,7 +4,7 @@    "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},    "cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},    "calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, -  "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, +  "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},    "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},    "comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},    "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, @@ -27,7 +27,7 @@    "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},    "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"},    "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, -  "hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, +  "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},    "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"},    "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},    "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, @@ -39,7 +39,7 @@    "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"},    "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},    "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, -  "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, +  "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},    "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"},    "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},    "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"}, @@ -66,6 +66,7 @@    "timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},    "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},    "tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, +  "ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},    "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},    "unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},    "web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/priv/repo/migrations/20190315101315_create_registrations.exs b/priv/repo/migrations/20190315101315_create_registrations.exs new file mode 100644 index 000000000..6b28cbdd3 --- /dev/null +++ b/priv/repo/migrations/20190315101315_create_registrations.exs @@ -0,0 +1,18 @@ +defmodule Pleroma.Repo.Migrations.CreateRegistrations do +  use Ecto.Migration + +  def change do +    create table(:registrations, primary_key: false) do +      add :id, :uuid, primary_key: true +      add :user_id, references(:users, type: :uuid, on_delete: :delete_all) +      add :provider, :string +      add :uid, :string +      add :info, :map, default: %{} + +      timestamps() +    end + +    create unique_index(:registrations, [:provider, :uid]) +    create unique_index(:registrations, [:user_id, :provider, :uid]) +  end +end  | 
