Refactored login code in OAuthController, reused in AccountController. Added tests.issue/1969
@@ -449,21 +449,32 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do | |||||
} | } | ||||
end | end | ||||
# TODO: This is actually a token respone, but there's no oauth operation file yet. | |||||
# Note: this is a token response (if login succeeds!), but there's no oauth operation file yet. | |||||
defp create_response do | defp create_response do | ||||
%Schema{ | %Schema{ | ||||
title: "AccountCreateResponse", | title: "AccountCreateResponse", | ||||
description: "Response schema for an account", | description: "Response schema for an account", | ||||
type: :object, | type: :object, | ||||
properties: %{ | properties: %{ | ||||
# The response when auto-login on create succeeds (token is issued): | |||||
token_type: %Schema{type: :string}, | token_type: %Schema{type: :string}, | ||||
access_token: %Schema{type: :string}, | access_token: %Schema{type: :string}, | ||||
refresh_token: %Schema{type: :string}, | refresh_token: %Schema{type: :string}, | ||||
scope: %Schema{type: :string}, | scope: %Schema{type: :string}, | ||||
created_at: %Schema{type: :integer, format: :"date-time"}, | created_at: %Schema{type: :integer, format: :"date-time"}, | ||||
me: %Schema{type: :string}, | me: %Schema{type: :string}, | ||||
expires_in: %Schema{type: :integer} | |||||
expires_in: %Schema{type: :integer}, | |||||
# | |||||
# The response when registration succeeds but auto-login fails (no token): | |||||
identifier: %Schema{type: :string}, | |||||
message: %Schema{type: :string} | |||||
}, | }, | ||||
required: [], | |||||
# Note: example of successful registration with failed login response: | |||||
# example: %{ | |||||
# "identifier" => "missing_confirmed_email", | |||||
# "message" => "You have been registered. Please check your email for further instructions." | |||||
# }, | |||||
example: %{ | example: %{ | ||||
"token_type" => "Bearer", | "token_type" => "Bearer", | ||||
"access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", | "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", | ||||
@@ -27,8 +27,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||||
alias Pleroma.Web.MastodonAPI.MastodonAPI | alias Pleroma.Web.MastodonAPI.MastodonAPI | ||||
alias Pleroma.Web.MastodonAPI.MastodonAPIController | alias Pleroma.Web.MastodonAPI.MastodonAPIController | ||||
alias Pleroma.Web.MastodonAPI.StatusView | alias Pleroma.Web.MastodonAPI.StatusView | ||||
alias Pleroma.Web.OAuth.OAuthController | |||||
alias Pleroma.Web.OAuth.OAuthView | alias Pleroma.Web.OAuth.OAuthView | ||||
alias Pleroma.Web.OAuth.Token | |||||
alias Pleroma.Web.TwitterAPI.TwitterAPI | alias Pleroma.Web.TwitterAPI.TwitterAPI | ||||
plug(Pleroma.Web.ApiSpec.CastAndValidate) | plug(Pleroma.Web.ApiSpec.CastAndValidate) | ||||
@@ -101,10 +101,33 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||||
with :ok <- validate_email_param(params), | with :ok <- validate_email_param(params), | ||||
:ok <- TwitterAPI.validate_captcha(app, params), | :ok <- TwitterAPI.validate_captcha(app, params), | ||||
{:ok, user} <- TwitterAPI.register_user(params), | {:ok, user} <- TwitterAPI.register_user(params), | ||||
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do | |||||
{_, {:ok, token}} <- | |||||
{:login, OAuthController.login(user, app, app.scopes)} do | |||||
json(conn, OAuthView.render("token.json", %{user: user, token: token})) | json(conn, OAuthView.render("token.json", %{user: user, token: token})) | ||||
else | else | ||||
{:error, error} -> json_response(conn, :bad_request, %{error: error}) | |||||
{:login, {:account_status, :confirmation_pending}} -> | |||||
json_response(conn, :ok, %{ | |||||
message: "You have been registered. Please check your email for further instructions.", | |||||
identifier: "missing_confirmed_email" | |||||
}) | |||||
{:login, {:account_status, :approval_pending}} -> | |||||
json_response(conn, :ok, %{ | |||||
message: | |||||
"You have been registered. You'll be able to log in once your account is approved.", | |||||
identifier: "awaiting_approval" | |||||
}) | |||||
{:login, _} -> | |||||
json_response(conn, :ok, %{ | |||||
message: | |||||
"You have been registered. Some post-registration steps may be pending. " <> | |||||
"Please log in manually.", | |||||
identifier: "manual_login_required" | |||||
}) | |||||
{:error, error} -> | |||||
json_response(conn, :bad_request, %{error: error}) | |||||
end | end | ||||
end | end | ||||
@@ -260,11 +260,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
) do | ) do | ||||
with {:ok, %User{} = user} <- Authenticator.get_user(conn), | with {:ok, %User{} = user} <- Authenticator.get_user(conn), | ||||
{:ok, app} <- Token.Utils.fetch_app(conn), | {:ok, app} <- Token.Utils.fetch_app(conn), | ||||
{:account_status, :active} <- {:account_status, User.account_status(user)}, | |||||
{:ok, scopes} <- validate_scopes(app, params), | |||||
{:ok, auth} <- Authorization.create_authorization(app, user, scopes), | |||||
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, | |||||
{:ok, token} <- Token.exchange_token(app, auth) do | |||||
requested_scopes <- Scopes.fetch_scopes(params, app.scopes), | |||||
{:ok, token} <- login(user, app, requested_scopes) do | |||||
json(conn, OAuthView.render("token.json", %{user: user, token: token})) | json(conn, OAuthView.render("token.json", %{user: user, token: token})) | ||||
else | else | ||||
error -> | error -> | ||||
@@ -522,6 +519,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
end | end | ||||
end | end | ||||
defp do_create_authorization(conn, auth_attrs, user \\ nil) | |||||
defp do_create_authorization( | defp do_create_authorization( | ||||
%Plug.Conn{} = conn, | %Plug.Conn{} = conn, | ||||
%{ | %{ | ||||
@@ -531,19 +530,37 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
"redirect_uri" => redirect_uri | "redirect_uri" => redirect_uri | ||||
} = auth_attrs | } = auth_attrs | ||||
}, | }, | ||||
user \\ nil | |||||
user | |||||
) do | ) do | ||||
with {_, {:ok, %User{} = user}} <- | with {_, {:ok, %User{} = user}} <- | ||||
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, | {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, | ||||
%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), | ||||
{:ok, scopes} <- validate_scopes(app, auth_attrs), | |||||
{:account_status, :active} <- {:account_status, User.account_status(user)}, | |||||
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do | |||||
requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), | |||||
{:ok, auth} <- do_create_authorization(user, app, requested_scopes) do | |||||
{:ok, auth, user} | {:ok, auth, user} | ||||
end | end | ||||
end | end | ||||
defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes) | |||||
when is_list(requested_scopes) do | |||||
with {:account_status, :active} <- {:account_status, User.account_status(user)}, | |||||
{:ok, scopes} <- validate_scopes(app, requested_scopes), | |||||
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do | |||||
{:ok, auth} | |||||
end | |||||
end | |||||
# Note: intended to be a private function but opened for AccountController that logs in on signup | |||||
@doc "If checks pass, creates authorization and token for given user, app and requested scopes." | |||||
def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do | |||||
with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), | |||||
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, | |||||
{:ok, token} <- Token.exchange_token(app, auth) do | |||||
{:ok, token} | |||||
end | |||||
end | |||||
# Special case: Local MastodonFE | # Special case: Local MastodonFE | ||||
defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) | defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) | ||||
@@ -560,12 +577,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
end | end | ||||
end | end | ||||
@spec validate_scopes(App.t(), map()) :: | |||||
@spec validate_scopes(App.t(), map() | list()) :: | |||||
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} | {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} | ||||
defp validate_scopes(%App{} = app, params) do | |||||
params | |||||
|> Scopes.fetch_scopes(app.scopes) | |||||
|> Scopes.validate(app.scopes) | |||||
defp validate_scopes(%App{} = app, params) when is_map(params) do | |||||
requested_scopes = Scopes.fetch_scopes(params, app.scopes) | |||||
validate_scopes(app, requested_scopes) | |||||
end | |||||
defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do | |||||
Scopes.validate(requested_scopes, app.scopes) | |||||
end | end | ||||
def default_redirect_uri(%App{} = app) do | def default_redirect_uri(%App{} = app) do | ||||
@@ -5,7 +5,6 @@ | |||||
defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | ||||
use Pleroma.Web.ConnCase | use Pleroma.Web.ConnCase | ||||
alias Pleroma.Config | |||||
alias Pleroma.Repo | alias Pleroma.Repo | ||||
alias Pleroma.User | alias Pleroma.User | ||||
alias Pleroma.Web.ActivityPub.ActivityPub | alias Pleroma.Web.ActivityPub.ActivityPub | ||||
@@ -16,8 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | |||||
import Pleroma.Factory | import Pleroma.Factory | ||||
describe "account fetching" do | describe "account fetching" do | ||||
setup do: clear_config([:instance, :limit_to_local_content]) | |||||
test "works by id" do | test "works by id" do | ||||
%User{id: user_id} = insert(:user) | %User{id: user_id} = insert(:user) | ||||
@@ -42,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | |||||
end | end | ||||
test "works by nickname for remote users" do | test "works by nickname for remote users" do | ||||
Config.put([:instance, :limit_to_local_content], false) | |||||
clear_config([:instance, :limit_to_local_content], false) | |||||
user = insert(:user, nickname: "user@example.com", local: false) | user = insert(:user, nickname: "user@example.com", local: false) | ||||
@@ -53,7 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | |||||
end | end | ||||
test "respects limit_to_local_content == :all for remote user nicknames" do | test "respects limit_to_local_content == :all for remote user nicknames" do | ||||
Config.put([:instance, :limit_to_local_content], :all) | |||||
clear_config([:instance, :limit_to_local_content], :all) | |||||
user = insert(:user, nickname: "user@example.com", local: false) | user = insert(:user, nickname: "user@example.com", local: false) | ||||
@@ -63,7 +60,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | |||||
end | end | ||||
test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do | test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do | ||||
Config.put([:instance, :limit_to_local_content], :unauthenticated) | |||||
clear_config([:instance, :limit_to_local_content], :unauthenticated) | |||||
user = insert(:user, nickname: "user@example.com", local: false) | user = insert(:user, nickname: "user@example.com", local: false) | ||||
reading_user = insert(:user) | reading_user = insert(:user) | ||||
@@ -903,8 +900,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | |||||
[valid_params: valid_params] | [valid_params: valid_params] | ||||
end | end | ||||
test "Account registration via Application, no confirmation required", %{conn: conn} do | |||||
test "registers and logs in without :account_activation_required / :account_approval_required", | |||||
%{conn: conn} do | |||||
clear_config([:instance, :account_activation_required], false) | clear_config([:instance, :account_activation_required], false) | ||||
clear_config([:instance, :account_approval_required], false) | |||||
conn = | conn = | ||||
conn | conn | ||||
@@ -962,15 +961,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | |||||
token_from_db = Repo.get_by(Token, token: token) | token_from_db = Repo.get_by(Token, token: token) | ||||
assert token_from_db | assert token_from_db | ||||
token_from_db = Repo.preload(token_from_db, :user) | |||||
assert token_from_db.user | |||||
refute token_from_db.user.confirmation_pending | |||||
end | |||||
user = Repo.preload(token_from_db, :user).user | |||||
setup do: clear_config([:instance, :account_approval_required]) | |||||
assert user | |||||
refute user.confirmation_pending | |||||
refute user.approval_pending | |||||
end | |||||
test "Account registration via Application", %{conn: conn} do | |||||
test "registers but does not log in with :account_activation_required", %{conn: conn} do | |||||
clear_config([:instance, :account_activation_required], true) | clear_config([:instance, :account_activation_required], true) | ||||
clear_config([:instance, :account_approval_required], false) | |||||
conn = | conn = | ||||
conn | conn | ||||
@@ -1019,23 +1019,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | |||||
agreement: true | agreement: true | ||||
}) | }) | ||||
%{ | |||||
"access_token" => token, | |||||
"created_at" => _created_at, | |||||
"scope" => ^scope, | |||||
"token_type" => "Bearer" | |||||
} = json_response_and_validate_schema(conn, 200) | |||||
token_from_db = Repo.get_by(Token, token: token) | |||||
assert token_from_db | |||||
token_from_db = Repo.preload(token_from_db, :user) | |||||
assert token_from_db.user | |||||
response = json_response_and_validate_schema(conn, 200) | |||||
assert %{"identifier" => "missing_confirmed_email"} = response | |||||
refute response["access_token"] | |||||
refute response["token_type"] | |||||
assert token_from_db.user.confirmation_pending | |||||
user = Repo.get_by(User, email: "lain@example.org") | |||||
assert user.confirmation_pending | |||||
end | end | ||||
test "Account registration via app with account_approval_required", %{conn: conn} do | |||||
Pleroma.Config.put([:instance, :account_approval_required], true) | |||||
test "registers but does not log in with :account_approval_required", %{conn: conn} do | |||||
clear_config([:instance, :account_approval_required], true) | |||||
clear_config([:instance, :account_activation_required], false) | |||||
conn = | conn = | ||||
conn | conn | ||||
@@ -1085,21 +1080,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | |||||
reason: "I'm a cool dude, bro" | reason: "I'm a cool dude, bro" | ||||
}) | }) | ||||
%{ | |||||
"access_token" => token, | |||||
"created_at" => _created_at, | |||||
"scope" => ^scope, | |||||
"token_type" => "Bearer" | |||||
} = json_response_and_validate_schema(conn, 200) | |||||
token_from_db = Repo.get_by(Token, token: token) | |||||
assert token_from_db | |||||
token_from_db = Repo.preload(token_from_db, :user) | |||||
assert token_from_db.user | |||||
response = json_response_and_validate_schema(conn, 200) | |||||
assert %{"identifier" => "awaiting_approval"} = response | |||||
refute response["access_token"] | |||||
refute response["token_type"] | |||||
assert token_from_db.user.approval_pending | |||||
user = Repo.get_by(User, email: "lain@example.org") | |||||
assert token_from_db.user.registration_reason == "I'm a cool dude, bro" | |||||
assert user.approval_pending | |||||
assert user.registration_reason == "I'm a cool dude, bro" | |||||
end | end | ||||
test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do | test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do | ||||
@@ -1153,11 +1142,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do | |||||
end) | end) | ||||
end | end | ||||
setup do: clear_config([:instance, :account_activation_required]) | |||||
test "returns bad_request if missing email params when :account_activation_required is enabled", | test "returns bad_request if missing email params when :account_activation_required is enabled", | ||||
%{conn: conn, valid_params: valid_params} do | %{conn: conn, valid_params: valid_params} do | ||||
Pleroma.Config.put([:instance, :account_activation_required], true) | |||||
clear_config([:instance, :account_activation_required], true) | |||||
app_token = insert(:oauth_token, user: nil) | app_token = insert(:oauth_token, user: nil) | ||||