o_auth_controller.ex 20.4 KB
Newer Older
1
# Pleroma: A lightweight social networking server
Haelwenn's avatar
Haelwenn committed
2
# Copyright © 2017-2021 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.AuthHelper
9
  alias Pleroma.Helpers.UriHelper
10
  alias Pleroma.Maps
lain's avatar
lain committed
11
  alias Pleroma.MFA
12
  alias Pleroma.Registration
13
14
  alias Pleroma.Repo
  alias Pleroma.User
15
  alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator
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
  alias Pleroma.Web.Utils.Params
27

28
29
  require Logger

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

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

35
  plug(:skip_auth)
36
37

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

lain's avatar
lain committed
39
  action_fallback(Pleroma.Web.OAuth.FallbackController)
40

41
42
  @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"

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

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

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
  # 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

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

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

80
81
82
83
84
85
86
    user =
      with %{assigns: %{user: %User{} = user}} <- conn do
        user
      else
        _ -> nil
      end

87
88
89
90
91
92
93
    scopes =
      if scopes == [] do
        available_scopes
      else
        scopes
      end

94
    # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
95
    render(conn, Authenticator.auth_template(), %{
96
97
      user: user,
      app: app && Map.delete(app, :client_secret),
98
99
      response_type: params["response_type"],
      client_id: params["client_id"],
100
101
      available_scopes: available_scopes,
      scopes: scopes,
102
103
104
      redirect_uri: params["redirect_uri"],
      state: params["state"],
      params: params
lain's avatar
lain committed
105
    })
106
107
  end

108
109
  defp handle_existing_authorization(
         %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
110
         %{"redirect_uri" => @oob_token_redirect_uri}
111
       ) do
112
113
114
115
116
117
118
119
    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
120
121
122
123
124

    redirect_uri =
      if is_binary(params["redirect_uri"]) do
        params["redirect_uri"]
      else
125
        default_redirect_uri(app)
126
127
      end

128
129
    if redirect_uri in String.split(app.redirect_uris) do
      redirect_uri = redirect_uri(conn, redirect_uri)
130
      url_params = %{access_token: token.token}
131
      url_params = Maps.put_if_present(url_params, :state, params["state"])
132
      url = UriHelper.modify_uri_params(redirect_uri, url_params)
133
      redirect(conn, external: url)
134
135
    else
      conn
136
      |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
137
      |> redirect(external: redirect_uri(conn, redirect_uri))
138
139
140
    end
  end

141
142
143
144
145
146
147
  def create_authorization(_, _, opts \\ [])

  def create_authorization(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, params, []) do
    create_authorization(conn, params, user: user)
  end

  def create_authorization(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do
148
149
    with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
         {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
150
      after_create_authorization(conn, auth, params)
151
152
    else
      error ->
153
        handle_create_authorization_error(conn, error, params)
154
155
    end
  end
lain's avatar
lain committed
156

157
158
159
  def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
        "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
      }) do
160
161
162
163
    # Enforcing the view to reuse the template when calling from other controllers
    conn
    |> put_view(OAuthView)
    |> render("oob_authorization_created.html", %{auth: auth})
164
165
  end

166
  def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
167
168
        "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
      }) do
169
    app = Repo.preload(auth, :app).app
lain's avatar
lain committed
170

Ivan Tashkinov's avatar
Ivan Tashkinov committed
171
    # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
172
173
    if redirect_uri in String.split(app.redirect_uris) do
      redirect_uri = redirect_uri(conn, redirect_uri)
174
      url_params = %{code: auth.token}
175
      url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
176
      url = UriHelper.modify_uri_params(redirect_uri, url_params)
177
      redirect(conn, external: url)
178
179
    else
      conn
180
      |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
181
      |> redirect(external: redirect_uri(conn, redirect_uri))
182
183
184
    end
  end

185
  defp handle_create_authorization_error(
186
         %Plug.Conn{} = conn,
187
         {:error, scopes_issue},
188
189
         %{"authorization" => _} = params
       )
190
191
192
193
       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
194
    |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
195
    |> put_status(:unauthorized)
196
    |> authorize(params)
197
198
  end

199
  defp handle_create_authorization_error(
200
         %Plug.Conn{} = conn,
201
         {:account_status, :confirmation_pending},
202
203
         %{"authorization" => _} = params
       ) do
204
    conn
205
    |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
206
    |> put_status(:forbidden)
207
    |> authorize(params)
208
209
  end

210
211
212
213
214
  defp handle_create_authorization_error(
         %Plug.Conn{} = conn,
         {:mfa_required, user, auth, _},
         params
       ) do
215
    {:ok, token} = MFA.Token.create(user, auth)
216
217
218
219
220
221
222
223
224
225

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

    MFAController.show(conn, data)
  end

226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
  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

248
  defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
