oauth_controller.ex 6.63 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 4
# SPDX-License-Identifier: AGPL-3.0-only

5 6 7
defmodule Pleroma.Web.OAuth.OAuthController do
  use Pleroma.Web, :controller

lain's avatar
lain committed
8 9
  alias Pleroma.Web.OAuth.{Authorization, Token, App}
  alias Pleroma.{Repo, User}
10 11
  alias Comeonin.Pbkdf2

12 13
  import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]

lain's avatar
lain committed
14 15
  plug(:fetch_session)
  plug(:fetch_flash)
16

lain's avatar
lain committed
17
  action_fallback(Pleroma.Web.OAuth.FallbackController)
18

19
  def authorize(conn, params) do
20 21 22 23 24 25 26 27 28 29
    params_scopes = oauth_scopes(params, nil)

    scopes =
      if params_scopes do
        params_scopes
      else
        app = Repo.get_by(App, client_id: params["client_id"])
        app && app.scopes
      end

lain's avatar
lain committed
30
    render(conn, "show.html", %{
31 32
      response_type: params["response_type"],
      client_id: params["client_id"],
33
      scopes: scopes || [],
lain's avatar
lain committed
34 35
      redirect_uri: params["redirect_uri"],
      state: params["state"]
lain's avatar
lain committed
36
    })
37 38
  end

lain's avatar
lain committed
39 40 41 42 43 44 45
  def create_authorization(conn, %{
        "authorization" =>
          %{
            "name" => name,
            "password" => password,
            "client_id" => client_id,
            "redirect_uri" => redirect_uri
46
          } = auth_params
lain's avatar
lain committed
47
      }) do
48
    with %User{} = user <- User.get_by_nickname_or_email(name),
49
         true <- Pbkdf2.checkpw(password, user.password_hash),
50
         {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
lain's avatar
lain committed
51
         %App{} = app <- Repo.get_by(App, client_id: client_id),
52
         true <- redirect_uri in String.split(app.redirect_uris),
53
         scopes <- oauth_scopes(auth_params, []),
54 55 56
         [] <- scopes -- app.scopes,
         true <- Enum.any?(scopes),
         {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
lain's avatar
lain committed
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
      # Special case: Local MastodonFE.
      redirect_uri =
        if redirect_uri == "." do
          mastodon_api_url(conn, :login)
        else
          redirect_uri
        end

      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 =
77 78
            if auth_params["state"] do
              Map.put(url_params, :state, auth_params["state"])
lain's avatar
lain committed
79 80 81 82 83 84 85
            else
              url_params
            end

          url = "#{url}#{Plug.Conn.Query.encode(url_params)}"

          redirect(conn, external: url)
lain's avatar
lain committed
86
      end
87
    else
88 89 90 91 92
      res ->
        msg =
          if res == {:auth_active, false},
            do: "Account confirmation pending",
            else: "Invalid Username/Password/Permissions"
93

94 95 96 97 98 99 100 101
        app = Repo.get_by(App, client_id: client_id)
        available_scopes = (app && app.scopes) || oauth_scopes(auth_params, [])
        scope_param = Enum.join(available_scopes, " ")

        conn
        |> put_flash(:error, msg)
        |> put_status(:unauthorized)
        |> authorize(Map.merge(auth_params, %{"scope" => scope_param}))
102 103 104 105
    end
  end

  def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
106
    with %App{} = app <- get_app_from_request(conn, params),
eal's avatar
eal committed
107
         fixed_token = fix_padding(params["code"]),
lain's avatar
lain committed
108 109
         %Authorization{} = auth <-
           Repo.get_by(Authorization, token: fixed_token, app_id: app.id),
110 111
         {:ok, token} <- Token.exchange_token(app, auth),
         {:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do
112 113 114 115
      response = %{
        token_type: "Bearer",
        access_token: token.token,
        refresh_token: token.refresh_token,
116
        created_at: DateTime.to_unix(inserted_at),
117
        expires_in: 60 * 10,
118
        scope: Enum.join(token.scopes)
119
      }
lain's avatar
lain committed
120

121
      json(conn, response)
eal's avatar
eal committed
122
    else
123 124 125
      _error ->
        put_status(conn, 400)
        |> json(%{error: "Invalid credentials"})
126 127
    end
  end
eal's avatar
eal committed
128

lain's avatar
lain committed
129 130
  def token_exchange(
        conn,
131
        %{"grant_type" => "password", "username" => name, "password" => password} = params
lain's avatar
lain committed
132
      ) do
133
    with %App{} = app <- get_app_from_request(conn, params),
134
         %User{} = user <- User.get_by_nickname_or_email(name),
135
         true <- Pbkdf2.checkpw(password, user.password_hash),
136
         {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
137
         scopes <- oauth_scopes(params, app.scopes),
138 139
         [] <- scopes -- app.scopes,
         true <- Enum.any?(scopes),
140
         {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
141 142 143 144 145 146
         {: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,
147
        scope: Enum.join(token.scopes, " ")
148
      }
lain's avatar
lain committed
149

150 151
      json(conn, response)
    else
152 153 154 155 156
      {:auth_active, false} ->
        conn
        |> put_status(:forbidden)
        |> json(%{error: "Account confirmation pending"})

157 158 159
      _error ->
        put_status(conn, 400)
        |> json(%{error: "Invalid credentials"})
160 161 162
    end
  end

163 164
  def token_exchange(
        conn,
Maksim's avatar
Maksim committed
165
        %{"grant_type" => "password", "name" => name, "password" => _password} = params
166 167 168 169 170 171 172 173 174
      ) do
    params =
      params
      |> Map.delete("name")
      |> Map.put("username", name)

    token_exchange(conn, params)
  end

175 176 177 178 179 180 181 182 183 184 185 186
  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

kaniini's avatar
kaniini committed
187 188
  # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
  # decoding it.  Investigate sometime.
eal's avatar
eal committed
189 190
  defp fix_padding(token) do
    token
kaniini's avatar
kaniini committed
191
    |> URI.decode()
eal's avatar
eal committed
192
    |> Base.url_decode64!(padding: false)
lain's avatar
lain committed
193
    |> Base.url_encode64()
eal's avatar
eal committed
194
  end
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218

  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
219
end