Merge branch 'develop' into 'remove-twitter-api'
[akkoma] / lib / pleroma / web / oauth / oauth_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.OAuth.OAuthController do
6 use Pleroma.Web, :controller
7
8 alias Pleroma.Helpers.UriHelper
9 alias Pleroma.MFA
10 alias Pleroma.Plugs.RateLimiter
11 alias Pleroma.Registration
12 alias Pleroma.Repo
13 alias Pleroma.User
14 alias Pleroma.Web.Auth.Authenticator
15 alias Pleroma.Web.ControllerHelper
16 alias Pleroma.Web.OAuth.App
17 alias Pleroma.Web.OAuth.Authorization
18 alias Pleroma.Web.OAuth.MFAController
19 alias Pleroma.Web.OAuth.Scopes
20 alias Pleroma.Web.OAuth.Token
21 alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
22 alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
23
24 require Logger
25
26 if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
27
28 plug(:fetch_session)
29 plug(:fetch_flash)
30
31 plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug])
32
33 plug(RateLimiter, [name: :authentication] when action == :create_authorization)
34
35 action_fallback(Pleroma.Web.OAuth.FallbackController)
36
37 @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
38
39 # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
40 def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
41 {auth_attrs, params} = Map.pop(params, "authorization")
42 authorize(conn, Map.merge(params, auth_attrs))
43 end
44
45 def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
46 if ControllerHelper.truthy_param?(params["force_login"]) do
47 do_authorize(conn, params)
48 else
49 handle_existing_authorization(conn, params)
50 end
51 end
52
53 # Note: the token is set in oauth_plug, but the token and client do not always go together.
54 # For example, MastodonFE's token is set if user requests with another client,
55 # after user already authorized to MastodonFE.
56 # So we have to check client and token.
57 def authorize(
58 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
59 %{"client_id" => client_id} = params
60 ) do
61 with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
62 ^client_id <- t.app.client_id do
63 handle_existing_authorization(conn, params)
64 else
65 _ -> do_authorize(conn, params)
66 end
67 end
68
69 def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
70
71 defp do_authorize(%Plug.Conn{} = conn, params) do
72 app = Repo.get_by(App, client_id: params["client_id"])
73 available_scopes = (app && app.scopes) || []
74 scopes = Scopes.fetch_scopes(params, available_scopes)
75
76 # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
77 render(conn, Authenticator.auth_template(), %{
78 response_type: params["response_type"],
79 client_id: params["client_id"],
80 available_scopes: available_scopes,
81 scopes: scopes,
82 redirect_uri: params["redirect_uri"],
83 state: params["state"],
84 params: params
85 })
86 end
87
88 defp handle_existing_authorization(
89 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
90 %{"redirect_uri" => @oob_token_redirect_uri}
91 ) do
92 render(conn, "oob_token_exists.html", %{token: token})
93 end
94
95 defp handle_existing_authorization(
96 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
97 %{} = params
98 ) do
99 app = Repo.preload(token, :app).app
100
101 redirect_uri =
102 if is_binary(params["redirect_uri"]) do
103 params["redirect_uri"]
104 else
105 default_redirect_uri(app)
106 end
107
108 if redirect_uri in String.split(app.redirect_uris) do
109 redirect_uri = redirect_uri(conn, redirect_uri)
110 url_params = %{access_token: token.token}
111 url_params = UriHelper.append_param_if_present(url_params, :state, params["state"])
112 url = UriHelper.append_uri_params(redirect_uri, url_params)
113 redirect(conn, external: url)
114 else
115 conn
116 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
117 |> redirect(external: redirect_uri(conn, redirect_uri))
118 end
119 end
120
121 def create_authorization(
122 %Plug.Conn{} = conn,
123 %{"authorization" => _} = params,
124 opts \\ []
125 ) do
126 with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
127 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
128 after_create_authorization(conn, auth, params)
129 else
130 error ->
131 handle_create_authorization_error(conn, error, params)
132 end
133 end
134
135 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
136 "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
137 }) do
138 render(conn, "oob_authorization_created.html", %{auth: auth})
139 end
140
141 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
142 "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
143 }) do
144 app = Repo.preload(auth, :app).app
145
146 # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
147 if redirect_uri in String.split(app.redirect_uris) do
148 redirect_uri = redirect_uri(conn, redirect_uri)
149 url_params = %{code: auth.token}
150 url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"])
151 url = UriHelper.append_uri_params(redirect_uri, url_params)
152 redirect(conn, external: url)
153 else
154 conn
155 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
156 |> redirect(external: redirect_uri(conn, redirect_uri))
157 end
158 end
159
160 defp handle_create_authorization_error(
161 %Plug.Conn{} = conn,
162 {:error, scopes_issue},
163 %{"authorization" => _} = params
164 )
165 when scopes_issue in [:unsupported_scopes, :missing_scopes] do
166 # Per https://github.com/tootsuite/mastodon/blob/
167 # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
168 conn
169 |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
170 |> put_status(:unauthorized)
171 |> authorize(params)
172 end
173
174 defp handle_create_authorization_error(
175 %Plug.Conn{} = conn,
176 {:account_status, :confirmation_pending},
177 %{"authorization" => _} = params
178 ) do
179 conn
180 |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
181 |> put_status(:forbidden)
182 |> authorize(params)
183 end
184
185 defp handle_create_authorization_error(
186 %Plug.Conn{} = conn,
187 {:mfa_required, user, auth, _},
188 params
189 ) do
190 {:ok, token} = MFA.Token.create_token(user, auth)
191
192 data = %{
193 "mfa_token" => token.token,
194 "redirect_uri" => params["authorization"]["redirect_uri"],
195 "state" => params["authorization"]["state"]
196 }
197
198 MFAController.show(conn, data)
199 end
200
201 defp handle_create_authorization_error(
202 %Plug.Conn{} = conn,
203 {:account_status, :password_reset_pending},
204 %{"authorization" => _} = params
205 ) do
206 conn
207 |> put_flash(:error, dgettext("errors", "Password reset is required"))
208 |> put_status(:forbidden)
209 |> authorize(params)
210 end
211
212 defp handle_create_authorization_error(
213 %Plug.Conn{} = conn,
214 {:account_status, :deactivated},
215 %{"authorization" => _} = params
216 ) do
217 conn
218 |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
219 |> put_status(:forbidden)
220 |> authorize(params)
221 end
222
223 defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
224 Authenticator.handle_error(conn, error)
225 end
226
227 @doc "Renew access_token with refresh_token"
228 def token_exchange(
229 %Plug.Conn{} = conn,
230 %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
231 ) do
232 with {:ok, app} <- Token.Utils.fetch_app(conn),
233 {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
234 {:ok, token} <- RefreshToken.grant(token) do
235 response_attrs = %{created_at: Token.Utils.format_created_at(token)}
236
237 json(conn, Token.Response.build(user, token, response_attrs))
238 else
239 _error -> render_invalid_credentials_error(conn)
240 end
241 end
242
243 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
244 with {:ok, app} <- Token.Utils.fetch_app(conn),
245 fixed_token = Token.Utils.fix_padding(params["code"]),
246 {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
247 %User{} = user <- User.get_cached_by_id(auth.user_id),
248 {:ok, token} <- Token.exchange_token(app, auth) do
249 response_attrs = %{created_at: Token.Utils.format_created_at(token)}
250
251 json(conn, Token.Response.build(user, token, response_attrs))
252 else
253 error ->
254 handle_token_exchange_error(conn, error)
255 end
256 end
257
258 def token_exchange(
259 %Plug.Conn{} = conn,
260 %{"grant_type" => "password"} = params
261 ) do
262 with {:ok, %User{} = user} <- Authenticator.get_user(conn),
263 {:ok, app} <- Token.Utils.fetch_app(conn),
264 {:account_status, :active} <- {:account_status, User.account_status(user)},
265 {:ok, scopes} <- validate_scopes(app, params),
266 {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
267 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
268 {:ok, token} <- Token.exchange_token(app, auth) do
269 json(conn, Token.Response.build(user, token))
270 else
271 error ->
272 handle_token_exchange_error(conn, error)
273 end
274 end
275
276 def token_exchange(
277 %Plug.Conn{} = conn,
278 %{"grant_type" => "password", "name" => name, "password" => _password} = params
279 ) do
280 params =
281 params
282 |> Map.delete("name")
283 |> Map.put("username", name)
284
285 token_exchange(conn, params)
286 end
287
288 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
289 with {:ok, app} <- Token.Utils.fetch_app(conn),
290 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
291 {:ok, token} <- Token.exchange_token(app, auth) do
292 json(conn, Token.Response.build_for_client_credentials(token))
293 else
294 _error ->
295 handle_token_exchange_error(conn, :invalid_credentails)
296 end
297 end
298
299 # Bad request
300 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
301
302 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
303 conn
304 |> put_status(:forbidden)
305 |> json(build_and_response_mfa_token(user, auth))
306 end
307
308 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
309 render_error(
310 conn,
311 :forbidden,
312 "Your account is currently disabled",
313 %{},
314 "account_is_disabled"
315 )
316 end
317
318 defp handle_token_exchange_error(
319 %Plug.Conn{} = conn,
320 {:account_status, :password_reset_pending}
321 ) do
322 render_error(
323 conn,
324 :forbidden,
325 "Password reset is required",
326 %{},
327 "password_reset_required"
328 )
329 end
330
331 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
332 render_error(
333 conn,
334 :forbidden,
335 "Your login is missing a confirmed e-mail address",
336 %{},
337 "missing_confirmed_email"
338 )
339 end
340
341 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
342 render_invalid_credentials_error(conn)
343 end
344
345 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
346 with {:ok, app} <- Token.Utils.fetch_app(conn),
347 {:ok, _token} <- RevokeToken.revoke(app, params) do
348 json(conn, %{})
349 else
350 _error ->
351 # RFC 7009: invalid tokens [in the request] do not cause an error response
352 json(conn, %{})
353 end
354 end
355
356 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
357
358 # Response for bad request
359 defp bad_request(%Plug.Conn{} = conn, _) do
360 render_error(conn, :internal_server_error, "Bad request")
361 end
362
363 @doc "Prepares OAuth request to provider for Ueberauth"
364 def prepare_request(%Plug.Conn{} = conn, %{
365 "provider" => provider,
366 "authorization" => auth_attrs
367 }) do
368 scope =
369 auth_attrs
370 |> Scopes.fetch_scopes([])
371 |> Scopes.to_string()
372
373 state =
374 auth_attrs
375 |> Map.delete("scopes")
376 |> Map.put("scope", scope)
377 |> Jason.encode!()
378
379 params =
380 auth_attrs
381 |> Map.drop(~w(scope scopes client_id redirect_uri))
382 |> Map.put("state", state)
383
384 # Handing the request to Ueberauth
385 redirect(conn, to: o_auth_path(conn, :request, provider, params))
386 end
387
388 def request(%Plug.Conn{} = conn, params) do
389 message =
390 if params["provider"] do
391 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
392 provider: params["provider"]
393 )
394 else
395 dgettext("errors", "Bad OAuth request.")
396 end
397
398 conn
399 |> put_flash(:error, message)
400 |> redirect(to: "/")
401 end
402
403 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
404 params = callback_params(params)
405 messages = for e <- Map.get(failure, :errors, []), do: e.message
406 message = Enum.join(messages, "; ")
407
408 conn
409 |> put_flash(
410 :error,
411 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
412 )
413 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
414 end
415
416 def callback(%Plug.Conn{} = conn, params) do
417 params = callback_params(params)
418
419 with {:ok, registration} <- Authenticator.get_registration(conn) do
420 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
421
422 case Repo.get_assoc(registration, :user) do
423 {:ok, user} ->
424 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
425
426 _ ->
427 registration_params =
428 Map.merge(auth_attrs, %{
429 "nickname" => Registration.nickname(registration),
430 "email" => Registration.email(registration)
431 })
432
433 conn
434 |> put_session_registration_id(registration.id)
435 |> registration_details(%{"authorization" => registration_params})
436 end
437 else
438 error ->
439 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
440
441 conn
442 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
443 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
444 end
445 end
446
447 defp callback_params(%{"state" => state} = params) do
448 Map.merge(params, Jason.decode!(state))
449 end
450
451 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
452 render(conn, "register.html", %{
453 client_id: auth_attrs["client_id"],
454 redirect_uri: auth_attrs["redirect_uri"],
455 state: auth_attrs["state"],
456 scopes: Scopes.fetch_scopes(auth_attrs, []),
457 nickname: auth_attrs["nickname"],
458 email: auth_attrs["email"]
459 })
460 end
461
462 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
463 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
464 %Registration{} = registration <- Repo.get(Registration, registration_id),
465 {_, {:ok, auth, _user}} <-
466 {:create_authorization, do_create_authorization(conn, params)},
467 %User{} = user <- Repo.preload(auth, :user).user,
468 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
469 conn
470 |> put_session_registration_id(nil)
471 |> after_create_authorization(auth, params)
472 else
473 {:create_authorization, error} ->
474 {:register, handle_create_authorization_error(conn, error, params)}
475
476 _ ->
477 {:register, :generic_error}
478 end
479 end
480
481 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
482 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
483 %Registration{} = registration <- Repo.get(Registration, registration_id),
484 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
485 conn
486 |> put_session_registration_id(nil)
487 |> create_authorization(
488 params,
489 user: user
490 )
491 else
492 {:error, changeset} ->
493 message =
494 Enum.map(changeset.errors, fn {field, {error, _}} ->
495 "#{field} #{error}"
496 end)
497 |> Enum.join("; ")
498
499 message =
500 String.replace(
501 message,
502 "ap_id has already been taken",
503 "nickname has already been taken"
504 )
505
506 conn
507 |> put_status(:forbidden)
508 |> put_flash(:error, "Error: #{message}.")
509 |> registration_details(params)
510
511 _ ->
512 {:register, :generic_error}
513 end
514 end
515
516 defp do_create_authorization(
517 %Plug.Conn{} = conn,
518 %{
519 "authorization" =>
520 %{
521 "client_id" => client_id,
522 "redirect_uri" => redirect_uri
523 } = auth_attrs
524 },
525 user \\ nil
526 ) do
527 with {_, {:ok, %User{} = user}} <-
528 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
529 %App{} = app <- Repo.get_by(App, client_id: client_id),
530 true <- redirect_uri in String.split(app.redirect_uris),
531 {:ok, scopes} <- validate_scopes(app, auth_attrs),
532 {:account_status, :active} <- {:account_status, User.account_status(user)},
533 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
534 {:ok, auth, user}
535 end
536 end
537
538 # Special case: Local MastodonFE
539 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
540
541 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
542
543 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
544
545 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
546 do: put_session(conn, :registration_id, registration_id)
547
548 defp build_and_response_mfa_token(user, auth) do
549 with {:ok, token} <- MFA.Token.create_token(user, auth) do
550 Token.Response.build_for_mfa_token(user, token)
551 end
552 end
553
554 @spec validate_scopes(App.t(), map()) ::
555 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
556 defp validate_scopes(%App{} = app, params) do
557 params
558 |> Scopes.fetch_scopes(app.scopes)
559 |> Scopes.validate(app.scopes)
560 end
561
562 def default_redirect_uri(%App{} = app) do
563 app.redirect_uris
564 |> String.split()
565 |> Enum.at(0)
566 end
567
568 defp render_invalid_credentials_error(conn) do
569 render_error(conn, :bad_request, "Invalid credentials")
570 end
571 end