oauth_controller.ex 15.6 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

8
  alias Pleroma.Helpers.UriHelper
9
  alias Pleroma.Registration
10
11
  alias Pleroma.Repo
  alias Pleroma.User
12
  alias Pleroma.Web.Auth.Authenticator
13
  alias Pleroma.Web.ControllerHelper
14
  alias Pleroma.Web.OAuth.App
Haelwenn's avatar
Haelwenn committed
15
16
  alias Pleroma.Web.OAuth.Authorization
  alias Pleroma.Web.OAuth.Token
Maksim's avatar
Maksim committed
17
18
  alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
  alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
19
  alias Pleroma.Web.OAuth.Scopes
20

21
22
  require Logger

23
  if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
24

lain's avatar
lain committed
25
26
  plug(:fetch_session)
  plug(:fetch_flash)
27

lain's avatar
lain committed
28
  action_fallback(Pleroma.Web.OAuth.FallbackController)
29

30
31
  @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"

32
  # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
33
  def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
34
35
36
37
    {auth_attrs, params} = Map.pop(params, "authorization")
    authorize(conn, Map.merge(params, auth_attrs))
  end

38
  def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do
39
40
41
    if ControllerHelper.truthy_param?(params["force_login"]) do
      do_authorize(conn, params)
    else
42
      handle_existing_authorization(conn, params)
43
44
45
    end
  end

46
  def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
47

48
  defp do_authorize(%Plug.Conn{} = conn, params) do
49
    app = Repo.get_by(App, client_id: params["client_id"])
50
    available_scopes = (app && app.scopes) || []
51
    scopes = Scopes.fetch_scopes(params, available_scopes)
52

53
    # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
54
    render(conn, Authenticator.auth_template(), %{
55
56
      response_type: params["response_type"],
      client_id: params["client_id"],
57
58
      available_scopes: available_scopes,
      scopes: scopes,
59
60
61
      redirect_uri: params["redirect_uri"],
      state: params["state"],
      params: params
lain's avatar
lain committed
62
    })
63
64
  end

65
66
  defp handle_existing_authorization(
         %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
67
         %{"redirect_uri" => @oob_token_redirect_uri}
68
       ) do
69
70
71
72
73
74
75
76
    render(conn, "oob_token_exists.html", %{token: token})
  end

  defp handle_existing_authorization(
         %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
         %{} = params
       ) do
    app = Repo.preload(token, :app).app
77
78
79
80
81

    redirect_uri =
      if is_binary(params["redirect_uri"]) do
        params["redirect_uri"]
      else
82
        default_redirect_uri(app)
83
84
      end

85
86
    if redirect_uri in String.split(app.redirect_uris) do
      redirect_uri = redirect_uri(conn, redirect_uri)
87
88
89
90
      url_params = %{access_token: token.token}
      url_params = UriHelper.append_param_if_present(url_params, :state, params["state"])
      url = UriHelper.append_uri_params(redirect_uri, url_params)
      redirect(conn, external: url)
91
92
    else
      conn
93
      |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
94
      |> redirect(external: redirect_uri(conn, redirect_uri))
95
96
97
    end
  end

98
  def create_authorization(
99
        %Plug.Conn{} = conn,
100
        %{"authorization" => _} = params,
101
102
        opts \\ []
      ) do
103
    with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do
104
      after_create_authorization(conn, auth, params)
105
106
    else
      error ->
107
        handle_create_authorization_error(conn, error, params)
108
109
    end
  end
lain's avatar
lain committed
110

111
112
113
114
115
116
  def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
        "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
      }) do
    render(conn, "oob_authorization_created.html", %{auth: auth})
  end

117
  def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
118
119
        "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
      }) do
120
    app = Repo.preload(auth, :app).app
lain's avatar
lain committed
121

Ivan Tashkinov's avatar
Ivan Tashkinov committed
122
    # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
123
124
    if redirect_uri in String.split(app.redirect_uris) do
      redirect_uri = redirect_uri(conn, redirect_uri)
