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