From aacbf0f57053786533df045125dee93ace0daa93 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 15 Mar 2019 17:08:03 +0300 Subject: [PATCH] [#923] OAuth: prototype of sign in / sign up with Twitter. --- config/config.exs | 6 +- lib/pleroma/user.ex | 46 +++++++++- lib/pleroma/web/auth/authenticator.ex | 9 +- lib/pleroma/web/auth/pleroma_authenticator.ex | 56 ++++++++++++- lib/pleroma/web/endpoint.ex | 11 ++- lib/pleroma/web/oauth/oauth_controller.ex | 83 ++++++++++++++----- lib/pleroma/web/oauth/oauth_view.ex | 1 - .../templates/o_auth/o_auth/consumer.html.eex | 14 ++++ .../web/templates/o_auth/o_auth/show.html.eex | 8 +- ...rovider_and_auth_provider_uid_to_users.exs | 12 +++ 10 files changed, 209 insertions(+), 37 deletions(-) create mode 100644 lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex create mode 100644 priv/repo/migrations/20190315101315_add_auth_provider_and_auth_provider_uid_to_users.exs diff --git a/config/config.exs b/config/config.exs index 8c754cef3..1ddc1bad1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -369,11 +369,15 @@ rel: false ] +config :pleroma, :auth, oauth_consumer_enabled: false + config :ueberauth, Ueberauth, base_path: "/oauth", providers: [ - twitter: {Ueberauth.Strategy.Twitter, []} + twitter: + {Ueberauth.Strategy.Twitter, + [callback_params: ~w[client_id redirect_uri scope scopes]]} ] config :ueberauth, Ueberauth.Strategy.Twitter.OAuth, diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index f49ede149..e17df8e34 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -40,6 +40,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) @@ -206,6 +208,36 @@ def reset_password(user, data) 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 @@ -504,13 +536,19 @@ def get_by_nickname(nickname) 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 @@ def implementation 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 3cc19af01..fb04ef8da 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 @@ def get_user(%Plug.Conn{} = conn) 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 d906db67d..31ffdecc0 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -57,10 +57,17 @@ defmodule Pleroma.Web.Endpoint do 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. - # Note: "SameSite=Strict" would cause issues with Twitter OAuth plug( Plug.Session, store: :cookie, @@ -68,7 +75,7 @@ defmodule Pleroma.Web.Endpoint do signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]}, http_only: true, secure: secure_cookies, - extra: "SameSite=Lax" + 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 7b052cb36..366085a57 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -15,20 +15,57 @@ defmodule Pleroma.Web.OAuth.OAuthController do import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] - plug(Ueberauth) + if Pleroma.Config.get([:auth, :oauth_consumer_enabled]), do: plug(Ueberauth) + plug(:fetch_session) plug(:fetch_flash) action_fallback(Pleroma.Web.OAuth.FallbackController) - def callback(%{assigns: %{ueberauth_failure: _failure}} = conn, _params) do + def request(conn, params) do + message = + if params["provider"] do + "Unsupported OAuth provider: #{params["provider"]}." + else + "Bad OAuth request." + end + conn - |> put_flash(:error, "Failed to authenticate.") + |> put_flash(:error, message) |> redirect(to: "/") end - def callback(%{assigns: %{ueberauth_auth: _auth}} = _conn, _params) do - raise "Authenticated successfully. Sign up via OAuth is not yet implemented." + 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 @@ -47,14 +84,21 @@ def authorize(conn, params) 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, []), @@ -63,13 +107,7 @@ def create_authorization(conn, %{ {: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" -> @@ -225,4 +263,9 @@ defp get_app_from_request(conn, params) 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/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex index 1450b5a8d..9b37a91c5 100644 --- a/lib/pleroma/web/oauth/oauth_view.ex +++ b/lib/pleroma/web/oauth/oauth_view.ex @@ -5,5 +5,4 @@ defmodule Pleroma.Web.OAuth.OAuthView do use Pleroma.Web, :view import Phoenix.HTML.Form - import Phoenix.HTML.Link end 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 @@ +

External OAuth Authorization

+<%= form_for @conn, o_auth_path(@conn, :request, :twitter), [method: "get"], fn f -> %> +
+ <%= label f, :scope, "Permissions" %> +
+ <%= text_input f, :scope, value: Enum.join(@available_scopes, " ") %> +
+
+ + <%= 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 d465f06b1..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 @@ -36,7 +36,7 @@ <%= submit "Authorize" %> <% end %> -
-<%= link to: "/oauth/twitter", class: "alert alert-info" do %> - Sign in with Twitter -<% end %> \ No newline at end of file +<%= if Pleroma.Config.get([:auth, :oauth_consumer_enabled]) do %> +
+ <%= render @view_module, "consumer.html", assigns %> +<% end %> diff --git a/priv/repo/migrations/20190315101315_add_auth_provider_and_auth_provider_uid_to_users.exs b/priv/repo/migrations/20190315101315_add_auth_provider_and_auth_provider_uid_to_users.exs new file mode 100644 index 000000000..90947f85a --- /dev/null +++ b/priv/repo/migrations/20190315101315_add_auth_provider_and_auth_provider_uid_to_users.exs @@ -0,0 +1,12 @@ +defmodule Pleroma.Repo.Migrations.AddAuthProviderAndAuthProviderUidToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :auth_provider, :string + add :auth_provider_uid, :string + end + + create unique_index(:users, [:auth_provider, :auth_provider_uid]) + end +end