expire mfa tokens through Oban

This commit is contained in:
Alexander Strizhakov 2020-09-05 18:35:01 +03:00
parent 3ce658b930
commit 7dd986a563
No known key found for this signature in database
GPG key ID: 022896A53AEF1381
11 changed files with 106 additions and 117 deletions

View file

@ -530,7 +530,7 @@ config :pleroma, Oban,
log: false, log: false,
queues: [ queues: [
activity_expiration: 10, activity_expiration: 10,
oauth_token_expiration: 1, token_expiration: 5,
federator_incoming: 50, federator_incoming: 50,
federator_outgoing: 50, federator_outgoing: 50,
web_push: 50, web_push: 50,

View file

@ -691,9 +691,8 @@ Pleroma has the following queues:
Pleroma has these periodic job workers: Pleroma has these periodic job workers:
`Pleroma.Workers.Cron.ClearOauthTokenWorker` - a job worker to cleanup expired oauth tokens. * `Pleroma.Workers.Cron.DigestEmailsWorker` - digest emails for users with new mentions and follows
* `Pleroma.Workers.Cron.NewUsersDigestWorker` - digest emails for admins with new registrations
Example:
```elixir ```elixir
config :pleroma, Oban, config :pleroma, Oban,
@ -705,7 +704,8 @@ config :pleroma, Oban,
federator_outgoing: 50 federator_outgoing: 50
], ],
crontab: [ crontab: [
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker} {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
] ]
``` ```
@ -972,7 +972,7 @@ Configure OAuth 2 provider capabilities:
* `token_expires_in` - The lifetime in seconds of the access token. * `token_expires_in` - The lifetime in seconds of the access token.
* `issue_new_refresh_token` - Keeps old refresh token or generate new refresh token when to obtain an access token. * `issue_new_refresh_token` - Keeps old refresh token or generate new refresh token when to obtain an access token.
* `clean_expired_tokens` - Enable a background job to clean expired oauth tokens. Defaults to `false`. Interval settings sets in configuration periodic jobs [`Oban.Cron`](#obancron) * `clean_expired_tokens` - Enable a background job to clean expired oauth tokens. Defaults to `false`.
## Link parsing ## Link parsing

View file

@ -10,10 +10,11 @@ defmodule Pleroma.MFA.Token do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token, as: OAuthToken
@expires 300 @expires 300
@type t() :: %__MODULE__{}
schema "mfa_tokens" do schema "mfa_tokens" do
field(:token, :string) field(:token, :string)
field(:valid_until, :naive_datetime_usec) field(:valid_until, :naive_datetime_usec)
@ -24,6 +25,7 @@ defmodule Pleroma.MFA.Token do
timestamps() timestamps()
end end
@spec get_by_token(String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(token) do def get_by_token(token) do
from( from(
t in __MODULE__, t in __MODULE__,
@ -33,33 +35,40 @@ defmodule Pleroma.MFA.Token do
|> Repo.find_resource() |> Repo.find_resource()
end end
def validate(token) do @spec validate(String.t()) :: {:ok, t()} | {:error, :not_found} | {:error, :expired_token}
with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)}, def validate(token_str) do
{:expired, false} <- {:expired, is_expired?(token)} do with {:ok, token} <- get_by_token(token_str),
false <- expired?(token) do
{:ok, token} {:ok, token}
else
{:expired, _} -> {:error, :expired_token}
{:fetch_token, _} -> {:error, :not_found}
error -> {:error, error}
end end
end end
def create_token(%User{} = user) do defp expired?(%__MODULE__{valid_until: valid_until}) do
%__MODULE__{} with true <- NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 do
|> change {:error, :expired_token}
|> assign_user(user) end
|> put_token
|> put_valid_until
|> Repo.insert()
end end
def create_token(user, authorization) do @spec create(User.t(), Authorization.t() | nil) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def create(user, authorization \\ nil) do
with {:ok, token} <- do_create(user, authorization) do
Pleroma.Workers.PurgeExpiredToken.enqueue(%{
token_id: token.id,
valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"),
mod: __MODULE__
})
{:ok, token}
end
end
defp do_create(user, authorization) do
%__MODULE__{} %__MODULE__{}
|> change |> change()
|> assign_user(user) |> assign_user(user)
|> assign_authorization(authorization) |> maybe_assign_authorization(authorization)
|> put_token |> put_token()
|> put_valid_until |> put_valid_until()
|> Repo.insert() |> Repo.insert()
end end
@ -69,15 +78,19 @@ defmodule Pleroma.MFA.Token do
|> validate_required([:user]) |> validate_required([:user])
end end
defp assign_authorization(changeset, authorization) do defp maybe_assign_authorization(changeset, %Authorization{} = authorization) do
changeset changeset
|> put_assoc(:authorization, authorization) |> put_assoc(:authorization, authorization)
|> validate_required([:authorization]) |> validate_required([:authorization])
end end
defp maybe_assign_authorization(changeset, _), do: changeset
defp put_token(changeset) do defp put_token(changeset) do
token = Pleroma.Web.OAuth.Token.Utils.generate_token()
changeset changeset
|> change(%{token: OAuthToken.Utils.generate_token()}) |> change(%{token: token})
|> validate_required([:token]) |> validate_required([:token])
|> unique_constraint(:token) |> unique_constraint(:token)
end end
@ -89,18 +102,4 @@ defmodule Pleroma.MFA.Token do
|> change(%{valid_until: expires_in}) |> change(%{valid_until: expires_in})
|> validate_required([:valid_until]) |> validate_required([:valid_until])
end end
def is_expired?(%__MODULE__{valid_until: valid_until}) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
end
def is_expired?(_), do: false
def delete_expired_tokens do
from(
q in __MODULE__,
where: fragment("?", q.valid_until) < ^Timex.now()
)
|> Repo.delete_all()
end
end end

View file

@ -197,7 +197,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:mfa_required, user, auth, _}, {:mfa_required, user, auth, _},
params params
) do ) do
{:ok, token} = MFA.Token.create_token(user, auth) {:ok, token} = MFA.Token.create(user, auth)
data = %{ data = %{
"mfa_token" => token.token, "mfa_token" => token.token,
@ -579,7 +579,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
do: put_session(conn, :registration_id, registration_id) do: put_session(conn, :registration_id, registration_id)
defp build_and_response_mfa_token(user, auth) do defp build_and_response_mfa_token(user, auth) do
with {:ok, token} <- MFA.Token.create_token(user, auth) do with {:ok, token} <- MFA.Token.create(user, auth) do
MFAView.render("mfa_response.json", %{token: token, user: user}) MFAView.render("mfa_response.json", %{token: token, user: user})
end end
end end

View file

@ -87,9 +87,10 @@ defmodule Pleroma.Web.OAuth.Token do
def create(%App{} = app, %User{} = user, attrs \\ %{}) do def create(%App{} = app, %User{} = user, attrs \\ %{}) do
with {:ok, token} <- do_create(app, user, attrs) do with {:ok, token} <- do_create(app, user, attrs) do
if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do
Pleroma.Workers.PurgeExpiredOAuthToken.enqueue(%{ Pleroma.Workers.PurgeExpiredToken.enqueue(%{
token_id: token.id, token_id: token.id,
valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC") valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"),
mod: __MODULE__
}) })
end end

View file

@ -1,36 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.CleanWorker do
@moduledoc """
The module represents functions to clean an expired OAuth and MFA tokens.
"""
use GenServer
@ten_seconds 10_000
@one_day 86_400_000
alias Pleroma.MFA
alias Pleroma.Workers.BackgroundWorker
def start_link(_), do: GenServer.start_link(__MODULE__, %{})
def init(_) do
Process.send_after(self(), :perform, @ten_seconds)
{:ok, nil}
end
@doc false
def handle_info(:perform, state) do
BackgroundWorker.enqueue("clean_expired_tokens", %{})
interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day)
Process.send_after(self(), :perform, interval)
{:noreply, state}
end
def perform(:clean) do
MFA.Token.delete_expired_tokens()
end
end

