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