125
126
127
      url_params = %{code: auth.token}
      url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"])
      url = UriHelper.append_uri_params(redirect_uri, url_params)
128
      redirect(conn, external: url)
129
130
    else
      conn
131
      |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
132
      |> redirect(external: redirect_uri(conn, redirect_uri))
133
134
135
    end
  end

136
  defp handle_create_authorization_error(
137
         %Plug.Conn{} = conn,
138
         {:error, scopes_issue},
139
140
         %{"authorization" => _} = params
       )
141
142
143
144
       when scopes_issue in [:unsupported_scopes, :missing_scopes] do
    # Per https://github.com/tootsuite/mastodon/blob/
    #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
    conn
145
    |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
146
    |> put_status(:unauthorized)
147
    |> authorize(params)
148
149
  end

150
  defp handle_create_authorization_error(
151
         %Plug.Conn{} = conn,
152
153
154
         {:auth_active, false},
         %{"authorization" => _} = params
       ) do
155
156
157
    # Per https://github.com/tootsuite/mastodon/blob/
    #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
    conn
158
    |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
159
    |> put_status(:forbidden)
160
    |> authorize(params)
161
162
  end

163
  defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
164
165
166
    Authenticator.handle_error(conn, error)
  end

Maksim's avatar
Maksim committed
167
168
  @doc "Renew access_token with refresh_token"
  def token_exchange(
169
        %Plug.Conn{} = conn,
Maksim's avatar
Maksim committed
170
        %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
Maksim's avatar
Maksim committed
171
      ) do
Maksim's avatar
Maksim committed
172
    with {:ok, app} <- Token.Utils.fetch_app(conn),
Maksim's avatar
Maksim committed
173
174
175
176
         {: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)}

Maksim's avatar
Maksim committed
177
      json(conn, Token.Response.build(user, token, response_attrs))
Maksim's avatar
Maksim committed
178
    else
179
      _error -> render_invalid_credentials_error(conn)
Maksim's avatar
Maksim committed
180
181
182
    end
  end

183
  def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
Maksim's avatar
Maksim committed
184
    with {:ok, app} <- Token.Utils.fetch_app(conn),
