o_auth_controller.ex 19.8 KB
Newer Older
1
# Pleroma: A lightweight social networking server
2
# Copyright © 2017-2020 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.Maps
lain's avatar
lain committed
10
  alias Pleroma.MFA
11
  alias Pleroma.Registration
12
13
  alias Pleroma.Repo
  alias Pleroma.User
14
  alias Pleroma.Web.Auth.Authenticator
15
  alias Pleroma.Web.ControllerHelper
16
  alias Pleroma.Web.OAuth.App
Haelwenn's avatar
Haelwenn committed
17
  alias Pleroma.Web.OAuth.Authorization
18
  alias Pleroma.Web.OAuth.MFAController
19
  alias Pleroma.Web.OAuth.MFAView
lain's avatar
lain committed
20
  alias Pleroma.Web.OAuth.OAuthView
Haelwenn's avatar
Haelwenn committed
21
  alias Pleroma.Web.OAuth.Scopes
Haelwenn's avatar
Haelwenn committed
22
  alias Pleroma.Web.OAuth.Token
Maksim's avatar
Maksim committed
23
24
  alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
  alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
25
  alias Pleroma.Web.Plugs.RateLimiter
26

27
28
  require Logger

29
  if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
30

lain's avatar
lain committed
31
32
  plug(:fetch_session)
  plug(:fetch_flash)
33

34
35
  plug(:skip_plug, [
    Pleroma.Web.Plugs.OAuthScopesPlug,
36
    Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
37
  ])
38
39

  plug(RateLimiter, [name: :authentication] when action == :create_authorization)
40

lain's avatar
lain committed
41
  action_fallback(Pleroma.Web.OAuth.FallbackController)
42

43
44
  @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"

45
  # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
46
  def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
47
48
49
50
    {auth_attrs, params} = Map.pop(params, "authorization")
    authorize(conn, Map.merge(params, auth_attrs))
  end

51
  def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
52
53
54
    if ControllerHelper.truthy_param?(params["force_login"]) do
      do_authorize(conn, params)
    else
55
      handle_existing_authorization(conn, params)
56
57
58
    end
  end

59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
  # Note: the token is set in oauth_plug, but the token and client do not always go together.
  # For example, MastodonFE's token is set if user requests with another client,
  # after user already authorized to MastodonFE.
  # So we have to check client and token.
  def authorize(
        %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
        %{"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
      handle_existing_authorization(conn, params)
    else
      _ -> do_authorize(conn, params)
    end
  end

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

77
  defp do_authorize(%Plug.Conn{} = conn, params) do
78
    app = Repo.get_by(App, client_id: params["client_id"])
79
    available_scopes = (app && app.scopes) || []
80
    scopes = Scopes.fetch_scopes(params, available_scopes)
81

82
83
84
85
86
87
88
    scopes =
      if scopes == [] do
        available_scopes
      else
        scopes
      end

89
    # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
90
    render(conn, Authenticator.auth_template(), %{
91
92
      response_type: params["response_type"],
      client_id: params["client_id"],
93
94
      available_scopes: available_scopes,
      scopes: scopes,
95
96
97
      redirect_uri: params["redirect_uri"],
      state: params["state"],
      params: params
lain's avatar
lain committed
98
    })
99
100
  end

101
102
  defp handle_existing_authorization(
         %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
103
         %{"redirect_uri" => @oob_token_redirect_uri}
104
       ) do
105
106
107
108
109
110
111
112
    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
113
114
115
116
117

    redirect_uri =
      if is_binary(params["redirect_uri"]) do
        params["redirect_uri"]
      else
118
        default_redirect_uri(app)
119
120
      end

121
122
    if redirect_uri in String.split(app.redirect_uris) do
      redirect_uri = redirect_uri(conn, redirect_uri)
123
      url_params = %{access_token: token.token}
124
      url_params = Maps.put_if_present(url_params, :state, params["state"])
125
      url = UriHelper.modify_uri_params(redirect_uri, url_params)
126
      redirect(conn, external: url)
127
128
    else
      conn
129
      |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
130
      |> redirect(external: redirect_uri(conn, redirect_uri))
131
132
133
    end
  end

134
  def create_authorization(
135
        %Plug.Conn{} = conn,
136
        %{"authorization" => _} = params,
137
138
        opts \\ []
      ) do
139
140
    with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
         {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
141
      after_create_authorization(conn, auth, params)
142
143
    else
      error ->
144
        handle_create_authorization_error(conn, error, params)
145
146
    end
  end
lain's avatar
lain committed
147

148
149
150
  def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
        "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
      }) do
151
152
153
154
    # Enforcing the view to reuse the template when calling from other controllers
    conn
    |> put_view(OAuthView)
    |> render("oob_authorization_created.html", %{auth: auth})
155
156
  end

157
  def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
158
159
        "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
      }) do
160
    app = Repo.preload(auth, :app).app
lain's avatar
lain committed
161

Ivan Tashkinov's avatar
Ivan Tashkinov committed
162
    # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
163
164
    if redirect_uri in String.split(app.redirect_uris) do
      redirect_uri = redirect_uri(conn, redirect_uri)
165
      url_params = %{code: auth.token}
166
      url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
167
      url = UriHelper.modify_uri_params(redirect_uri, url_params)
168
      redirect(conn, external: url)
169
170
    else
      conn
171
      |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
172
      |> redirect(external: redirect_uri(conn, redirect_uri))
173
174
175
    end
  end

176
  defp handle_create_authorization_error(
177
         %Plug.Conn{} = conn,
178
         {:error, scopes_issue},
179
180
         %{"authorization" => _} = params
       )
181
182
183
184
       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
185
    |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
186
    |> put_status(:unauthorized)
187
    |> authorize(params)
188
189
  end

190
  defp handle_create_authorization_error(
191
         %Plug.Conn{} = conn,
192
         {:account_status, :confirmation_pending},
193
194
         %{"authorization" => _} = params
       ) do
195
    conn
196
    |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
197
    |> put_status(:forbidden)
198
    |> authorize(params)
199
200
  end

201
202
203
204
205
  defp handle_create_authorization_error(
         %Plug.Conn{} = conn,
         {:mfa_required, user, auth, _},
         params
       ) do
206
    {:ok, token} = MFA.Token.create(user, auth)
207
208
209
210
211
212
213
214
215
216

    data = %{
      "mfa_token" => token.token,
      "redirect_uri" => params["authorization"]["redirect_uri"],
      "state" => params["authorization"]["state"]
    }

    MFAController.show(conn, data)
  end

217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
  defp handle_create_authorization_error(
         %Plug.Conn{} = conn,
         {:account_status, :password_reset_pending},
         %{"authorization" => _} = params
       ) do
    conn
    |> put_flash(:error, dgettext("errors", "Password reset is required"))
    |> put_status(:forbidden)
    |> authorize(params)
  end

  defp handle_create_authorization_error(
         %Plug.Conn{} = conn,
         {:account_status, :deactivated},
         %{"authorization" => _} = params
       ) do
    conn
    |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
    |> put_status(:forbidden)
    |> authorize(params)
  end

239
  defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
240
241
242
    Authenticator.handle_error(conn, error)
  end

Maksim's avatar
Maksim committed
243
244
  @doc "Renew access_token with refresh_token"
  def token_exchange(
245
        %Plug.Conn{} = conn,
Maksim's avatar
Maksim committed
246
        %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
Maksim's avatar
Maksim committed
247
      ) do
Maksim's avatar
Maksim committed
248
    with {:ok, app} <- Token.Utils.fetch_app(conn),
Maksim's avatar
Maksim committed
249
250
         {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
         {:ok, token} <- RefreshToken.grant(token) do
251
      json(conn, OAuthView.render("token.json", %{user: user, token: token}))
Maksim's avatar
Maksim committed
252
    else
253
      _error -> render_invalid_credentials_error(conn)
Maksim's avatar
Maksim committed
254
255
256
    end
  end

257
  def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
Maksim's avatar
Maksim committed
258
    with {:ok, app} <- Token.Utils.fetch_app(conn),
Maksim's avatar
Maksim committed
259
260
         fixed_token = Token.Utils.fix_padding(params["code"]),
         {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
minibikini's avatar
minibikini committed
261
         %User{} = user <- User.get_cached_by_id(auth.user_id),
Maksim's avatar
Maksim committed
262
         {:ok, token} <- Token.exchange_token(app, auth) do
263
      json(conn, OAuthView.render("token.json", %{user: user, token: token}))
eal's avatar
eal committed
264
    else
265
266
      error ->
        handle_token_exchange_error(conn, error)
267
268
    end
  end
eal's avatar
eal committed
269

lain's avatar
lain committed
270
  def token_exchange(
271
        %Plug.Conn{} = conn,
272
        %{"grant_type" => "password"} = params
lain's avatar
lain committed
273
      ) do
Maksim's avatar
Maksim committed
274
275
    with {:ok, %User{} = user} <- Authenticator.get_user(conn),
         {:ok, app} <- Token.Utils.fetch_app(conn),
276
277
         requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
         {:ok, token} <- login(user, app, requested_scopes) do
278
      json(conn, OAuthView.render("token.json", %{user: user, token: token}))
279
    else
280
281
      error ->
        handle_token_exchange_error(conn, error)
282
283
284
    end
  end

285
  def token_exchange(
286
        %Plug.Conn{} = conn,
Maksim's avatar
Maksim committed
287
        %{"grant_type" => "password", "name" => name, "password" => _password} = params
288
289
290
291
292
293
294
295
296
      ) do
    params =
      params
      |> Map.delete("name")
      |> Map.put("username", name)

    token_exchange(conn, params)
  end

297
  def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
Maksim's avatar
Maksim committed
298
    with {:ok, app} <- Token.Utils.fetch_app(conn),
299
         {:ok, auth} <- Authorization.create_authorization(app, %User{}),
Maksim's avatar
Maksim committed
300
         {:ok, token} <- Token.exchange_token(app, auth) do
301
      json(conn, OAuthView.render("token.json", %{token: token}))
302
    else
303
304
      _error ->
        handle_token_exchange_error(conn, :invalid_credentails)
305
306
307
    end
  end

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

311
312
313
314
315
316
  defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
    conn
    |> put_status(:forbidden)
    |> json(build_and_response_mfa_token(user, auth))
  end

317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
  defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
    render_error(
      conn,
      :forbidden,
      "Your account is currently disabled",
      %{},
      "account_is_disabled"
    )
  end

  defp handle_token_exchange_error(
         %Plug.Conn{} = conn,
         {:account_status, :password_reset_pending}
       ) do
    render_error(
      conn,
      :forbidden,
      "Password reset is required",
      %{},
      "password_reset_required"
    )
  end

  defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
    render_error(
      conn,
      :forbidden,
      "Your login is missing a confirmed e-mail address",
      %{},
      "missing_confirmed_email"
    )
  end

350
351
352
353
354
355
356
357
358
359
  defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
    render_error(
      conn,
      :forbidden,
      "Your account is awaiting approval.",
      %{},
      "awaiting_approval"
    )
  end

360
361
362
363
  defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
    render_invalid_credentials_error(conn)
  end

364
  def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
Maksim's avatar
Maksim committed
365
    with {:ok, app} <- Token.Utils.fetch_app(conn),
Maksim's avatar
Maksim committed
366
         {:ok, _token} <- RevokeToken.revoke(app, params) do
367
368
369
370
371
372
373
374
      json(conn, %{})
    else
      _error ->
        # RFC 7009: invalid tokens [in the request] do not cause an error response
        json(conn, %{})
    end
  end

375
  def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
Maksim's avatar
Maksim committed
376
377

  # Response for bad request
378
  defp bad_request(%Plug.Conn{} = conn, _) do
379
    render_error(conn, :internal_server_error, "Bad request")
Maksim's avatar
Maksim committed
380
381
  end

382
  @doc "Prepares OAuth request to provider for Ueberauth"
383
384
385
386
  def prepare_request(%Plug.Conn{} = conn, %{
        "provider" => provider,
        "authorization" => auth_attrs
      }) do
387
    scope =
388
389
390
      auth_attrs
      |> Scopes.fetch_scopes([])
      |> Scopes.to_string()
391
392

    state =
393
      auth_attrs
394
395
      |> Map.delete("scopes")
      |> Map.put("scope", scope)
feld's avatar
feld committed
396
      |> Jason.encode!()
397
398

    params =
399
      auth_attrs
400
401
402
      |> Map.drop(~w(scope scopes client_id redirect_uri))
      |> Map.put("state", state)

403
    # Handing the request to Ueberauth
404
405
406
    redirect(conn, to: o_auth_path(conn, :request, provider, params))
  end

407
  def request(%Plug.Conn{} = conn, params) do
408
409
    message =
      if params["provider"] do
410
411
412
        dgettext("errors", "Unsupported OAuth provider: %{provider}.",
          provider: params["provider"]
        )
413
      else
414
        dgettext("errors", "Bad OAuth request.")
415
416
417
418
419
420
421
      end

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

422
  def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
423
    params = callback_params(params)
424
425
426
427
    messages = for e <- Map.get(failure, :errors, []), do: e.message
    message = Enum.join(messages, "; ")

    conn
428
429
430
431
    |> put_flash(
      :error,
      dgettext("errors", "Failed to authenticate: %{message}.", message: message)
    )
432
    |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
433
434
  end

435
  def callback(%Plug.Conn{} = conn, params) do
436
437
    params = callback_params(params)

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

Maksim's avatar
Maksim committed
441
442
443
      case Repo.get_assoc(registration, :user) do
        {:ok, user} ->
          create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
444

Maksim's avatar
Maksim committed
445
446
447
448
449
450
451
452
        _ ->
          registration_params =
            Map.merge(auth_attrs, %{
              "nickname" => Registration.nickname(registration),
              "email" => Registration.email(registration)
            })

          conn
453
          |> put_session_registration_id(registration.id)
Maksim's avatar
Maksim committed
454
          |> registration_details(%{"authorization" => registration_params})
455
456
      end
    else
457
458
459
      error ->
        Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))

460
        conn
461
        |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
462
        |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
463
464
465
    end
  end

466
  defp callback_params(%{"state" => state} = params) do
feld's avatar
feld committed
467
    Map.merge(params, Jason.decode!(state))
468
469
  end

470
  def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
471
    render(conn, "register.html", %{
472
473
474
      client_id: auth_attrs["client_id"],
      redirect_uri: auth_attrs["redirect_uri"],
      state: auth_attrs["state"],
475
      scopes: Scopes.fetch_scopes(auth_attrs, []),
476
477
      nickname: auth_attrs["nickname"],
      email: auth_attrs["email"]
478
479
480
    })
  end

481
  def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
482
483
    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
         %Registration{} = registration <- Repo.get(Registration, registration_id),
484
485
         {_, {:ok, auth, _user}} <-
           {:create_authorization, do_create_authorization(conn, params)},
486
487
488
489
         %User{} = user <- Repo.preload(auth, :user).user,
         {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
      conn
      |> put_session_registration_id(nil)
490
      |> after_create_authorization(auth, params)
491
    else
492
      {:create_authorization, error} ->
493
        {:register, handle_create_authorization_error(conn, error, params)}
494

495
496
      _ ->
        {:register, :generic_error}
497
498
499
    end
  end

500
  def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
501
502
    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
         %Registration{} = registration <- Repo.get(Registration, registration_id),
503
         {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
504
505
506
      conn
      |> put_session_registration_id(nil)
      |> create_authorization(
507
        params,
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
        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
526
        |> put_status(:forbidden)
527
        |> put_flash(:error, "Error: #{message}.")
528
        |> registration_details(params)
529
530

      _ ->
531
        {:register, :generic_error}
532
533
534
    end
  end

535
536
  defp do_create_authorization(conn, auth_attrs, user \\ nil)

537
  defp do_create_authorization(
538
         %Plug.Conn{} = conn,
539
540
541
542
543
         %{
           "authorization" =>
             %{
               "client_id" => client_id,
               "redirect_uri" => redirect_uri
544
545
             } = auth_attrs
         },
546
         user
547
548
       ) do
    with {_, {:ok, %User{} = user}} <-
549
           {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
550
551
         %App{} = app <- Repo.get_by(App, client_id: client_id),
         true <- redirect_uri in String.split(app.redirect_uris),
552
553
         requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
         {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
554
      {:ok, auth, user}
555
556
557
    end
  end

558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
  defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
       when is_list(requested_scopes) do
    with {:account_status, :active} <- {:account_status, User.account_status(user)},
         {:ok, scopes} <- validate_scopes(app, requested_scopes),
         {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
      {:ok, auth}
    end
  end

  # Note: intended to be a private function but opened for AccountController that logs in on signup
  @doc "If checks pass, creates authorization and token for given user, app and requested scopes."
  def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
    with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
         {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
         {:ok, token} <- Token.exchange_token(app, auth) do
      {:ok, token}
    end
  end

577
  # Special case: Local MastodonFE
minibikini's avatar
minibikini committed
578
  defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
579

580
  defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
581

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

584
  defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
585
    do: put_session(conn, :registration_id, registration_id)
Maksim's avatar
Maksim committed
586

587
  defp build_and_response_mfa_token(user, auth) do
588
    with {:ok, token} <- MFA.Token.create(user, auth) do
589
      MFAView.render("mfa_response.json", %{token: token, user: user})
590
591
592
    end
  end

593
  @spec validate_scopes(App.t(), map() | list()) ::
594
          {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
595
596
597
598
599
600
601
  defp validate_scopes(%App{} = app, params) when is_map(params) do
    requested_scopes = Scopes.fetch_scopes(params, app.scopes)
    validate_scopes(app, requested_scopes)
  end

  defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
    Scopes.validate(requested_scopes, app.scopes)
602
  end
603

604
  def default_redirect_uri(%App{} = app) do
605
606
607
608
    app.redirect_uris
    |> String.split()
    |> Enum.at(0)
  end
609
610
611
612

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