View file

@ -135,7 +135,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
end end
defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do
{:ok, %{token: token}} = MFA.Token.create_token(user) {:ok, %{token: token}} = MFA.Token.create(user)
render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false}) render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false})
end end

View file

@ -2,14 +2,14 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.PurgeExpiredOAuthToken do defmodule Pleroma.Workers.PurgeExpiredToken do
@moduledoc """ @moduledoc """
Worker which purges expired OAuth tokens Worker which purges expired OAuth tokens
""" """
use Oban.Worker, queue: :oauth_token_expiration, max_attempts: 1 use Oban.Worker, queue: :token_expiration, max_attempts: 1
@spec enqueue(%{token_id: integer(), valid_until: DateTime.t()}) :: @spec enqueue(%{token_id: integer(), valid_until: DateTime.t(), mod: module()}) ::
{:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()} {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()}
def enqueue(args) do def enqueue(args) do
{scheduled_at, args} = Map.pop(args, :valid_until) {scheduled_at, args} = Map.pop(args, :valid_until)
@ -20,8 +20,9 @@ defmodule Pleroma.Workers.PurgeExpiredOAuthToken do
end end
@impl true @impl true
def perform(%Oban.Job{args: %{"token_id" => id}}) do def perform(%Oban.Job{args: %{"token_id" => id, "mod" => module}}) do
Pleroma.Web.OAuth.Token module
|> String.to_existing_atom()
|> Pleroma.Repo.get(id) |> Pleroma.Repo.get(id)
|> Pleroma.Repo.delete() |> Pleroma.Repo.delete()
end end

