[#923] OAuth: prototype of sign in / sign up with Twitter.
This commit is contained in:
parent
63ab61ed3f
commit
aacbf0f570
10 changed files with 209 additions and 37 deletions
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,5 +5,4 @@
|
|||
defmodule Pleroma.Web.OAuth.OAuthView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML.Form
|
||||
import Phoenix.HTML.Link
|
||||
end
|
||||
|
|
14
lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex
Normal file
14
lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex
Normal file
|
@ -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 %>
|
|
@ -36,7 +36,7 @@
|
|||
<%= submit "Authorize" %>
|
||||
<% end %>
|
||||
|
||||
<br>
|
||||
<%= link to: "/oauth/twitter", class: "alert alert-info" do %>
|
||||
Sign in with Twitter
|
||||
<% end %>
|
||||
<%= if Pleroma.Config.get([:auth, :oauth_consumer_enabled]) do %>
|
||||
<br>
|
||||
<%= render @view_module, "consumer.html", assigns %>
|
||||
<% end %>
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue