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.ex57
-rw-r--r--lib/pleroma/web/oauth/scopes.ex67
-rw-r--r--lib/pleroma/web/oauth/token.ex11
5 files changed, 146 insertions, 29 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 e3c01217d..4ee8339e2 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -15,8 +15,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
-
- import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
+ alias Pleroma.Web.OAuth.Scopes
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
@@ -57,7 +56,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
defp do_authorize(conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
available_scopes = (app && app.scopes) || []
- scopes = oauth_scopes(params, nil) || available_scopes
+ scopes = Scopes.fetch_scopes(params, available_scopes)
# Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{
@@ -113,7 +112,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
defp handle_create_authorization_error(
conn,
- {scopes_issue, _},
+ {:error, scopes_issue},
%{"authorization" => _} = params
)
when scopes_issue in [:unsupported_scopes, :missing_scopes] do
@@ -184,9 +183,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
%App{} = app <- get_app_from_request(conn, params),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:user_active, true} <- {:user_active, !user.info.deactivated},
- scopes <- oauth_scopes(params, app.scopes),
- [] <- scopes -- app.scopes,
- true <- Enum.any?(scopes),
+ {: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))
@@ -221,6 +218,28 @@ defmodule Pleroma.Web.OAuth.OAuthController do
token_exchange(conn, params)
end
+ def token_exchange(conn, %{"grant_type" => "client_credentials"} = params) do
+ with %App{} = app <- get_app_from_request(conn, params),
+ {:ok, auth} <- Authorization.create_authorization(app, %User{}),
+ {:ok, token} <- Token.exchange_token(app, auth),
+ {:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do
+ response = %{
+ token_type: "Bearer",
+ access_token: token.token,
+ refresh_token: token.refresh_token,
+ created_at: DateTime.to_unix(inserted_at),
+ expires_in: 60 * 10,
+ scope: Enum.join(token.scopes, " ")
+ }
+
+ json(conn, response)
+ else
+ _error ->
+ put_status(conn, 400)
+ |> json(%{error: "Invalid credentials"})
+ end
+ end
+
# Bad request
def token_exchange(conn, params), do: bad_request(conn, params)
@@ -247,14 +266,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do
@doc "Prepares OAuth request to provider for Ueberauth"
def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do
scope =
- oauth_scopes(auth_attrs, [])
- |> Enum.join(" ")
+ auth_attrs
+ |> Scopes.fetch_scopes([])
+ |> Scopes.to_string()
state =
auth_attrs
|> Map.delete("scopes")
|> Map.put("scope", scope)
- |> Poison.encode!()
+ |> Jason.encode!()
params =
auth_attrs
@@ -318,7 +338,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
@@ -326,7 +346,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
client_id: auth_attrs["client_id"],
redirect_uri: auth_attrs["redirect_uri"],
state: auth_attrs["state"],
- scopes: oauth_scopes(auth_attrs, []),
+ scopes: Scopes.fetch_scopes(auth_attrs, []),
nickname: auth_attrs["nickname"],
email: auth_attrs["email"]
})
@@ -401,10 +421,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
- scopes <- oauth_scopes(auth_attrs, []),
- {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes},
- # Note: `scope` param is intentionally not optional in this context
- {:missing_scopes, false} <- {:missing_scopes, scopes == []},
+ {:ok, scopes} <- validate_scopes(app, auth_attrs),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)} do
Authorization.create_authorization(app, user, scopes)
end
@@ -458,4 +475,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do
}
|> Map.merge(opts)
end
+
+ @spec validate_scopes(App.t(), map()) ::
+ {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
+ defp validate_scopes(app, params) do
+ params
+ |> Scopes.fetch_scopes(app.scopes)
+ |> Scopes.validates(app.scopes)
+ end
end
diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex
new file mode 100644
index 000000000..ad9dfb260
--- /dev/null
+++ b/lib/pleroma/web/oauth/scopes.ex
@@ -0,0 +1,67 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.Scopes do
+ @moduledoc """
+ Functions for dealing with scopes.
+ """
+
+ @doc """
+ Fetch scopes from requiest params.
+
+ Note: `scopes` is used by Mastodon — supporting it but sticking to
+ OAuth's standard `scope` wherever we control it
+ """
+ @spec fetch_scopes(map(), list()) :: list()
+ def fetch_scopes(params, default) do
+ parse_scopes(params["scope"] || params["scopes"], default)
+ end
+
+ def parse_scopes(scopes, _default) when is_list(scopes) do
+ Enum.filter(scopes, &(&1 not in [nil, ""]))
+ end
+
+ def parse_scopes(scopes, default) when is_binary(scopes) do
+ scopes
+ |> to_list
+ |> parse_scopes(default)
+ end
+
+ def parse_scopes(_, default) do
+ default
+ end
+
+ @doc """
+ Convert scopes string to list
+ """
+ @spec to_list(binary()) :: [binary()]
+ def to_list(nil), do: []
+
+ def to_list(str) do
+ str
+ |> String.trim()
+ |> String.split(~r/[\s,]+/)
+ end
+
+ @doc """
+ Convert scopes list to string
+ """
+ @spec to_string(list()) :: binary()
+ def to_string(scopes), do: Enum.join(scopes, " ")
+
+ @doc """
+ Validates scopes.
+ """
+ @spec validates(list() | nil, list()) ::
+ {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
+ def validates([], _app_scopes), do: {:error, :missing_scopes}
+ def validates(nil, _app_scopes), do: {:error, :missing_scopes}
+
+ def validates(scopes, app_scopes) do
+ case scopes -- app_scopes do
+ [] -> {:ok, scopes}
+ _ -> {:error, :unsupported_scopes}
+ end
+ end
+end
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