[#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
|
rel: false
|
||||||
]
|
]
|
||||||
|
|
||||||
|
config :pleroma, :auth, oauth_consumer_enabled: false
|
||||||
|
|
||||||
config :ueberauth,
|
config :ueberauth,
|
||||||
Ueberauth,
|
Ueberauth,
|
||||||
base_path: "/oauth",
|
base_path: "/oauth",
|
||||||
providers: [
|
providers: [
|
||||||
twitter: {Ueberauth.Strategy.Twitter, []}
|
twitter:
|
||||||
|
{Ueberauth.Strategy.Twitter,
|
||||||
|
[callback_params: ~w[client_id redirect_uri scope scopes]]}
|
||||||
]
|
]
|
||||||
|
|
||||||
config :ueberauth, Ueberauth.Strategy.Twitter.OAuth,
|
config :ueberauth, Ueberauth.Strategy.Twitter.OAuth,
|
||||||
|
|
|
@ -40,6 +40,8 @@ defmodule Pleroma.User do
|
||||||
field(:email, :string)
|
field(:email, :string)
|
||||||
field(:name, :string)
|
field(:name, :string)
|
||||||
field(:nickname, :string)
|
field(:nickname, :string)
|
||||||
|
field(:auth_provider, :string)
|
||||||
|
field(:auth_provider_uid, :string)
|
||||||
field(:password_hash, :string)
|
field(:password_hash, :string)
|
||||||
field(:password, :string, virtual: true)
|
field(:password, :string, virtual: true)
|
||||||
field(:password_confirmation, :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))
|
update_and_set_cache(password_update_changeset(user, data))
|
||||||
end
|
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
|
def register_changeset(struct, params \\ %{}, opts \\ []) do
|
||||||
confirmation_status =
|
confirmation_status =
|
||||||
if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
|
if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
|
||||||
|
@ -504,13 +536,19 @@ def get_by_nickname(nickname) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_by_email(email), do: Repo.get_by(User, email: email)
|
||||||
|
|
||||||
def get_by_nickname_or_email(nickname_or_email) do
|
def get_by_nickname_or_email(nickname_or_email) do
|
||||||
case user = Repo.get_by(User, nickname: nickname_or_email) do
|
get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
|
||||||
%User{} -> user
|
|
||||||
nil -> Repo.get_by(User, email: nickname_or_email)
|
|
||||||
end
|
|
||||||
end
|
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
|
def get_cached_user_info(user) do
|
||||||
key = "user_info:#{user.id}"
|
key = "user_info:#{user.id}"
|
||||||
Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
|
Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
|
||||||
|
|
|
@ -12,8 +12,13 @@ def implementation do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@callback get_user(Plug.Conn.t()) :: {:ok, User.t()} | {:error, any()}
|
@callback get_user(Plug.Conn.t(), Map.t()) :: {:ok, User.t()} | {:error, any()}
|
||||||
def get_user(plug), do: implementation().get_user(plug)
|
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()
|
@callback handle_error(Plug.Conn.t(), any()) :: any()
|
||||||
def handle_error(plug, error), do: implementation().handle_error(plug, error)
|
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
|
@behaviour Pleroma.Web.Auth.Authenticator
|
||||||
|
|
||||||
def get_user(%Plug.Conn{} = conn) do
|
def get_user(%Plug.Conn{} = _conn, %{
|
||||||
%{"authorization" => %{"name" => name, "password" => password}} = conn.params
|
"authorization" => %{"name" => name, "password" => password}
|
||||||
|
}) do
|
||||||
with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)},
|
with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)},
|
||||||
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
|
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
|
@ -20,6 +20,56 @@ def get_user(%Plug.Conn{} = conn) do
|
||||||
end
|
end
|
||||||
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
|
def handle_error(%Plug.Conn{} = _conn, error) do
|
||||||
error
|
error
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,10 +57,17 @@ defmodule Pleroma.Web.Endpoint do
|
||||||
do: "__Host-pleroma_key",
|
do: "__Host-pleroma_key",
|
||||||
else: "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,
|
# The session will be stored in the cookie and signed,
|
||||||
# this means its contents can be read but not tampered with.
|
# this means its contents can be read but not tampered with.
|
||||||
# Set :encryption_salt if you would also like to encrypt it.
|
# Set :encryption_salt if you would also like to encrypt it.
|
||||||
# Note: "SameSite=Strict" would cause issues with Twitter OAuth
|
|
||||||
plug(
|
plug(
|
||||||
Plug.Session,
|
Plug.Session,
|
||||||
store: :cookie,
|
store: :cookie,
|
||||||
|
@ -68,7 +75,7 @@ defmodule Pleroma.Web.Endpoint do
|
||||||
signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},
|
signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},
|
||||||
http_only: true,
|
http_only: true,
|
||||||
secure: secure_cookies,
|
secure: secure_cookies,
|
||||||
extra: "SameSite=Lax"
|
extra: same_site
|
||||||
)
|
)
|
||||||
|
|
||||||
plug(Pleroma.Web.Router)
|
plug(Pleroma.Web.Router)
|
||||||
|
|
|
@ -15,20 +15,57 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
||||||
|
|
||||||
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
|
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_session)
|
||||||
plug(:fetch_flash)
|
plug(:fetch_flash)
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.OAuth.FallbackController)
|
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
|
conn
|
||||||
|> put_flash(:error, "Failed to authenticate.")
|
|> put_flash(:error, message)
|
||||||
|> redirect(to: "/")
|
|> redirect(to: "/")
|
||||||
end
|
end
|
||||||
|
|
||||||
def callback(%{assigns: %{ueberauth_auth: _auth}} = _conn, _params) do
|
def callback(%{assigns: %{ueberauth_failure: failure}} = conn, %{"redirect_uri" => redirect_uri}) do
|
||||||
raise "Authenticated successfully. Sign up via OAuth is not yet implemented."
|
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
|
end
|
||||||
|
|
||||||
def authorize(conn, params) do
|
def authorize(conn, params) do
|
||||||
|
@ -47,14 +84,21 @@ def authorize(conn, params) do
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_authorization(conn, %{
|
def create_authorization(conn, params), do: do_create_authorization(conn, params, nil)
|
||||||
|
|
||||||
|
defp do_create_authorization(
|
||||||
|
conn,
|
||||||
|
%{
|
||||||
"authorization" =>
|
"authorization" =>
|
||||||
%{
|
%{
|
||||||
"client_id" => client_id,
|
"client_id" => client_id,
|
||||||
"redirect_uri" => redirect_uri
|
"redirect_uri" => redirect_uri
|
||||||
} = auth_params
|
} = auth_params
|
||||||
}) do
|
} = params,
|
||||||
with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)},
|
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),
|
%App{} = app <- Repo.get_by(App, client_id: client_id),
|
||||||
true <- redirect_uri in String.split(app.redirect_uris),
|
true <- redirect_uri in String.split(app.redirect_uris),
|
||||||
scopes <- oauth_scopes(auth_params, []),
|
scopes <- oauth_scopes(auth_params, []),
|
||||||
|
@ -63,13 +107,7 @@ def create_authorization(conn, %{
|
||||||
{:missing_scopes, false} <- {:missing_scopes, scopes == []},
|
{:missing_scopes, false} <- {:missing_scopes, scopes == []},
|
||||||
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
|
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
|
||||||
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
|
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
|
||||||
redirect_uri =
|
redirect_uri = redirect_uri(conn, redirect_uri)
|
||||||
if redirect_uri == "." do
|
|
||||||
# Special case: Local MastodonFE
|
|
||||||
mastodon_api_url(conn, :login)
|
|
||||||
else
|
|
||||||
redirect_uri
|
|
||||||
end
|
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
|
redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
|
||||||
|
@ -225,4 +263,9 @@ defp get_app_from_request(conn, params) do
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -5,5 +5,4 @@
|
||||||
defmodule Pleroma.Web.OAuth.OAuthView do
|
defmodule Pleroma.Web.OAuth.OAuthView do
|
||||||
use Pleroma.Web, :view
|
use Pleroma.Web, :view
|
||||||
import Phoenix.HTML.Form
|
import Phoenix.HTML.Form
|
||||||
import Phoenix.HTML.Link
|
|
||||||
end
|
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" %>
|
<%= submit "Authorize" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<%= if Pleroma.Config.get([:auth, :oauth_consumer_enabled]) do %>
|
||||||
<br>
|
<br>
|
||||||
<%= link to: "/oauth/twitter", class: "alert alert-info" do %>
|
<%= render @view_module, "consumer.html", assigns %>
|
||||||
Sign in with Twitter
|
|
||||||
<% end %>
|
<% 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