akkoma/lib/pleroma/web/oauth/oauth_controller.ex

272 lines
8.3 KiB
Elixir
Raw Normal View History

# Pleroma: A lightweight social networking server
2018-12-31 15:41:47 +00:00
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Comeonin.Pbkdf2
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Auth.Authenticator
alias Pleroma.Web.OAuth.App
2019-02-09 15:16:26 +00:00
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
if Pleroma.Config.get([:auth, :oauth_consumer_enabled]), do: plug(Ueberauth)
2018-03-30 13:01:53 +00:00
plug(:fetch_session)
plug(:fetch_flash)
2018-03-30 13:01:53 +00:00
action_fallback(Pleroma.Web.OAuth.FallbackController)
def request(conn, params) do
message =
if params["provider"] do
"Unsupported OAuth provider: #{params["provider"]}."
else
"Bad OAuth request."
end
2019-03-11 17:37:26 +00:00
conn
|> put_flash(:error, message)
2019-03-11 17:37:26 +00:00
|> redirect(to: "/")
end
def callback(%{assigns: %{ueberauth_failure: failure}} = conn, %{"redirect_uri" => redirect_uri}) do
messages = for e <- Map.get(failure, :errors, []), do: e.message
message = Enum.join(messages, "; ")
conn
|> put_flash(:error, "Failed to authenticate: #{message}.")
|> redirect(external: redirect_uri(conn, redirect_uri))
end
def callback(
conn,
%{"client_id" => client_id, "redirect_uri" => redirect_uri} = params
) do
with {:ok, user} <- Authenticator.get_or_create_user_by_oauth(conn, params) do
do_create_authorization(
conn,
%{
"authorization" => %{
"client_id" => client_id,
"redirect_uri" => redirect_uri,
"scope" => oauth_scopes(params, nil)
}
},
user
)
else
_ ->
conn
|> put_flash(:error, "Failed to set up user account.")
|> redirect(external: redirect_uri(conn, redirect_uri))
end
2019-03-11 17:37:26 +00:00
end
def authorize(conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
available_scopes = (app && app.scopes) || []
scopes = oauth_scopes(params, nil) || available_scopes
render(conn, Authenticator.auth_template(), %{
response_type: params["response_type"],
client_id: params["client_id"],
available_scopes: available_scopes,
scopes: scopes,
2017-09-14 07:29:51 +00:00
redirect_uri: params["redirect_uri"],
state: params["state"],
params: params
2018-03-30 13:01:53 +00:00
})
end
def create_authorization(conn, params), do: do_create_authorization(conn, params, nil)
defp do_create_authorization(
conn,
%{
"authorization" =>
%{
"client_id" => client_id,
"redirect_uri" => redirect_uri
} = auth_params
} = params,
user
) do
with {_, {:ok, %User{} = user}} <-
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn, params)},
2017-09-07 06:58:10 +00:00
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
scopes <- oauth_scopes(auth_params, []),
{:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes},
# Note: `scope` param is intentionally not optional in this context
{:missing_scopes, false} <- {:missing_scopes, scopes == []},
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
redirect_uri = redirect_uri(conn, redirect_uri)
cond do
redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
render(conn, "results.html", %{
auth: auth
})
true ->
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
url = "#{redirect_uri}#{connector}"
url_params = %{:code => auth.token}
url_params =
if auth_params["state"] do
Map.put(url_params, :state, auth_params["state"])
else
url_params
end
url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
redirect(conn, external: url)
2017-09-09 17:03:57 +00:00
end
else
{scopes_issue, _} when scopes_issue in [:unsupported_scopes, :missing_scopes] ->
conn
|> put_flash(:error, "Permissions not specified.")
|> put_status(:unauthorized)
|> authorize(auth_params)
{:auth_active, false} ->
conn
|> put_flash(:error, "Account confirmation pending.")
|> put_status(:forbidden)
|> authorize(auth_params)
error ->
Authenticator.handle_error(conn, error)
end
end
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
with %App{} = app <- get_app_from_request(conn, params),
2017-11-06 19:51:31 +00:00
fixed_token = fix_padding(params["code"]),
2018-04-21 07:43:53 +00:00
%Authorization{} = auth <-
Repo.get_by(Authorization, token: fixed_token, app_id: app.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, " ")
}
2018-03-30 13:01:53 +00:00
json(conn, response)
2017-11-06 19:51:31 +00:00
else
_error ->
put_status(conn, 400)
|> json(%{error: "Invalid credentials"})
end
end
2017-11-06 19:51:31 +00:00
2018-03-30 13:01:53 +00:00
def token_exchange(
conn,
%{"grant_type" => "password", "username" => name, "password" => password} = params
2018-03-30 13:01:53 +00:00
) do
with %App{} = app <- get_app_from_request(conn, params),
%User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
scopes <- oauth_scopes(params, app.scopes),
[] <- scopes -- app.scopes,
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, " ")
}
2018-03-30 13:01:53 +00:00
json(conn, response)
else
{:auth_active, false} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Account confirmation pending"})
_error ->
put_status(conn, 400)
|> json(%{error: "Invalid credentials"})
end
end
def token_exchange(
conn,
2018-12-09 09:12:48 +00:00
%{"grant_type" => "password", "name" => name, "password" => _password} = params
) do
params =
params
|> Map.delete("name")
|> Map.put("username", name)
token_exchange(conn, params)
end
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
json(conn, %{})
else
_error ->
# RFC 7009: invalid tokens [in the request] do not cause an error response
json(conn, %{})
end
end
2018-11-11 05:11:27 +00:00
# XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
# decoding it. Investigate sometime.
2017-11-06 19:51:31 +00:00
defp fix_padding(token) do
token
2018-11-11 05:11:27 +00:00
|> URI.decode()
2017-11-06 19:51:31 +00:00
|> Base.url_decode64!(padding: false)
|> Base.url_encode64(padding: false)
2017-11-06 19:51:31 +00:00
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
if client_id && client_secret do
Repo.get_by(
App,
client_id: client_id,
client_secret: client_secret
)
else
nil
end
end
# Special case: Local MastodonFE
defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login)
defp redirect_uri(_conn, redirect_uri), do: redirect_uri
end