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,
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)
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
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)
)
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)
@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}
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
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,
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)
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
})
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, []),
{: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" ->
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
defmodule Pleroma.Web.OAuth.OAuthView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
- import Phoenix.HTML.Link
end
--- /dev/null
+<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 %>
<%= submit "Authorize" %>
<% end %>
-<br>
-<%= 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 %>
+ <br>
+ <%= render @view_module, "consumer.html", assigns %>
+<% end %>
--- /dev/null
+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