@@ -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 | |||
@@ -473,6 +473,10 @@ config :pleroma, Pleroma.ScheduledActivity, | |||
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" |
@@ -1,6 +1,6 @@ | |||
# Differences in Mastodon API responses from vanilla Mastodon | |||
A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma <version>)" present in `version` field in response from `/api/v1/instance` | |||
A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma <version>)" 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. |
@@ -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"]]` | |||
@@ -19,4 +19,32 @@ defmodule Pleroma.Repo 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 |
@@ -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) | |||
@@ -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 @@ defmodule Pleroma.Web.OAuth.Authorization 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 |
@@ -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 @@ defmodule Pleroma.Web.OAuth.OAuthController 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 | |||
} | |||
json(conn, response) | |||
{: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)) | |||
else | |||
_error -> | |||
put_status(conn, 400) | |||
@@ -177,16 +189,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
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 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
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 @@ defmodule Pleroma.Web.OAuth.OAuthController 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 @@ defmodule Pleroma.Web.OAuth.OAuthController 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 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
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 @@ 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 | |||
end |
@@ -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) | |||
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) | |||
} | |||
Repo.insert(token) | |||
defp put_token(changeset) do | |||
changeset | |||
|> change(%{token: Token.Utils.generate_token()}) | |||
|> validate_required([:token]) | |||
|> unique_constraint(:token) | |||
end | |||
defp put_refresh_token(changeset, attrs) do | |||
refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_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 @@ defmodule Pleroma.Web.OAuth.Token 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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest 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 |