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