249
250
251
    Authenticator.handle_error(conn, error)
  end

Maksim's avatar
Maksim committed
252
253
  @doc "Renew access_token with refresh_token"
  def token_exchange(
254
        %Plug.Conn{} = conn,
Maksim's avatar
Maksim committed
255
        %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
Maksim's avatar
Maksim committed
256
      ) do
Maksim's avatar
Maksim committed
257
    with {:ok, app} <- Token.Utils.fetch_app(conn),
Maksim's avatar
Maksim committed
258
259
         {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
         {:ok, token} <- RefreshToken.grant(token) do
260
      after_token_exchange(conn, %{user: user, token: token})
Maksim's avatar
Maksim committed
261
    else
262
      _error -> render_invalid_credentials_error(conn)
Maksim's avatar
Maksim committed
263
264
265
    end
  end

266
  def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
Maksim's avatar
Maksim committed
267
    with {:ok, app} <- Token.Utils.fetch_app(conn),
Maksim's avatar
Maksim committed
268
269
         fixed_token = Token.Utils.fix_padding(params["code"]),
         {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
minibikini's avatar
minibikini committed
270
         %User{} = user <- User.get_cached_by_id(auth.user_id),
Maksim's avatar
Maksim committed
271
         {:ok, token} <- Token.exchange_token(app, auth) do
272
      after_token_exchange(conn, %{user: user, token: token})
eal's avatar
eal committed
273
    else
274
275
      error ->
        handle_token_exchange_error(conn, error)
276
277
    end
  end
eal's avatar
eal committed
278

lain's avatar
lain committed
279
  def token_exchange(
280
        %Plug.Conn{} = conn,
281
        %{"grant_type" => "password"} = params
lain's avatar
lain committed
282
      ) do
Maksim's avatar
Maksim committed
283
284
    with {:ok, %User{} = user} <- Authenticator.get_user(conn),
         {:ok, app} <- Token.Utils.fetch_app(conn),
285
286
         requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
         {:ok, token} <- login(user, app, requested_scopes) do
287
      after_token_exchange(conn, %{user: user, token: token})
288
    else
289
290
      error ->
        handle_token_exchange_error(conn, error)
291
292
293
    end
  end

294
  def token_exchange(
295
        %Plug.Conn{} = conn,
Maksim's avatar
Maksim committed
296
        %{"grant_type" => "password", "name" => name, "password" => _password} = params
297
298
299
300
301
302
303
304
305
      ) do
    params =
      params
      |> Map.delete("name")
      |> Map.put("username", name)

    token_exchange(conn, params)
  end

306
  def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
Maksim's avatar
Maksim committed
307
    with {:ok, app} <- Token.Utils.fetch_app(conn),
308
         {:ok, auth} <- Authorization.create_authorization(app, %User{}),
Maksim's avatar
Maksim committed
309
         {:ok, token} <- Token.exchange_token(app, auth) do
310
      after_token_exchange(conn, %{token: token})
311
    else
312
313
      _error ->
        handle_token_exchange_error(conn, :invalid_credentails)
314
315
316
    end
  end

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

320
321
322
323
324
325
  def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
    conn
    |> AuthHelper.put_session_token(token.token)
    |> json(OAuthView.render("token.json", view_params))
  end

326
327
328
329
330
331
  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

332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
  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

365
366
367
368
369
370
371
372
373
374
  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

375
376
377
378
  defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
    render_invalid_credentials_error(conn)
  end

379
380
381
  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
382
      conn =
383
        with session_token = AuthHelper.get_session_token(conn),
384
             %Token{token: ^session_token} <- oauth_token do
385
          AuthHelper.delete_session_token(conn)
386
387
388
389
        else
          _ -> conn
        end

390
391
392
393
394
395
396
397
      json(conn, %{})
    else
      _error ->
        # RFC 7009: invalid tokens [in the request] do not cause an error response
        json(conn, %{})
    end
  end

398
  def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
Maksim's avatar
Maksim committed
399
400

  # Response for bad request
401
  defp bad_request(%Plug.Conn{} = conn, _) do
402
    render_error(conn, :internal_server_error, "Bad request")
Maksim's avatar
Maksim committed
403
404
  end

405
  @doc "Prepares OAuth request to provider for Ueberauth"
406
407
408
409
  def prepare_request(%Plug.Conn{} = conn, %{
        "provider" => provider,
        "authorization" => auth_attrs
      }) do
410
    scope =
411
412
413
      auth_attrs
      |> Scopes.fetch_scopes([])
      |> Scopes.to_string()
414
415

    state =
416
      auth_attrs
417
418
      |> Map.delete("scopes")
      |> Map.put("scope", scope)
feld's avatar
feld committed
419
      |> Jason.encode!()
420
421

    params =
422
      auth_attrs
423
424
425
      |> Map.drop(~w(scope scopes client_id redirect_uri))
      |> Map.put("state", state)

426
    # Handing the request to Ueberauth
427
    redirect(conn, to: Routes.o_auth_path(conn, :request, provider, params))
428
429
  end

430
  def request(%Plug.Conn{} = conn, params) do
431
432
    message =
      if params["provider"] do
433
434
435
        dgettext("errors", "Unsupported OAuth provider: %{provider}.",
          provider: params["provider"]
        )
436
      else
437
        dgettext("errors", "Bad OAuth request.")
438
439
440
441
442
443
444
      end

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

445
  def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
446
    params = callback_params(params)
447
448
449
450
    messages = for e <- Map.get(failure, :errors, []), do: e.message
    message = Enum.join(messages, "; ")

    conn
451
452
453
454
    |> put_flash(
      :error,
      dgettext("errors", "Failed to authenticate: %{message}.", message: message)
    )
455
    |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
456
457
  end

458
  def callback(%Plug.Conn{} = conn, params) do
459
460
    params = callback_params(params)

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

Maksim's avatar
Maksim committed
464
465
466
      case Repo.get_assoc(registration, :user) do
        {:ok, user} ->
          create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
467

Maksim's avatar
Maksim committed
468
469
470
471
472
473
474
475
        _ ->
          registration_params =
            Map.merge(auth_attrs, %{
              "nickname" => Registration.nickname(registration),
              "email" => Registration.email(registration)
            })

          conn
476
          |> put_session_registration_id(registration.id)
Maksim's avatar
Maksim committed
477
          |> registration_details(%{"authorization" => registration_params})
478
479
      end
    else
480
481
482
      error ->
        Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))

