Merge remote-tracking branch 'upstream/develop' into feature/move-activity
[akkoma] / lib / pleroma / web / oauth / oauth_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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.Token
18 alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
19 alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
20 alias Pleroma.Web.OAuth.Scopes
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 {:auth_active, false},
171 %{"authorization" => _} = params
172 ) do
173 # Per https://github.com/tootsuite/mastodon/blob/
174 # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
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(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
182 Authenticator.handle_error(conn, error)
183 end
184
185 @doc "Renew access_token with refresh_token"
186 def token_exchange(
187 %Plug.Conn{} = conn,
188 %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
189 ) do
190 with {:ok, app} <- Token.Utils.fetch_app(conn),
191 {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
192 {:ok, token} <- RefreshToken.grant(token) do
193 response_attrs = %{created_at: Token.Utils.format_created_at(token)}
194
195 json(conn, Token.Response.build(user, token, response_attrs))
196 else
197 _error -> render_invalid_credentials_error(conn)
198 end
199 end
200
201 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
202 with {:ok, app} <- Token.Utils.fetch_app(conn),
203 fixed_token = Token.Utils.fix_padding(params["code"]),
204 {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
205 %User{} = user <- User.get_cached_by_id(auth.user_id),
206 {:ok, token} <- Token.exchange_token(app, auth) do
207 response_attrs = %{created_at: Token.Utils.format_created_at(token)}
208
209 json(conn, Token.Response.build(user, token, response_attrs))
210 else
211 _error -> render_invalid_credentials_error(conn)
212 end
213 end
214
215 def token_exchange(
216 %Plug.Conn{} = conn,
217 %{"grant_type" => "password"} = params
218 ) do
219 with {:ok, %User{} = user} <- Authenticator.get_user(conn),
220 {:ok, app} <- Token.Utils.fetch_app(conn),
221 {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
222 {:user_active, true} <- {:user_active, !user.deactivated},
223 {:password_reset_pending, false} <-
224 {:password_reset_pending, user.password_reset_pending},
225 {:ok, scopes} <- validate_scopes(app, params),
226 {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
227 {:ok, token} <- Token.exchange_token(app, auth) do
228 json(conn, Token.Response.build(user, token))
229 else
230 {:auth_active, false} ->
231 # Per https://github.com/tootsuite/mastodon/blob/
232 # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
233 render_error(
234 conn,
235 :forbidden,
236 "Your login is missing a confirmed e-mail address",
237 %{},
238 "missing_confirmed_email"
239 )
240
241 {:user_active, false} ->
242 render_error(
243 conn,
244 :forbidden,
245 "Your account is currently disabled",
246 %{},
247 "account_is_disabled"
248 )
249
250 {:password_reset_pending, true} ->
251 render_error(
252 conn,
253 :forbidden,
254 "Password reset is required",
255 %{},
256 "password_reset_required"
257 )
258
259 _error ->
260 render_invalid_credentials_error(conn)
261 end
262 end
263
264 def token_exchange(
265 %Plug.Conn{} = conn,
266 %{"grant_type" => "password", "name" => name, "password" => _password} = params
267 ) do
268 params =
269 params
270 |> Map.delete("name")
271 |> Map.put("username", name)
272
273 token_exchange(conn, params)
274 end
275
276 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
277 with {:ok, app} <- Token.Utils.fetch_app(conn),
278 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
279 {:ok, token} <- Token.exchange_token(app, auth) do
280 json(conn, Token.Response.build_for_client_credentials(token))
281 else
282 _error -> render_invalid_credentials_error(conn)
283 end
284 end
285
286 # Bad request
287 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
288
289 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
290 with {:ok, app} <- Token.Utils.fetch_app(conn),
291 {:ok, _token} <- RevokeToken.revoke(app, params) do
292 json(conn, %{})
293 else
294 _error ->
295 # RFC 7009: invalid tokens [in the request] do not cause an error response
296 json(conn, %{})
297 end
298 end
299
300 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
301
302 # Response for bad request
303 defp bad_request(%Plug.Conn{} = conn, _) do
304 render_error(conn, :internal_server_error, "Bad request")
305 end
306
307 @doc "Prepares OAuth request to provider for Ueberauth"
308 def prepare_request(%Plug.Conn{} = conn, %{
309 "provider" => provider,
310 "authorization" => auth_attrs
311 }) do
312 scope =
313 auth_attrs
314 |> Scopes.fetch_scopes([])
315 |> Scopes.to_string()
316
317 state =
318 auth_attrs
319 |> Map.delete("scopes")
320 |> Map.put("scope", scope)
321 |> Jason.encode!()
322
323 params =
324 auth_attrs
325 |> Map.drop(~w(scope scopes client_id redirect_uri))
326 |> Map.put("state", state)
327
328 # Handing the request to Ueberauth
329 redirect(conn, to: o_auth_path(conn, :request, provider, params))
330 end
331
332 def request(%Plug.Conn{} = conn, params) do
333 message =
334 if params["provider"] do
335 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
336 provider: params["provider"]
337 )
338 else
339 dgettext("errors", "Bad OAuth request.")
340 end
341
342 conn
343 |> put_flash(:error, message)
344 |> redirect(to: "/")
345 end
346
347 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
348 params = callback_params(params)
349 messages = for e <- Map.get(failure, :errors, []), do: e.message
350 message = Enum.join(messages, "; ")
351
352 conn
353 |> put_flash(
354 :error,
355 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
356 )
357 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
358 end
359
360 def callback(%Plug.Conn{} = conn, params) do
361 params = callback_params(params)
362
363 with {:ok, registration} <- Authenticator.get_registration(conn) do
364 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
365
366 case Repo.get_assoc(registration, :user) do
367 {:ok, user} ->
368 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
369
370 _ ->
371 registration_params =
372 Map.merge(auth_attrs, %{
373 "nickname" => Registration.nickname(registration),
374 "email" => Registration.email(registration)
375 })
376
377 conn
378 |> put_session_registration_id(registration.id)
379 |> registration_details(%{"authorization" => registration_params})
380 end
381 else
382 error ->
383 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
384
385 conn
386 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
387 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
388 end
389 end
390
391 defp callback_params(%{"state" => state} = params) do
392 Map.merge(params, Jason.decode!(state))
393 end
394
395 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
396 render(conn, "register.html", %{
397 client_id: auth_attrs["client_id"],
398 redirect_uri: auth_attrs["redirect_uri"],
399 state: auth_attrs["state"],
400 scopes: Scopes.fetch_scopes(auth_attrs, []),
401 nickname: auth_attrs["nickname"],
402 email: auth_attrs["email"]
403 })
404 end
405
406 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
407 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
408 %Registration{} = registration <- Repo.get(Registration, registration_id),
409 {_, {:ok, auth}} <- {:create_authorization, do_create_authorization(conn, params)},
410 %User{} = user <- Repo.preload(auth, :user).user,
411 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
412 conn
413 |> put_session_registration_id(nil)
414 |> after_create_authorization(auth, params)
415 else
416 {:create_authorization, error} ->
417 {:register, handle_create_authorization_error(conn, error, params)}
418
419 _ ->
420 {:register, :generic_error}
421 end
422 end
423
424 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
425 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
426 %Registration{} = registration <- Repo.get(Registration, registration_id),
427 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
428 conn
429 |> put_session_registration_id(nil)
430 |> create_authorization(
431 params,
432 user: user
433 )
434 else
435 {:error, changeset} ->
436 message =
437 Enum.map(changeset.errors, fn {field, {error, _}} ->
438 "#{field} #{error}"
439 end)
440 |> Enum.join("; ")
441
442 message =
443 String.replace(
444 message,
445 "ap_id has already been taken",
446 "nickname has already been taken"
447 )
448
449 conn
450 |> put_status(:forbidden)
451 |> put_flash(:error, "Error: #{message}.")
452 |> registration_details(params)
453
454 _ ->
455 {:register, :generic_error}
456 end
457 end
458
459 defp do_create_authorization(
460 %Plug.Conn{} = conn,
461 %{
462 "authorization" =>
463 %{
464 "client_id" => client_id,
465 "redirect_uri" => redirect_uri
466 } = auth_attrs
467 },
468 user \\ nil
469 ) do
470 with {_, {:ok, %User{} = user}} <-
471 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
472 %App{} = app <- Repo.get_by(App, client_id: client_id),
473 true <- redirect_uri in String.split(app.redirect_uris),
474 {:ok, scopes} <- validate_scopes(app, auth_attrs),
475 {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do
476 Authorization.create_authorization(app, user, scopes)
477 end
478 end
479
480 # Special case: Local MastodonFE
481 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
482
483 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
484
485 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
486
487 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
488 do: put_session(conn, :registration_id, registration_id)
489
490 @spec validate_scopes(App.t(), map()) ::
491 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
492 defp validate_scopes(app, params) do
493 params
494 |> Scopes.fetch_scopes(app.scopes)
495 |> Scopes.validate(app.scopes)
496 end
497
498 def default_redirect_uri(%App{} = app) do
499 app.redirect_uris
500 |> String.split()
501 |> Enum.at(0)
502 end
503
504 defp render_invalid_credentials_error(conn) do
505 render_error(conn, :bad_request, "Invalid credentials")
506 end
507 end