aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/web/oauth
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/web/oauth')
-rw-r--r--lib/pleroma/web/oauth/app.ex1
-rw-r--r--lib/pleroma/web/oauth/authorization.ex39
-rw-r--r--lib/pleroma/web/oauth/oauth_controller.ex75
-rw-r--r--lib/pleroma/web/oauth/token.ex11
-rw-r--r--lib/pleroma/web/oauth/token/response.ex32
-rw-r--r--lib/pleroma/web/oauth/token/utils.ex38
6 files changed, 131 insertions, 65 deletions
diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex
index bccc2ac96..ddcdb1871 100644
--- a/lib/pleroma/web/oauth/app.ex
+++ b/lib/pleroma/web/oauth/app.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.App do
import Ecto.Changeset
@type t :: %__MODULE__{}
+
schema "apps" do
field(:client_name, :string)
field(:redirect_uris, :string)
diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex
index ca3901cc4..b47688de1 100644
--- a/lib/pleroma/web/oauth/authorization.ex
+++ b/lib/pleroma/web/oauth/authorization.ex
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
import Ecto.Query
@type t :: %__MODULE__{}
+
schema "oauth_authorizations" do
field(:token, :string)
field(:scopes, {:array, :string}, default: [])
@@ -25,28 +26,45 @@ defmodule Pleroma.Web.OAuth.Authorization do
timestamps()
end
+ @spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) ::
+ {:ok, Authorization.t()} | {:error, Changeset.t()}
def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do
- scopes = scopes || app.scopes
- token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
-
- authorization = %Authorization{
- token: token,
- used: false,
+ %{
+ scopes: scopes || app.scopes,
user_id: user.id,
- app_id: app.id,
- scopes: scopes,
- valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
+ app_id: app.id
}
+ |> create_changeset()
+ |> Repo.insert()
+ end
+
+ @spec create_changeset(map()) :: Changeset.t()
+ def create_changeset(attrs \\ %{}) do
+ %Authorization{}
+ |> cast(attrs, [:user_id, :app_id, :scopes, :valid_until])
+ |> validate_required([:app_id, :scopes])
+ |> add_token()
+ |> add_lifetime()
+ end
+
+ defp add_token(changeset) do
+ token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
+ put_change(changeset, :token, token)
+ end
- Repo.insert(authorization)
+ defp add_lifetime(changeset) do
+ put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10))
end
+ @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t()
def use_changeset(%Authorization{} = auth, params) do
auth
|> cast(params, [:used])
|> validate_required([:used])
end
+ @spec use_token(Authorization.t()) ::
+ {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()}
def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do
if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do
Repo.update(use_changeset(auth, %{used: true}))
@@ -57,6 +75,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
def use_token(%Authorization{used: true}), do: {:error, "already used"}
+ @spec delete_user_authorizations(User.t()) :: {integer(), any()}
def delete_user_authorizations(%User{id: user_id}) do
from(
a in Pleroma.Web.OAuth.Authorization,
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 8ee0da667..ae2b80d95 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -19,8 +19,6 @@ defmodule Pleroma.Web.OAuth.OAuthController do
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
- @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
-
plug(:fetch_session)
plug(:fetch_flash)
@@ -144,14 +142,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do
@doc "Renew access_token with refresh_token"
def token_exchange(
conn,
- %{"grant_type" => "refresh_token", "refresh_token" => token} = params
+ %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
) do
- with %App{} = app <- get_app_from_request(conn, params),
+ with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)}
- json(conn, response_token(user, token, response_attrs))
+ json(conn, Token.Response.build(user, token, response_attrs))
else
_error ->
put_status(conn, 400)
@@ -160,14 +158,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
- with %App{} = app <- get_app_from_request(conn, params),
+ with {:ok, app} <- Token.Utils.fetch_app(conn),
fixed_token = Token.Utils.fix_padding(params["code"]),
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)}
- json(conn, response_token(user, token, response_attrs))
+ json(conn, Token.Response.build(user, token, response_attrs))
else
_error ->
put_status(conn, 400)
@@ -179,14 +177,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do
conn,
%{"grant_type" => "password"} = params
) do
- with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)},
- %App{} = app <- get_app_from_request(conn, params),
+ with {:ok, %User{} = user} <- Authenticator.get_user(conn),
+ {:ok, app} <- Token.Utils.fetch_app(conn),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:user_active, true} <- {:user_active, !user.info.deactivated},
{:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do
- json(conn, response_token(user, token))
+ json(conn, Token.Response.build(user, token))
else
{:auth_active, false} ->
# Per https://github.com/tootsuite/mastodon/blob/
@@ -218,11 +216,23 @@ defmodule Pleroma.Web.OAuth.OAuthController do
token_exchange(conn, params)
end
+ def token_exchange(conn, %{"grant_type" => "client_credentials"} = _params) do
+ with {:ok, app} <- Token.Utils.fetch_app(conn),
+ {:ok, auth} <- Authorization.create_authorization(app, %User{}),
+ {:ok, token} <- Token.exchange_token(app, auth) do
+ json(conn, Token.Response.build_for_client_credentials(token))
+ else
+ _error ->
+ put_status(conn, 400)
+ |> json(%{error: "Invalid credentials"})
+ end
+ end
+
# Bad request
def token_exchange(conn, params), do: bad_request(conn, params)
def token_revoke(conn, %{"token" => _token} = params) do
- with %App{} = app <- get_app_from_request(conn, params),
+ with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, _token} <- RevokeToken.revoke(app, params) do
json(conn, %{})
else
@@ -252,7 +262,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
auth_attrs
|> Map.delete("scopes")
|> Map.put("scope", scope)
- |> Poison.encode!()
+ |> Jason.encode!()
params =
auth_attrs
@@ -316,7 +326,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
defp callback_params(%{"state" => state} = params) do
- Map.merge(params, Poison.decode!(state))
+ Map.merge(params, Jason.decode!(state))
end
def registration_details(conn, %{"authorization" => auth_attrs}) do
@@ -405,33 +415,6 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
- defp get_app_from_request(conn, params) do
- conn
- |> fetch_client_credentials(params)
- |> fetch_client
- end
-
- defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do
- Repo.get_by(App, client_id: id, client_secret: secret)
- end
-
- defp fetch_client({_id, _secret}), do: nil
-
- defp fetch_client_credentials(conn, params) do
- # Per RFC 6749, HTTP Basic is preferred to body params
- with ["Basic " <> encoded] <- get_req_header(conn, "authorization"),
- {:ok, decoded} <- Base.decode64(encoded),
- [id, secret] <-
- Enum.map(
- String.split(decoded, ":"),
- fn s -> URI.decode_www_form(s) end
- ) do
- {id, secret}
- else
- _ -> {params["client_id"], params["client_secret"]}
- end
- end
-
# Special case: Local MastodonFE
defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login)
@@ -442,18 +425,6 @@ defmodule Pleroma.Web.OAuth.OAuthController do
defp put_session_registration_id(conn, registration_id),
do: put_session(conn, :registration_id, registration_id)
- defp response_token(%User{} = user, token, opts \\ %{}) do
- %{
- token_type: "Bearer",
- access_token: token.token,
- refresh_token: token.refresh_token,
- expires_in: @expires_in,
- scope: Enum.join(token.scopes, " "),
- me: user.ap_id
- }
- |> Map.merge(opts)
- end
-
@spec validate_scopes(App.t(), map()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(app, params) do
diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex
index 4e5d1d118..ef047d565 100644
--- a/lib/pleroma/web/oauth/token.ex
+++ b/lib/pleroma/web/oauth/token.ex
@@ -45,12 +45,16 @@ defmodule Pleroma.Web.OAuth.Token do
|> Repo.find_resource()
end
+ @spec exchange_token(App.t(), Authorization.t()) ::
+ {:ok, Token.t()} | {:error, Changeset.t()}
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do
+ user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{}
+
create_token(
app,
- User.get_cached_by_id(auth.user_id),
+ user,
%{scopes: auth.scopes}
)
end
@@ -81,12 +85,13 @@ defmodule Pleroma.Web.OAuth.Token do
|> validate_required([:valid_until])
end
+ @spec create_token(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()}
def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do
%__MODULE__{user_id: user.id, app_id: app.id}
|> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes])
- |> validate_required([:scopes, :user_id, :app_id])
+ |> validate_required([:scopes, :app_id])
|> put_valid_until(attrs)
- |> put_token
+ |> put_token()
|> put_refresh_token(attrs)
|> Repo.insert()
end
diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex
new file mode 100644
index 000000000..64e78b183
--- /dev/null
+++ b/lib/pleroma/web/oauth/token/response.ex
@@ -0,0 +1,32 @@
+defmodule Pleroma.Web.OAuth.Token.Response do
+ @moduledoc false
+
+ alias Pleroma.User
+ alias Pleroma.Web.OAuth.Token.Utils
+
+ @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
+
+ @doc false
+ def build(%User{} = user, token, opts \\ %{}) do
+ %{
+ token_type: "Bearer",
+ access_token: token.token,
+ refresh_token: token.refresh_token,
+ expires_in: @expires_in,
+ scope: Enum.join(token.scopes, " "),
+ me: user.ap_id
+ }
+ |> Map.merge(opts)
+ end
+
+ def build_for_client_credentials(token) do
+ %{
+ token_type: "Bearer",
+ access_token: token.token,
+ refresh_token: token.refresh_token,
+ created_at: Utils.format_created_at(token),
+ expires_in: @expires_in,
+ scope: Enum.join(token.scopes, " ")
+ }
+ end
+end
diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex
index a81560a1c..7a4fddafd 100644
--- a/lib/pleroma/web/oauth/token/utils.ex
+++ b/lib/pleroma/web/oauth/token/utils.ex
@@ -3,6 +3,44 @@ defmodule Pleroma.Web.OAuth.Token.Utils do
Auxiliary functions for dealing with tokens.
"""
+ alias Pleroma.Repo
+ alias Pleroma.Web.OAuth.App
+
+ @doc "Fetch app by client credentials from request"
+ @spec fetch_app(Plug.Conn.t()) :: {:ok, App.t()} | {:error, :not_found}
+ def fetch_app(conn) do
+ res =
+ conn
+ |> fetch_client_credentials()
+ |> fetch_client
+
+ case res do
+ %App{} = app -> {:ok, app}
+ _ -> {:error, :not_found}
+ end
+ end
+
+ defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do
+ Repo.get_by(App, client_id: id, client_secret: secret)
+ end
+
+ defp fetch_client({_id, _secret}), do: nil
+
+ defp fetch_client_credentials(conn) do
+ # Per RFC 6749, HTTP Basic is preferred to body params
+ with ["Basic " <> encoded] <- Plug.Conn.get_req_header(conn, "authorization"),
+ {:ok, decoded} <- Base.decode64(encoded),
+ [id, secret] <-
+ Enum.map(
+ String.split(decoded, ":"),
+ fn s -> URI.decode_www_form(s) end
+ ) do
+ {id, secret}
+ else
+ _ -> {conn.params["client_id"], conn.params["client_secret"]}
+ end
+ end
+
@doc "convert token inserted_at to unix timestamp"
def format_created_at(%{inserted_at: inserted_at} = _token) do
inserted_at