483
        conn
484
        |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
485
        |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
486
487
488
    end
  end

489
  defp callback_params(%{"state" => state} = params) do
feld's avatar
feld committed
490
    Map.merge(params, Jason.decode!(state))
491
492
  end

493
  def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
494
    render(conn, "register.html", %{
495
496
497
      client_id: auth_attrs["client_id"],
      redirect_uri: auth_attrs["redirect_uri"],
      state: auth_attrs["state"],
498
      scopes: Scopes.fetch_scopes(auth_attrs, []),
499
500
      nickname: auth_attrs["nickname"],
      email: auth_attrs["email"]
501
502
503
    })
  end

504
  def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
505
506
    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
         %Registration{} = registration <- Repo.get(Registration, registration_id),
507
508
         {_, {:ok, auth, _user}} <-
           {:create_authorization, do_create_authorization(conn, params)},
509
510
511
512
         %User{} = user <- Repo.preload(auth, :user).user,
         {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
      conn
      |> put_session_registration_id(nil)
513
      |> after_create_authorization(auth, params)
514
    else
515
      {:create_authorization, error} ->
516
        {:register, handle_create_authorization_error(conn, error, params)}
517

518
519
      _ ->
        {:register, :generic_error}
520
521
522
    end
  end

523
  def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
524
525
    with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
         %Registration{} = registration <- Repo.get(Registration, registration_id),
526
         {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
527
528
529
      conn
      |> put_session_registration_id(nil)
      |> create_authorization(
530
        params,
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
        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
549
        |> put_status(:forbidden)
550
        |> put_flash(:error, "Error: #{message}.")
551
        |> registration_details(params)
552
553

      _ ->
554
        {:register, :generic_error}
555
556
557
    end
  end

558
559
  defp do_create_authorization(conn, auth_attrs, user \\ nil)

560
  defp do_create_authorization(
561
         %Plug.Conn{} = conn,
562
563
564
565
566
         %{
           "authorization" =>
             %{
               "client_id" => client_id,
               "redirect_uri" => redirect_uri
567
568
             } = auth_attrs
         },
569
         user
570
571
       ) do
    with {_, {:ok, %User{} = user}} <-
572
           {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
573
574
         %App{} = app <- Repo.get_by(App, client_id: client_id),
         true <- redirect_uri in String.split(app.redirect_uris),
575
576
         requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
         {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
577
      {:ok, auth, user}
578
579
580
    end
  end

581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
  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
599

600
  defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
601

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

604
  defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
605
    do: put_session(conn, :registration_id, registration_id)
Maksim's avatar
Maksim committed
606

607
  defp build_and_response_mfa_token(user, auth) do
608
    with {:ok, token} <- MFA.Token.create(user, auth) do
609
      MFAView.render("mfa_response.json", %{token: token, user: user})
610
611
612
    end
  end

613
  @spec validate_scopes(App.t(), map() | list()) ::
614
          {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
615
616
617
618
619
620
621
  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)
622
  end
623

624
  def default_redirect_uri(%App{} = app) do
625
626
627
628
    app.redirect_uris
    |> String.split()
    |> Enum.at(0)
  end
629
630
631
632

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