From 1040caf096347b638b9fda5b23fcccde87b32ede Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 6 May 2019 17:51:03 +0000 Subject: [PATCH] fix format Modified-by: Maksim Pechnikov --- CHANGELOG.md | 1 + config/config.exs | 4 + docs/api/differences_in_mastoapi_responses.md | 9 +- docs/config.md | 9 +- lib/pleroma/repo.ex | 28 +++ lib/pleroma/web/oauth/app.ex | 1 + lib/pleroma/web/oauth/authorization.ex | 8 + lib/pleroma/web/oauth/oauth_controller.ex | 157 +++++++------- lib/pleroma/web/oauth/token.ex | 77 +++++-- .../web/oauth/token/strategy/refresh_token.ex | 54 +++++ .../web/oauth/token/strategy/revoke.ex | 22 ++ lib/pleroma/web/oauth/token/utils.ex | 30 +++ ...33552_add_refresh_token_index_to_token.exs | 7 + test/repo_test.exs | 44 ++++ test/web/oauth/oauth_controller_test.exs | 196 ++++++++++++++++++ 15 files changed, 561 insertions(+), 86 deletions(-) create mode 100644 lib/pleroma/web/oauth/token/strategy/refresh_token.ex create mode 100644 lib/pleroma/web/oauth/token/strategy/revoke.ex create mode 100644 lib/pleroma/web/oauth/token/utils.ex create mode 100644 priv/repo/migrations/20190501133552_add_refresh_token_index_to_token.exs create mode 100644 test/repo_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d44f6786..210aae2e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/) - ActivityPub C2S: OAuth endpoints - Metadata RelMe provider +- OAuth: added support for refresh tokens - Emoji packs and emoji pack manager ### Changed diff --git a/config/config.exs b/config/config.exs index 7792e9a87..946ba9adf 100644 --- a/config/config.exs +++ b/config/config.exs @@ -473,6 +473,10 @@ total_user_limit: 300, enabled: true +config :pleroma, :oauth2, + token_expires_in: 600, + issue_new_refresh_token: true + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 1350ace43..d3ba41b6a 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -1,6 +1,6 @@ # Differences in Mastodon API responses from vanilla Mastodon -A Pleroma instance can be identified by " (compatible; Pleroma )" present in `version` field in response from `/api/v1/instance` +A Pleroma instance can be identified by " (compatible; Pleroma )" present in `version` field in response from `/api/v1/instance` ## Flake IDs @@ -80,3 +80,10 @@ Additional parameters can be added to the JSON body/Form data: - `hide_favorites` - if true, user's favorites timeline will be hidden - `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API - `default_scope` - the scope returned under `privacy` key in Source subentity + +## Authentication + +*Pleroma supports refreshing tokens. + +`POST /oauth/token` +Post here request with grant_type=refresh_token to obtain new access token. Returns an access token. diff --git a/docs/config.md b/docs/config.md index bbdc22688..d999952e1 100644 --- a/docs/config.md +++ b/docs/config.md @@ -474,7 +474,7 @@ Authentication / authorization settings. * `oauth_consumer_template`: OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`. * `oauth_consumer_strategies`: the list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. -# OAuth consumer mode +## OAuth consumer mode OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). Implementation is based on Ueberauth; see the list of [available strategies](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies). @@ -527,6 +527,13 @@ config :ueberauth, Ueberauth, ] ``` +## OAuth 2.0 provider - :oauth2 + +Configure OAuth 2 provider capabilities: + +* `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. + ## :emoji * `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]` * `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]` diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index aa5d427ae..f57e088bc 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -19,4 +19,32 @@ defmodule Instrumenter do def init(_, opts) do {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} end + + @doc "find resource based on prepared query" + @spec find_resource(Ecto.Query.t()) :: {:ok, struct()} | {:error, :not_found} + def find_resource(%Ecto.Query{} = query) do + case __MODULE__.one(query) do + nil -> {:error, :not_found} + resource -> {:ok, resource} + end + end + + def find_resource(_query), do: {:error, :not_found} + + @doc """ + Gets association from cache or loads if need + + ## Examples + + iex> Repo.get_assoc(token, :user) + %User{} + + """ + @spec get_assoc(struct(), atom()) :: {:ok, struct()} | {:error, :not_found} + def get_assoc(resource, association) do + case __MODULE__.preload(resource, association) do + %{^association => assoc} when not is_nil(assoc) -> {:ok, assoc} + _ -> {:error, :not_found} + end + end end diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex index 3476da484..bccc2ac96 100644 --- a/lib/pleroma/web/oauth/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.App do use Ecto.Schema 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 3461f9983..ca3901cc4 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.Authorization do import Ecto.Changeset import Ecto.Query + @type t :: %__MODULE__{} schema "oauth_authorizations" do field(:token, :string) field(:scopes, {:array, :string}, default: []) @@ -63,4 +64,11 @@ def delete_user_authorizations(%User{id: user_id}) do ) |> Repo.delete_all() end + + @doc "gets auth for app by token" + @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_token(%App{id: app_id} = _app, token) do + from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) + |> Repo.find_resource() + end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 688eaca11..e3c01217d 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -13,11 +13,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization 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] 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) @@ -138,25 +142,33 @@ defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do Authenticator.handle_error(conn, error) end + @doc "Renew access_token with refresh_token" + def token_exchange( + conn, + %{"grant_type" => "refresh_token", "refresh_token" => token} = params + ) do + with %App{} = app <- get_app_from_request(conn, params), + {: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)) + else + _error -> + put_status(conn, 400) + |> json(%{error: "Invalid credentials"}) + end + end + def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do with %App{} = app <- get_app_from_request(conn, params), - fixed_token = fix_padding(params["code"]), - %Authorization{} = auth <- - Repo.get_by(Authorization, token: fixed_token, app_id: app.id), + 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), - {: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, " "), - me: user.ap_id - } + {:ok, token} <- Token.exchange_token(app, auth) do + response_attrs = %{created_at: Token.Utils.format_created_at(token)} - json(conn, response) + json(conn, response_token(user, token, response_attrs)) else _error -> put_status(conn, 400) @@ -177,16 +189,7 @@ def token_exchange( true <- Enum.any?(scopes), {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, token} <- Token.exchange_token(app, auth) do - response = %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - expires_in: 60 * 10, - scope: Enum.join(token.scopes, " "), - me: user.ap_id - } - - json(conn, response) + json(conn, response_token(user, token)) else {:auth_active, false} -> # Per https://github.com/tootsuite/mastodon/blob/ @@ -218,10 +221,12 @@ def token_exchange( token_exchange(conn, params) end - def token_revoke(conn, %{"token" => token} = params) do + # 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), - %Token{} = token <- Repo.get_by(Token, token: token, app_id: app.id), - {:ok, %Token{}} <- Repo.delete(token) do + {:ok, _token} <- RevokeToken.revoke(app, params) do json(conn, %{}) else _error -> @@ -230,6 +235,15 @@ def token_revoke(conn, %{"token" => token} = params) do end end + def token_revoke(conn, params), do: bad_request(conn, params) + + # Response for bad request + defp bad_request(conn, _) do + conn + |> put_status(500) + |> json(%{error: "Bad request"}) + end + @doc "Prepares OAuth request to provider for Ueberauth" def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do scope = @@ -278,25 +292,22 @@ def callback(conn, params) do params = callback_params(params) with {:ok, registration} <- Authenticator.get_registration(conn) do - user = Repo.preload(registration, :user).user auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) - if user do - create_authorization( - conn, - %{"authorization" => auth_attrs}, - user: user - ) - else - registration_params = - Map.merge(auth_attrs, %{ - "nickname" => Registration.nickname(registration), - "email" => Registration.email(registration) - }) + case Repo.get_assoc(registration, :user) do + {:ok, user} -> + create_authorization(conn, %{"authorization" => auth_attrs}, user: user) - conn - |> put_session(:registration_id, registration.id) - |> registration_details(%{"authorization" => registration_params}) + _ -> + registration_params = + Map.merge(auth_attrs, %{ + "nickname" => Registration.nickname(registration), + "email" => Registration.email(registration) + }) + + conn + |> put_session(:registration_id, registration.id) + |> registration_details(%{"authorization" => registration_params}) end else _ -> @@ -399,36 +410,30 @@ defp do_create_authorization( end end - # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be - # decoding it. Investigate sometime. - defp fix_padding(token) do - token - |> URI.decode() - |> Base.url_decode64!(padding: false) - |> Base.url_encode64(padding: false) + defp get_app_from_request(conn, params) do + conn + |> fetch_client_credentials(params) + |> fetch_client end - defp get_app_from_request(conn, params) do - # Per RFC 6749, HTTP Basic is preferred to body params - {client_id, client_secret} = - with ["Basic " <> encoded] <- get_req_header(conn, "authorization"), - {:ok, decoded} <- Base.decode64(encoded), - [id, secret] <- - String.split(decoded, ":") - |> Enum.map(fn s -> URI.decode_www_form(s) end) do - {id, secret} - else - _ -> {params["client_id"], params["client_secret"]} - 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 - if client_id && client_secret do - Repo.get_by( - App, - client_id: client_id, - client_secret: client_secret - ) + 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 - nil + _ -> {params["client_id"], params["client_secret"]} end end @@ -441,4 +446,16 @@ defp get_session_registration_id(conn), do: get_session(conn, :registration_id) 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 end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 399140003..4e5d1d118 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.Token do use Ecto.Schema import Ecto.Query + import Ecto.Changeset alias Pleroma.Repo alias Pleroma.User @@ -13,6 +14,9 @@ defmodule Pleroma.Web.OAuth.Token do alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Token + @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) + @type t :: %__MODULE__{} + schema "oauth_tokens" do field(:token, :string) field(:refresh_token, :string) @@ -24,28 +28,67 @@ defmodule Pleroma.Web.OAuth.Token do timestamps() end + @doc "Gets token for app by access token" + @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_token(%App{id: app_id} = _app, token) do + from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) + |> Repo.find_resource() + end + + @doc "Gets token for app by refresh token" + @spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_refresh_token(%App{id: app_id} = _app, token) do + from(t in __MODULE__, + where: t.app_id == ^app_id and t.refresh_token == ^token, + preload: [:user] + ) + |> Repo.find_resource() + end + def exchange_token(app, auth) do with {:ok, auth} <- Authorization.use_token(auth), true <- auth.app_id == app.id do - create_token(app, User.get_cached_by_id(auth.user_id), auth.scopes) + create_token( + app, + User.get_cached_by_id(auth.user_id), + %{scopes: auth.scopes} + ) end end - def create_token(%App{} = app, %User{} = user, scopes \\ nil) do - scopes = scopes || app.scopes - token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + defp put_token(changeset) do + changeset + |> change(%{token: Token.Utils.generate_token()}) + |> validate_required([:token]) + |> unique_constraint(:token) + end - token = %Token{ - token: token, - refresh_token: refresh_token, - scopes: scopes, - user_id: user.id, - app_id: app.id, - valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10) - } + defp put_refresh_token(changeset, attrs) do + refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token()) - Repo.insert(token) + changeset + |> change(%{refresh_token: refresh_token}) + |> validate_required([:refresh_token]) + |> unique_constraint(:refresh_token) + end + + defp put_valid_until(changeset, attrs) do + expires_in = + Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), @expires_in)) + + changeset + |> change(%{valid_until: expires_in}) + |> validate_required([:valid_until]) + end + + 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]) + |> put_valid_until(attrs) + |> put_token + |> put_refresh_token(attrs) + |> Repo.insert() end def delete_user_tokens(%User{id: user_id}) do @@ -73,4 +116,10 @@ def get_user_tokens(%User{id: user_id}) do |> Repo.all() |> Repo.preload(:app) end + + def is_expired?(%__MODULE__{valid_until: valid_until}) do + NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 + end + + def is_expired?(_), do: false end diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex new file mode 100644 index 000000000..7df0be14e --- /dev/null +++ b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex @@ -0,0 +1,54 @@ +defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do + @moduledoc """ + Functions for dealing with refresh token strategy. + """ + + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Strategy.Revoke + + @doc """ + Will grant access token by refresh token. + """ + @spec grant(Token.t()) :: {:ok, Token.t()} | {:error, any()} + def grant(token) do + access_token = Repo.preload(token, [:user, :app]) + + result = + Repo.transaction(fn -> + token_params = %{ + app: access_token.app, + user: access_token.user, + scopes: access_token.scopes + } + + access_token + |> revoke_access_token() + |> create_access_token(token_params) + end) + + case result do + {:ok, {:error, reason}} -> {:error, reason} + {:ok, {:ok, token}} -> {:ok, token} + {:error, reason} -> {:error, reason} + end + end + + defp revoke_access_token(token) do + Revoke.revoke(token) + end + + defp create_access_token({:error, error}, _), do: {:error, error} + + defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do + Token.create_token(app, user, add_refresh_token(token_params, token.refresh_token)) + end + + defp add_refresh_token(params, token) do + case Config.get([:oauth2, :issue_new_refresh_token], false) do + true -> Map.put(params, :refresh_token, token) + false -> params + end + end +end diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/oauth/token/strategy/revoke.ex new file mode 100644 index 000000000..dea63ca54 --- /dev/null +++ b/lib/pleroma/web/oauth/token/strategy/revoke.ex @@ -0,0 +1,22 @@ +defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do + @moduledoc """ + Functions for dealing with revocation. + """ + + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token + + @doc "Finds and revokes access token for app and by token" + @spec revoke(App.t(), map()) :: {:ok, Token.t()} | {:error, :not_found | Ecto.Changeset.t()} + def revoke(%App{} = app, %{"token" => token} = _attrs) do + with {:ok, token} <- Token.get_by_token(app, token), + do: revoke(token) + end + + @doc "Revokes access token" + @spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} + def revoke(%Token{} = token) do + Repo.delete(token) + end +end diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex new file mode 100644 index 000000000..a81560a1c --- /dev/null +++ b/lib/pleroma/web/oauth/token/utils.ex @@ -0,0 +1,30 @@ +defmodule Pleroma.Web.OAuth.Token.Utils do + @moduledoc """ + Auxiliary functions for dealing with tokens. + """ + + @doc "convert token inserted_at to unix timestamp" + def format_created_at(%{inserted_at: inserted_at} = _token) do + inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + end + + @doc false + @spec generate_token(keyword()) :: binary() + def generate_token(opts \\ []) do + opts + |> Keyword.get(:size, 32) + |> :crypto.strong_rand_bytes() + |> Base.url_encode64(padding: false) + end + + # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be + # decoding it. Investigate sometime. + def fix_padding(token) do + token + |> URI.decode() + |> Base.url_decode64!(padding: false) + |> Base.url_encode64(padding: false) + end +end diff --git a/priv/repo/migrations/20190501133552_add_refresh_token_index_to_token.exs b/priv/repo/migrations/20190501133552_add_refresh_token_index_to_token.exs new file mode 100644 index 000000000..449f2a3d4 --- /dev/null +++ b/priv/repo/migrations/20190501133552_add_refresh_token_index_to_token.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddRefreshTokenIndexToToken do + use Ecto.Migration + + def change do + create(unique_index(:oauth_tokens, [:refresh_token])) + end +end diff --git a/test/repo_test.exs b/test/repo_test.exs new file mode 100644 index 000000000..5382289c7 --- /dev/null +++ b/test/repo_test.exs @@ -0,0 +1,44 @@ +defmodule Pleroma.RepoTest do + use Pleroma.DataCase + import Pleroma.Factory + + describe "find_resource/1" do + test "returns user" do + user = insert(:user) + query = from(t in Pleroma.User, where: t.id == ^user.id) + assert Repo.find_resource(query) == {:ok, user} + end + + test "returns not_found" do + query = from(t in Pleroma.User, where: t.id == ^"9gBuXNpD2NyDmmxxdw") + assert Repo.find_resource(query) == {:error, :not_found} + end + end + + describe "get_assoc/2" do + test "get assoc from preloaded data" do + user = %Pleroma.User{name: "Agent Smith"} + token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} + assert Repo.get_assoc(token, :user) == {:ok, user} + end + + test "get one-to-one assoc from repo" do + user = insert(:user, name: "Jimi Hendrix") + token = refresh_record(insert(:oauth_token, user: user)) + + assert Repo.get_assoc(token, :user) == {:ok, user} + end + + test "get one-to-many assoc from repo" do + user = insert(:user) + notification = refresh_record(insert(:notification, user: user)) + + assert Repo.get_assoc(user, :notifications) == {:ok, [notification]} + end + + test "return error if has not assoc " do + token = insert(:oauth_token, user: nil) + assert Repo.get_assoc(token, :user) == {:error, :not_found} + end + end +end diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index 6e96537ec..cb6836983 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -12,6 +12,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Token + @oauth_config_path [:oauth2, :issue_new_refresh_token] @session_opts [ store: :cookie, key: "_test", @@ -714,4 +715,199 @@ test "rejects an invalid authorization code" do refute Map.has_key?(resp, "access_token") end end + + describe "POST /oauth/token - refresh token" do + setup do + oauth_token_config = Pleroma.Config.get(@oauth_config_path) + + on_exit(fn -> + Pleroma.Config.get(@oauth_config_path, oauth_token_config) + end) + end + + test "issues a new access token with keep fresh token" do + Pleroma.Config.put(@oauth_config_path, true) + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + new_token = Repo.get_by(Token, token: response["access_token"]) + assert new_token.refresh_token == token.refresh_token + assert new_token.scopes == auth.scopes + assert new_token.user_id == user.id + assert new_token.app_id == app.id + end + + test "issues a new access token with new fresh token" do + Pleroma.Config.put(@oauth_config_path, false) + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + new_token = Repo.get_by(Token, token: response["access_token"]) + refute new_token.refresh_token == token.refresh_token + assert new_token.scopes == auth.scopes + assert new_token.user_id == user.id + assert new_token.app_id == app.id + end + + test "returns 400 if we try use access token" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert %{"error" => "Invalid credentials"} == response + end + + test "returns 400 if refresh_token invalid" do + app = insert(:oauth_app, scopes: ["read", "write"]) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => "token.refresh_token", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert %{"error" => "Invalid credentials"} == response + end + + test "issues a new token if token expired" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + change = + Ecto.Changeset.change( + token, + %{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)} + ) + + {:ok, access_token} = Repo.update(change) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => access_token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + token = Repo.get_by(Token, token: response["access_token"]) + assert token + assert token.scopes == auth.scopes + assert token.user_id == user.id + assert token.app_id == app.id + end + end + + describe "POST /oauth/token - bad request" do + test "returns 500" do + response = + build_conn() + |> post("/oauth/token", %{}) + |> json_response(500) + + assert %{"error" => "Bad request"} == response + end + end + + describe "POST /oauth/revoke - bad request" do + test "returns 500" do + response = + build_conn() + |> post("/oauth/revoke", %{}) + |> json_response(500) + + assert %{"error" => "Bad request"} == response + end + end end