- 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
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"
# 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
- `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.
* `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).
]
```
+## 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"]]`
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
use Ecto.Schema
import Ecto.Changeset
+ @type t :: %__MODULE__{}
schema "apps" do
field(:client_name, :string)
field(:redirect_uris, :string)
import Ecto.Changeset
import Ecto.Query
+ @type t :: %__MODULE__{}
schema "oauth_authorizations" do
field(:token, :string)
field(:scopes, {:array, :string}, default: [])
)
|> 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
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)
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)
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/
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 ->
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 =
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
_ ->
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
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
use Ecto.Schema
import Ecto.Query
+ import Ecto.Changeset
alias Pleroma.Repo
alias Pleroma.User
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)
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
|> 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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+defmodule Pleroma.Repo.Migrations.AddRefreshTokenIndexToToken do
+ use Ecto.Migration
+
+ def change do
+ create(unique_index(:oauth_tokens, [:refresh_token]))
+ end
+end
--- /dev/null
+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
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
+ @oauth_config_path [:oauth2, :issue_new_refresh_token]
@session_opts [
store: :cookie,
key: "_test",
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