diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/pleroma/web/api_spec/operations/o_auth_operation.ex | 250 | ||||
-rw-r--r-- | lib/pleroma/web/auth/authenticator.ex | 2 | ||||
-rw-r--r-- | lib/pleroma/web/o_auth/o_auth_controller.ex | 37 | ||||
-rw-r--r-- | lib/pleroma/web/o_auth/token/utils.ex | 2 | ||||
-rw-r--r-- | lib/pleroma/web/router.ex | 8 |
5 files changed, 281 insertions, 18 deletions
diff --git a/lib/pleroma/web/api_spec/operations/o_auth_operation.ex b/lib/pleroma/web/api_spec/operations/o_auth_operation.ex new file mode 100644 index 000000000..d507fddd5 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/o_auth_operation.ex @@ -0,0 +1,250 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.OAuthOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + defp client_id_parameter(opts) do + Operation.parameter( + :client_id, + :query, + :string, + "Client ID, obtained during app registration", + opts + ) + end + + defp client_secret_parameter(opts) do + Operation.parameter( + :client_secret, + :query, + :string, + "Client secret, obtained during app registration", + opts + ) + end + + defp redirect_uri_parameter(opts) do + Operation.parameter( + :redirect_uri, + :query, + :string, + "Set a URI to redirect the user to. If this parameter is set to `urn:ietf:wg:oauth:2.0:oob` then the token will be shown instead. Must match one of the redirect URIs declared during app registration.", + opts + ) + end + + defp scope_parameter(opts) do + Operation.parameter( + :scope, + :query, + :string, + "List of requested OAuth scopes, separated by spaces. Must be a subset of scopes declared during app registration. If not provided, defaults to `read`.", + opts + ) + end + + def token_exchange_operation do + %Operation{ + tags: ["OAuth"], + summary: "Access Token Request", + operationId: "OAuthController.token_exchange", + parameters: [ + # code is required when grant_type == "authorization_code" + # Mastodon requires `redirect_uri`, we don't + client_id_parameter(required: true), + client_secret_parameter(required: true), + redirect_uri_parameter([]), + scope_parameter([]), + Operation.parameter( + :code, + :query, + :string, + "A user authorization code, obtained via /oauth/authorize" + ), + Operation.parameter( + :grant_type, + :query, + :string, + "Set equal to `authorization_code` if `code` is provided in order to gain user-level access. Set equal to `password` if `username` and `password` are provided. Otherwise, set equal to `client_credentials` to obtain app-level access only.", + required: true + ), + Operation.parameter(:username, :query, :string, "User's username, used with `grant_type=password`"), + Operation.parameter(:password, :query, :string, "User's password, used with `grant_type=password`") + ], + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError), + 403 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def token_revoke_operation do + %Operation{ + tags: ["OAuth"], + summary: "Revokes token", + operationId: "OAuthController.token_revoke", + parameters: [], + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def registration_details_operation do + %Operation{ + tags: ["OAuth"], + summary: "Register", + operationId: "OAuthController.registration_details", + parameters: [], + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def authorize_operation do + %Operation{ + tags: ["OAuth"], + summary: "OAuth callback", + operationId: "OAuthController.authorize", + parameters: [ + client_id_parameter(required: true), + client_secret_parameter([]), + Operation.parameter( + :response_type, + :query, + :string, + "Note: `code` is the only value supported (MastodonAPI and OAuth 2.1)", + required: true + ), + redirect_uri_parameter([]), + scope_parameter([]), + Operation.parameter( + :state, + :query, + :string, + "An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client." + ) + ], + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def create_authorization_operation do + %Operation{ + tags: ["OAuth"], + summary: "Create Authorization", + operationId: "OAuthController.create_authorization", + parameters: [], + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def prepare_request_operation do + %Operation{ + tags: ["OAuth"], + summary: "Prepare OAuth request for third-party auth providers", + operationId: "OAuthController.prepare_request", + parameters: [], + responses: %{ + 302 => + Operation.response("Success", "text/html", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + # The following operations should be moved to another controller, they aren't meant to be into OpenAPI + + def request_operation do + %Operation{ + tags: ["OAuth"], + summary: "", + operationId: "OAuthController.request", + parameters: [], + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def register_operation do + %Operation{ + tags: ["OAuth"], + summary: "", + operationId: "OAuthController.register", + parameters: [], + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def callback_operation do + %Operation{ + tags: ["OAuth"], + summary: "", + operationId: "OAuthController.callback", + parameters: [], + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end +end diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index 84741ee11..496c3973c 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -60,7 +60,7 @@ defmodule Pleroma.Web.Auth.Authenticator do %{"authorization" => %{"name" => name, "password" => password}} -> {:ok, {name, password}} - %{"grant_type" => "password", "username" => name, "password" => password} -> + %{grant_type: "password", username: name, password: password} -> {:ok, {name, password}} _ -> diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex index 215d97b3a..741f57195 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -29,6 +29,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) + plug( + Pleroma.Web.ApiSpec.CastAndValidate + when action not in [:prepare_request, :callback, :request, :register] + ) + plug(:fetch_session) plug(:fetch_flash) @@ -43,14 +48,16 @@ defmodule Pleroma.Web.OAuth.OAuthController do @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.OAuthOperation + # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg - def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do - {auth_attrs, params} = Map.pop(params, "authorization") + def authorize(%Plug.Conn{} = conn, %{authorization: _} = params) do + {auth_attrs, params} = Map.pop(params, :authorization) authorize(conn, Map.merge(params, auth_attrs)) end - def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do - if ControllerHelper.truthy_param?(params["force_login"]) do + def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{force_login: _} = params) do + if ControllerHelper.truthy_param?(params[:force_login]) do do_authorize(conn, params) else handle_existing_authorization(conn, params) @@ -63,7 +70,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do # So we have to check client and token. def authorize( %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, - %{"client_id" => client_id} = params + %{client_id: client_id} = params ) do with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app), ^client_id <- t.app.client_id do @@ -147,7 +154,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do create_authorization(conn, params, user: user) end - def create_authorization(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do + def create_authorization(%Plug.Conn{} = conn, %{authorization: _} = params, opts) do with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do after_create_authorization(conn, auth, params) @@ -255,7 +262,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do @doc "Renew access_token with refresh_token" def token_exchange( %Plug.Conn{} = conn, - %{"grant_type" => "refresh_token", "refresh_token" => token} = _params + %{grant_type: "refresh_token", refresh_token: token} = _params ) do with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), @@ -266,9 +273,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do end end - def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do + def token_exchange(%Plug.Conn{} = conn, %{grant_type: "authorization_code"} = params) do with {:ok, app} <- Token.Utils.fetch_app(conn), - fixed_token = Token.Utils.fix_padding(params["code"]), + 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 @@ -281,7 +288,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do def token_exchange( %Plug.Conn{} = conn, - %{"grant_type" => "password"} = params + %{grant_type: "password"} = params ) do with {:ok, %User{} = user} <- Authenticator.get_user(conn), {:ok, app} <- Token.Utils.fetch_app(conn), @@ -296,7 +303,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do def token_exchange( %Plug.Conn{} = conn, - %{"grant_type" => "password", "name" => name, "password" => _password} = params + %{grant_type: "password", name: name, password: _password} = params ) do params = params @@ -306,7 +313,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do token_exchange(conn, params) end - def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do + def token_exchange(%Plug.Conn{} = 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 @@ -379,7 +386,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do render_invalid_credentials_error(conn) end - def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do + def token_revoke(%Plug.Conn{} = conn, %{token: token}) do with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token), {:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do conn = @@ -477,7 +484,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do conn |> put_session_registration_id(registration.id) - |> registration_details(%{"authorization" => registration_params}) + |> registration_details(%{authorization: registration_params}) end else error -> @@ -493,7 +500,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do Map.merge(params, Jason.decode!(state)) end - def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do + def registration_details(%Plug.Conn{} = conn, %{authorization: auth_attrs}) do render(conn, "register.html", %{ client_id: auth_attrs["client_id"], redirect_uri: auth_attrs["redirect_uri"], diff --git a/lib/pleroma/web/o_auth/token/utils.ex b/lib/pleroma/web/o_auth/token/utils.ex index b572dc9cf..11ee34c72 100644 --- a/lib/pleroma/web/o_auth/token/utils.ex +++ b/lib/pleroma/web/o_auth/token/utils.ex @@ -41,7 +41,7 @@ defmodule Pleroma.Web.OAuth.Token.Utils do ) do {id, secret} else - _ -> {conn.params["client_id"], conn.params["client_secret"]} + _ -> {conn.params[:client_id], conn.params[:client_secret]} end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 8ba0fc702..34df3f365 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -30,11 +30,17 @@ defmodule Pleroma.Web.Router do plug(:fetch_session) end + pipeline :fetch_session_api do + plug(:fetch_session) + plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec) + end + pipeline :oauth do plug(:fetch_session) plug(Pleroma.Web.Plugs.OAuthPlug) plug(Pleroma.Web.Plugs.UserEnabledPlug) plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug) + plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec) end # Note: expects _user_ authentication (user-unbound app-bound tokens don't qualify) @@ -344,7 +350,7 @@ defmodule Pleroma.Web.Router do end scope [] do - pipe_through(:fetch_session) + pipe_through(:fetch_session_api) post("/token", OAuthController, :token_exchange) post("/revoke", OAuthController, :token_revoke) |