Maksim's avatar
Maksim committed
185
186
         fixed_token = Token.Utils.fix_padding(params["code"]),
         {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
minibikini's avatar
minibikini committed
187
         %User{} = user <- User.get_cached_by_id(auth.user_id),
Maksim's avatar
Maksim committed
188
189
190
         {:ok, token} <- Token.exchange_token(app, auth) do
      response_attrs = %{created_at: Token.Utils.format_created_at(token)}

Maksim's avatar
Maksim committed
191
      json(conn, Token.Response.build(user, token, response_attrs))
eal's avatar
eal committed
192
    else
193
      _error -> render_invalid_credentials_error(conn)
194
195
    end
  end
eal's avatar
eal committed
196

lain's avatar
lain committed
197
  def token_exchange(
198
        %Plug.Conn{} = conn,
199
        %{"grant_type" => "password"} = params
lain's avatar
lain committed
200
      ) do
Maksim's avatar
Maksim committed
201
202
    with {:ok, %User{} = user} <- Authenticator.get_user(conn),
         {:ok, app} <- Token.Utils.fetch_app(conn),
203
         {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
204
         {:user_active, true} <- {:user_active, !user.info.deactivated},
205
         {:ok, scopes} <- validate_scopes(app, params),
206
         {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
207
         {:ok, token} <- Token.exchange_token(app, auth) do
Maksim's avatar
Maksim committed
208
      json(conn, Token.Response.build(user, token))
209
    else
210
      {:auth_active, false} ->
Ivan Tashkinov's avatar
Ivan Tashkinov committed
211
212
        # Per https://github.com/tootsuite/mastodon/blob/
        #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
213
        render_error(conn, :forbidden, "Your login is missing a confirmed e-mail address")
214

215
      {:user_active, false} ->
216
        render_error(conn, :forbidden, "Your account is currently disabled")
217

218
      _error ->
219
        render_invalid_credentials_error(conn)
220
221
222
    end
  end

223
  def token_exchange(
224
        %Plug.Conn{} = conn,
Maksim's avatar
Maksim committed
225
        %{"grant_type" => "password", "name" => name, "password" => _password} = params
226
227
228
229
230
231
232
233
234
      ) do
    params =
      params
      |> Map.delete("name")
      |> Map.put("username", name)

    token_exchange(conn, params)
  end

235
  def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
Maksim's avatar
Maksim committed
236
    with {:ok, app} <- Token.Utils.fetch_app(conn),
237
         {:ok, auth} <- Authorization.create_authorization(app, %User{}),
Maksim's avatar
Maksim committed
238
239
         {:ok, token} <- Token.exchange_token(app, auth) do
      json(conn, Token.Response.build_for_client_credentials(token))
240
    else
241
      _error -> render_invalid_credentials_error(conn)
242
243
244
    end
  end

Maksim's avatar
Maksim committed
245
  # Bad request
246
  def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
Maksim's avatar
Maksim committed
247

248
  def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
Maksim's avatar
Maksim committed
249
    with {:ok, app} <- Token.Utils.fetch_app(conn),
Maksim's avatar
Maksim committed
250
         {:ok, _token} <- RevokeToken.revoke(app, params) do
251
252
253
254
255
256
257
258
      json(conn, %{})
    else
      _error ->
        # RFC 7009: invalid tokens [in the request] do not cause an error response
        json(conn, %{})
    end
  end

259
  def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
Maksim's avatar
Maksim committed
260
261

  # Response for bad request
262
  defp bad_request(%Plug.Conn{} = conn, _) do
263
    render_error(conn, :internal_server_error, "Bad request")
Maksim's avatar
Maksim committed
264
265
  end

266
  @doc "Prepares OAuth request to provider for Ueberauth"
267
268
269
270
  def prepare_request(%Plug.Conn{} = conn, %{
        "provider" => provider,
        "authorization" => auth_attrs
      }) do
271
    scope =
272
273
274
      auth_attrs
      |> Scopes.fetch_scopes([])
      |> Scopes.to_string()
275
276

    state =
277
      auth_attrs
278
279
      |> Map.delete("scopes")
      |> Map.put("scope", scope)
feld's avatar
feld committed
280
      |> Jason.encode!()
281
282

    params =
283
      auth_attrs
284
285
286
      |> Map.drop(~w(scope scopes client_id redirect_uri))
      |> Map.put("state", state)

287
    # Handing the request to Ueberauth
288
289
290
    redirect(conn, to: o_auth_path(conn, :request, provider, params))
  end

291
  def request(%Plug.Conn{} = conn, params) do
292
293
    message =
      if params["provider"] do
294
295
296
        dgettext("errors", "Unsupported OAuth provider: %{provider}.",
          provider: params["provider"]
        )
297
      else
298
        dgettext("errors", "Bad OAuth request.")
299
300
301
302
303
304
305
      end

    conn
    |> put_flash(:error, message)
    |> redirect(to: "/")
  end

306
  def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
307
    params = callback_params(params)
308
309
310
311
    messages = for e <- Map.get(failure, :errors, []), do: e.message
    message = Enum.join(messages, "; ")

    conn
312
313
314
315
    |> put_flash(
      :error,
      dgettext("errors", "Failed to authenticate: %{message}.", message: message)
    )
316
    |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
317
318
  end

319
  def callback(%Plug.Conn{} = conn, params) do
320
321
    params = callback_params(params)

322
323
    with {:ok, registration} <- Authenticator.get_registration(conn) do
      auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
324

Maksim's avatar
Maksim committed
325
326
327
      case Repo.get_assoc(registration, :user) do
        {:ok, user} ->
          create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
328

Maksim's avatar
Maksim committed
329
330
331
332
333
334
335
336
        _ ->
          registration_params =
            Map.merge(auth_attrs, %{
              "nickname" => Registration.nickname(registration),
              "email" => Registration.email(registration)
            })

          conn
337
          |> put_session_registration_id(registration.id)
Maksim's avatar
Maksim committed
338
          |> registration_details(%{"authorization" => registration_params})
339
340
      end
    else
341
342
343
      error ->
        Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))

344
        conn
345
        |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
346
        |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
347
348
349
    end
  end

350
  defp callback_params(%{"state" => state} = params) do
feld's avatar
feld committed
351
    Map.merge(params, Jason.decode!(state))
352
353
  end

354
  def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
355
    render(conn, "register.html", %{
356
357
358
      client_id: auth_attrs["client_id"],
      redirect_uri: auth_attrs["redirect_uri"],
      state: auth_attrs["state"],
359
      scopes: Scopes.fetch_scopes(auth_attrs, []),
360
361
      nickname: auth_attrs["nickname"],
      email: auth_attrs["email"]
362
363
364
    })
  end

365
  def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
366
367
    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
         %Registration{} = registration <- Repo.get(Registration, registration_id),
368
         {_, {:ok, auth}} <- {:create_authorization, do_create_authorization(conn, params)},
369
370
371
372
         %User{} = user <- Repo.preload(auth, :user).user,
         {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
      conn
      |> put_session_registration_id(nil)
373
      |> after_create_authorization(auth, params)
374
    else
375
      {:create_authorization, error} ->
376
        {:register, handle_create_authorization_error(conn, error, params)}
377

378
379
      _ ->
        {:register, :generic_error}
380
381
382
    end
  end

383
  def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
384
385
    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
         %Registration{} = registration <- Repo.get(Registration, registration_id),
386
         {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
387
388
389
      conn
      |> put_session_registration_id(nil)
      |> create_authorization(
390
        params,
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
        user: user
      )
    else
      {:error, changeset} ->
        message =
          Enum.map(changeset.errors, fn {field, {error, _}} ->
            "#{field} #{error}"
          end)
          |> Enum.join("; ")

        message =
          String.replace(
            message,
            "ap_id has already been taken",
            "nickname has already been taken"
          )

        conn
409
        |> put_status(:forbidden)
410
        |> put_flash(:error, "Error: #{message}.")
411
        |> registration_details(params)
412
413

      _ ->
414
        {:register, :generic_error}
415
416
417
418
    end
  end

  defp do_create_authorization(
419
         %Plug.Conn{} = conn,
420
421
422
423
424
         %{
           "authorization" =>
             %{
               "client_id" => client_id,
               "redirect_uri" => redirect_uri
425
426
             } = auth_attrs
         },
427
428
429
         user \\ nil
       ) do
    with {_, {:ok, %User{} = user}} <-
430
           {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
431
432
         %App{} = app <- Repo.get_by(App, client_id: client_id),
         true <- redirect_uri in String.split(app.redirect_uris),
433
         {:ok, scopes} <- validate_scopes(app, auth_attrs),
434
435
436
437
438
         {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do
      Authorization.create_authorization(app, user, scopes)
    end
  end

439
  # Special case: Local MastodonFE
440
  defp redirect_uri(%Plug.Conn{} = conn, "."), do: mastodon_api_url(conn, :login)
441

442
  defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
443

444
  defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
445

446
  defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
447
    do: put_session(conn, :registration_id, registration_id)
Maksim's avatar
Maksim committed
448

449
450
451
452
453
454
455
  @spec validate_scopes(App.t(), map()) ::
          {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
  defp validate_scopes(app, params) do
    params
    |> Scopes.fetch_scopes(app.scopes)
    |> Scopes.validates(app.scopes)
  end
456

457
  def default_redirect_uri(%App{} = app) do
458
459
460
461
    app.redirect_uris
    |> String.split()
    |> Enum.at(0)
  end
462
463
464
465

  defp render_invalid_credentials_error(conn) do
    render_error(conn, :bad_request, "Invalid credentials")
  end
466
end