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