View file

@ -227,7 +227,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
} }
) )
{:ok, %{token: token}} = MFA.Token.create_token(user) {:ok, %{token: token}} = MFA.Token.create(user)
user2 = insert(:user) user2 = insert(:user)
otp_token = TOTP.generate_token(otp_secret) otp_token = TOTP.generate_token(otp_secret)
@ -256,7 +256,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
} }
) )
{:ok, %{token: token}} = MFA.Token.create_token(user) {:ok, %{token: token}} = MFA.Token.create(user)
user2 = insert(:user) user2 = insert(:user)
otp_token = TOTP.generate_token(TOTP.generate_secret()) otp_token = TOTP.generate_token(TOTP.generate_secret())

View file

@ -1,27 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.PurgeExpiredOAuthTokenTest do
use Pleroma.DataCase, async: true
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
setup do: clear_config([:oauth2, :clean_expired_tokens], true)
test "purges expired token" do
user = insert(:user)
app = insert(:oauth_app)
{:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredOAuthToken,
args: %{token_id: id}
)
assert {:ok, %{id: ^id}} =
perform_job(Pleroma.Workers.PurgeExpiredOAuthToken, %{token_id: id})
end
end

View file

@ -0,0 +1,51 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.PurgeExpiredTokenTest do
use Pleroma.DataCase, async: true
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
setup do: clear_config([:oauth2, :clean_expired_tokens], true)
test "purges expired oauth token" do
user = insert(:user)
app = insert(:oauth_app)
{:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredToken,
args: %{token_id: id, mod: Pleroma.Web.OAuth.Token}
)
assert {:ok, %{id: ^id}} =
perform_job(Pleroma.Workers.PurgeExpiredToken, %{
token_id: id,
mod: Pleroma.Web.OAuth.Token
})
assert Repo.aggregate(Pleroma.Web.OAuth.Token, :count, :id) == 0
end
test "purges expired mfa token" do
authorization = insert(:oauth_authorization)
{:ok, %{id: id}} = Pleroma.MFA.Token.create(authorization.user, authorization)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredToken,
args: %{token_id: id, mod: Pleroma.MFA.Token}
)
assert {:ok, %{id: ^id}} =
perform_job(Pleroma.Workers.PurgeExpiredToken, %{
token_id: id,
mod: Pleroma.MFA.Token
})
assert Repo.aggregate(Pleroma.MFA.Token, :count, :id) == 0
end
end