First
[anni] / test / pleroma / web / o_auth / o_auth_controller_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.OAuth.OAuthControllerTest do
6   use Pleroma.Web.ConnCase
7
8   import Pleroma.Factory
9
10   alias Pleroma.Helpers.AuthHelper
11   alias Pleroma.MFA
12   alias Pleroma.MFA.TOTP
13   alias Pleroma.Repo
14   alias Pleroma.User
15   alias Pleroma.Web.OAuth.Authorization
16   alias Pleroma.Web.OAuth.OAuthController
17   alias Pleroma.Web.OAuth.Token
18
19   @session_opts [
20     store: :cookie,
21     key: "_test",
22     signing_salt: "cooldude"
23   ]
24   setup do
25     clear_config([:instance, :account_activation_required])
26     clear_config([:instance, :account_approval_required])
27   end
28
29   describe "in OAuth consumer mode, " do
30     setup do
31       [
32         app: insert(:oauth_app),
33         conn:
34           build_conn()
35           |> Plug.Session.call(Plug.Session.init(@session_opts))
36           |> fetch_session()
37       ]
38     end
39
40     setup do: clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook))
41
42     test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{
43       app: app,
44       conn: conn
45     } do
46       conn =
47         get(
48           conn,
49           "/oauth/authorize",
50           %{
51             "response_type" => "code",
52             "client_id" => app.client_id,
53             "redirect_uri" => OAuthController.default_redirect_uri(app),
54             "scope" => "read"
55           }
56         )
57
58       assert response = html_response(conn, 200)
59       assert response =~ "Sign in with Twitter"
60       assert response =~ o_auth_path(conn, :prepare_request)
61     end
62
63     test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{
64       app: app,
65       conn: conn
66     } do
67       conn =
68         get(
69           conn,
70           "/oauth/prepare_request",
71           %{
72             "provider" => "twitter",
73             "authorization" => %{
74               "scope" => "read follow",
75               "client_id" => app.client_id,
76               "redirect_uri" => OAuthController.default_redirect_uri(app),
77               "state" => "a_state"
78             }
79           }
80         )
81
82       assert html_response(conn, 302)
83
84       redirect_query = URI.parse(redirected_to(conn)).query
85       assert %{"state" => state_param} = URI.decode_query(redirect_query)
86       assert {:ok, state_components} = Jason.decode(state_param)
87
88       expected_client_id = app.client_id
89       expected_redirect_uri = app.redirect_uris
90
91       assert %{
92                "scope" => "read follow",
93                "client_id" => ^expected_client_id,
94                "redirect_uri" => ^expected_redirect_uri,
95                "state" => "a_state"
96              } = state_components
97     end
98
99     test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`",
100          %{app: app, conn: conn} do
101       registration = insert(:registration)
102       redirect_uri = OAuthController.default_redirect_uri(app)
103
104       state_params = %{
105         "scope" => Enum.join(app.scopes, " "),
106         "client_id" => app.client_id,
107         "redirect_uri" => redirect_uri,
108         "state" => ""
109       }
110
111       conn =
112         conn
113         |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid})
114         |> get(
115           "/oauth/twitter/callback",
116           %{
117             "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
118             "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
119             "provider" => "twitter",
120             "state" => Jason.encode!(state_params)
121           }
122         )
123
124       assert html_response(conn, 302)
125       assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
126     end
127
128     test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page",
129          %{app: app, conn: conn} do
130       user = insert(:user)
131
132       state_params = %{
133         "scope" => "read write",
134         "client_id" => app.client_id,
135         "redirect_uri" => OAuthController.default_redirect_uri(app),
136         "state" => "a_state"
137       }
138
139       conn =
140         conn
141         |> assign(:ueberauth_auth, %{
142           provider: "twitter",
143           uid: "171799000",
144           info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil}
145         })
146         |> get(
147           "/oauth/twitter/callback",
148           %{
149             "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
150             "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
151             "provider" => "twitter",
152             "state" => Jason.encode!(state_params)
153           }
154         )
155
156       assert response = html_response(conn, 200)
157       assert response =~ ~r/name="op" type="submit" value="register"/
158       assert response =~ ~r/name="op" type="submit" value="connect"/
159       assert response =~ user.email
160       assert response =~ user.nickname
161     end
162
163     test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{
164       app: app,
165       conn: conn
166     } do
167       state_params = %{
168         "scope" => Enum.join(app.scopes, " "),
169         "client_id" => app.client_id,
170         "redirect_uri" => OAuthController.default_redirect_uri(app),
171         "state" => ""
172       }
173
174       conn =
175         conn
176         |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]})
177         |> get(
178           "/oauth/twitter/callback",
179           %{
180             "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
181             "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
182             "provider" => "twitter",
183             "state" => Jason.encode!(state_params)
184           }
185         )
186
187       assert html_response(conn, 302)
188       assert redirected_to(conn) == app.redirect_uris
189       assert get_flash(conn, :error) == "Failed to authenticate: (error description)."
190     end
191
192     test "GET /oauth/registration_details renders registration details form", %{
193       app: app,
194       conn: conn
195     } do
196       conn =
197         get(
198           conn,
199           "/oauth/registration_details",
200           %{
201             "authorization" => %{
202               "scopes" => app.scopes,
203               "client_id" => app.client_id,
204               "redirect_uri" => OAuthController.default_redirect_uri(app),
205               "state" => "a_state",
206               "nickname" => nil,
207               "email" => "john@doe.com"
208             }
209           }
210         )
211
212       assert response = html_response(conn, 200)
213       assert response =~ ~r/name="op" type="submit" value="register"/
214       assert response =~ ~r/name="op" type="submit" value="connect"/
215     end
216
217     test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`",
218          %{
219            app: app,
220            conn: conn
221          } do
222       registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
223       redirect_uri = OAuthController.default_redirect_uri(app)
224
225       conn =
226         conn
227         |> put_session(:registration_id, registration.id)
228         |> post(
229           "/oauth/register",
230           %{
231             "op" => "register",
232             "authorization" => %{
233               "scopes" => app.scopes,
234               "client_id" => app.client_id,
235               "redirect_uri" => redirect_uri,
236               "state" => "a_state",
237               "nickname" => "availablenick",
238               "email" => "available@email.com"
239             }
240           }
241         )
242
243       assert html_response(conn, 302)
244       assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
245     end
246
247     test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401",
248          %{
249            app: app,
250            conn: conn
251          } do
252       registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
253       unlisted_redirect_uri = "http://cross-site-request.com"
254
255       conn =
256         conn
257         |> put_session(:registration_id, registration.id)
258         |> post(
259           "/oauth/register",
260           %{
261             "op" => "register",
262             "authorization" => %{
263               "scopes" => app.scopes,
264               "client_id" => app.client_id,
265               "redirect_uri" => unlisted_redirect_uri,
266               "state" => "a_state",
267               "nickname" => "availablenick",
268               "email" => "available@email.com"
269             }
270           }
271         )
272
273       assert html_response(conn, 401)
274     end
275
276     test "with invalid params, POST /oauth/register?op=register renders registration_details page",
277          %{
278            app: app,
279            conn: conn
280          } do
281       another_user = insert(:user)
282       registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
283
284       params = %{
285         "op" => "register",
286         "authorization" => %{
287           "scopes" => app.scopes,
288           "client_id" => app.client_id,
289           "redirect_uri" => OAuthController.default_redirect_uri(app),
290           "state" => "a_state",
291           "nickname" => "availablenickname",
292           "email" => "available@email.com"
293         }
294       }
295
296       for {bad_param, bad_param_value} <-
297             [{"nickname", another_user.nickname}, {"email", another_user.email}] do
298         bad_registration_attrs = %{
299           "authorization" => Map.put(params["authorization"], bad_param, bad_param_value)
300         }
301
302         bad_params = Map.merge(params, bad_registration_attrs)
303
304         conn =
305           conn
306           |> put_session(:registration_id, registration.id)
307           |> post("/oauth/register", bad_params)
308
309         assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/
310         assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken."
311       end
312     end
313
314     test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`",
315          %{
316            app: app,
317            conn: conn
318          } do
319       user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword"))
320       registration = insert(:registration, user: nil)
321       redirect_uri = OAuthController.default_redirect_uri(app)
322
323       conn =
324         conn
325         |> put_session(:registration_id, registration.id)
326         |> post(
327           "/oauth/register",
328           %{
329             "op" => "connect",
330             "authorization" => %{
331               "scopes" => app.scopes,
332               "client_id" => app.client_id,
333               "redirect_uri" => redirect_uri,
334               "state" => "a_state",
335               "name" => user.nickname,
336               "password" => "testpassword"
337             }
338           }
339         )
340
341       assert html_response(conn, 302)
342       assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
343     end
344
345     test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`",
346          %{
347            app: app,
348            conn: conn
349          } do
350       user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword"))
351       registration = insert(:registration, user: nil)
352       unlisted_redirect_uri = "http://cross-site-request.com"
353
354       conn =
355         conn
356         |> put_session(:registration_id, registration.id)
357         |> post(
358           "/oauth/register",
359           %{
360             "op" => "connect",
361             "authorization" => %{
362               "scopes" => app.scopes,
363               "client_id" => app.client_id,
364               "redirect_uri" => unlisted_redirect_uri,
365               "state" => "a_state",
366               "name" => user.nickname,
367               "password" => "testpassword"
368             }
369           }
370         )
371
372       assert html_response(conn, 401)
373     end
374
375     test "with invalid params, POST /oauth/register?op=connect renders registration_details page",
376          %{
377            app: app,
378            conn: conn
379          } do
380       user = insert(:user)
381       registration = insert(:registration, user: nil)
382
383       params = %{
384         "op" => "connect",
385         "authorization" => %{
386           "scopes" => app.scopes,
387           "client_id" => app.client_id,
388           "redirect_uri" => OAuthController.default_redirect_uri(app),
389           "state" => "a_state",
390           "name" => user.nickname,
391           "password" => "wrong password"
392         }
393       }
394
395       conn =
396         conn
397         |> put_session(:registration_id, registration.id)
398         |> post("/oauth/register", params)
399
400       assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/
401       assert get_flash(conn, :error) == "Invalid Username/Password"
402     end
403   end
404
405   describe "GET /oauth/authorize" do
406     setup do
407       [
408         app: insert(:oauth_app, redirect_uris: "https://redirect.url"),
409         conn:
410           build_conn()
411           |> Plug.Session.call(Plug.Session.init(@session_opts))
412           |> fetch_session()
413       ]
414     end
415
416     test "renders authentication page", %{app: app, conn: conn} do
417       conn =
418         get(
419           conn,
420           "/oauth/authorize",
421           %{
422             "response_type" => "code",
423             "client_id" => app.client_id,
424             "redirect_uri" => OAuthController.default_redirect_uri(app),
425             "scope" => "read"
426           }
427         )
428
429       assert html_response(conn, 200) =~ ~s(type="submit")
430     end
431
432     test "properly handles internal calls with `authorization`-wrapped params", %{
433       app: app,
434       conn: conn
435     } do
436       conn =
437         get(
438           conn,
439           "/oauth/authorize",
440           %{
441             "authorization" => %{
442               "response_type" => "code",
443               "client_id" => app.client_id,
444               "redirect_uri" => OAuthController.default_redirect_uri(app),
445               "scope" => "read"
446             }
447           }
448         )
449
450       assert html_response(conn, 200) =~ ~s(type="submit")
451     end
452
453     test "renders authentication page if user is already authenticated but `force_login` is tru-ish",
454          %{app: app, conn: conn} do
455       token = insert(:oauth_token, app: app)
456
457       conn =
458         conn
459         |> AuthHelper.put_session_token(token.token)
460         |> get(
461           "/oauth/authorize",
462           %{
463             "response_type" => "code",
464             "client_id" => app.client_id,
465             "redirect_uri" => OAuthController.default_redirect_uri(app),
466             "scope" => "read",
467             "force_login" => "true"
468           }
469         )
470
471       assert html_response(conn, 200) =~ ~s(type="submit")
472     end
473
474     test "renders authentication page if user is already authenticated but user request with another client",
475          %{
476            app: app,
477            conn: conn
478          } do
479       token = insert(:oauth_token, app: app)
480
481       conn =
482         conn
483         |> AuthHelper.put_session_token(token.token)
484         |> get(
485           "/oauth/authorize",
486           %{
487             "response_type" => "code",
488             "client_id" => "another_client_id",
489             "redirect_uri" => OAuthController.default_redirect_uri(app),
490             "scope" => "read"
491           }
492         )
493
494       assert html_response(conn, 200) =~ ~s(type="submit")
495     end
496
497     test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
498          %{
499            app: app,
500            conn: conn
501          } do
502       token = insert(:oauth_token, app: app)
503
504       conn =
505         conn
506         |> AuthHelper.put_session_token(token.token)
507         |> get(
508           "/oauth/authorize",
509           %{
510             "response_type" => "code",
511             "client_id" => app.client_id,
512             "redirect_uri" => OAuthController.default_redirect_uri(app),
513             "state" => "specific_client_state",
514             "scope" => "read"
515           }
516         )
517
518       assert URI.decode(redirected_to(conn)) ==
519                "https://redirect.url?access_token=#{token.token}&state=specific_client_state"
520     end
521
522     test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials",
523          %{
524            app: app,
525            conn: conn
526          } do
527       unlisted_redirect_uri = "http://cross-site-request.com"
528       token = insert(:oauth_token, app: app)
529
530       conn =
531         conn
532         |> AuthHelper.put_session_token(token.token)
533         |> get(
534           "/oauth/authorize",
535           %{
536             "response_type" => "code",
537             "client_id" => app.client_id,
538             "redirect_uri" => unlisted_redirect_uri,
539             "state" => "specific_client_state",
540             "scope" => "read"
541           }
542         )
543
544       assert redirected_to(conn) == unlisted_redirect_uri
545     end
546
547     test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params",
548          %{
549            app: app,
550            conn: conn
551          } do
552       token = insert(:oauth_token, app: app)
553
554       conn =
555         conn
556         |> AuthHelper.put_session_token(token.token)
557         |> get(
558           "/oauth/authorize",
559           %{
560             "response_type" => "code",
561             "client_id" => app.client_id,
562             "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
563             "scope" => "read"
564           }
565         )
566
567       assert html_response(conn, 200) =~ "Authorization exists"
568     end
569   end
570
571   describe "POST /oauth/authorize" do
572     test "redirects with oauth authorization, " <>
573            "granting requested app-supported scopes to both admin- and non-admin users" do
574       app_scopes = ["read", "write", "admin", "secret_scope"]
575       app = insert(:oauth_app, scopes: app_scopes)
576       redirect_uri = OAuthController.default_redirect_uri(app)
577
578       non_admin = insert(:user, is_admin: false)
579       admin = insert(:user, is_admin: true)
580       scopes_subset = ["read:subscope", "write", "admin"]
581
582       # In case scope param is missing, expecting _all_ app-supported scopes to be granted
583       for user <- [non_admin, admin],
584           {requested_scopes, expected_scopes} <-
585             %{scopes_subset => scopes_subset, nil: app_scopes} do
586         conn =
587           post(
588             build_conn(),
589             "/oauth/authorize",
590             %{
591               "authorization" => %{
592                 "name" => user.nickname,
593                 "password" => "test",
594                 "client_id" => app.client_id,
595                 "redirect_uri" => redirect_uri,
596                 "scope" => requested_scopes,
597                 "state" => "statepassed"
598               }
599             }
600           )
601
602         target = redirected_to(conn)
603         assert target =~ redirect_uri
604
605         query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
606
607         assert %{"state" => "statepassed", "code" => code} = query
608         auth = Repo.get_by(Authorization, token: code)
609         assert auth
610         assert auth.scopes == expected_scopes
611       end
612     end
613
614     test "authorize from cookie" do
615       user = insert(:user)
616       app = insert(:oauth_app)
617       oauth_token = insert(:oauth_token, user: user, app: app)
618       redirect_uri = OAuthController.default_redirect_uri(app)
619
620       conn =
621         build_conn()
622         |> Plug.Session.call(Plug.Session.init(@session_opts))
623         |> fetch_session()
624         |> AuthHelper.put_session_token(oauth_token.token)
625         |> post(
626           "/oauth/authorize",
627           %{
628             "authorization" => %{
629               "name" => user.nickname,
630               "client_id" => app.client_id,
631               "redirect_uri" => redirect_uri,
632               "scope" => app.scopes,
633               "state" => "statepassed"
634             }
635           }
636         )
637
638       target = redirected_to(conn)
639       assert target =~ redirect_uri
640
641       query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
642
643       assert %{"state" => "statepassed", "code" => code} = query
644       auth = Repo.get_by(Authorization, token: code)
645       assert auth
646       assert auth.scopes == app.scopes
647     end
648
649     test "redirect to on two-factor auth page" do
650       otp_secret = TOTP.generate_secret()
651
652       user =
653         insert(:user,
654           multi_factor_authentication_settings: %MFA.Settings{
655             enabled: true,
656             totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
657           }
658         )
659
660       app = insert(:oauth_app, scopes: ["read", "write", "follow"])
661
662       conn =
663         build_conn()
664         |> post("/oauth/authorize", %{
665           "authorization" => %{
666             "name" => user.nickname,
667             "password" => "test",
668             "client_id" => app.client_id,
669             "redirect_uri" => app.redirect_uris,
670             "scope" => "read write",
671             "state" => "statepassed"
672           }
673         })
674
675       result = html_response(conn, 200)
676
677       mfa_token = Repo.get_by(MFA.Token, user_id: user.id)
678       assert result =~ app.redirect_uris
679       assert result =~ "statepassed"
680       assert result =~ mfa_token.token
681       assert result =~ "Two-factor authentication"
682     end
683
684     test "returns 401 for wrong credentials", %{conn: conn} do
685       user = insert(:user)
686       app = insert(:oauth_app)
687       redirect_uri = OAuthController.default_redirect_uri(app)
688
689       result =
690         conn
691         |> post("/oauth/authorize", %{
692           "authorization" => %{
693             "name" => user.nickname,
694             "password" => "wrong",
695             "client_id" => app.client_id,
696             "redirect_uri" => redirect_uri,
697             "state" => "statepassed",
698             "scope" => Enum.join(app.scopes, " ")
699           }
700         })
701         |> html_response(:unauthorized)
702
703       # Keep the details
704       assert result =~ app.client_id
705       assert result =~ redirect_uri
706
707       # Error message
708       assert result =~ "Invalid Username/Password"
709     end
710
711     test "returns 401 for missing scopes" do
712       user = insert(:user, is_admin: false)
713       app = insert(:oauth_app, scopes: ["read", "write", "admin"])
714       redirect_uri = OAuthController.default_redirect_uri(app)
715
716       result =
717         build_conn()
718         |> post("/oauth/authorize", %{
719           "authorization" => %{
720             "name" => user.nickname,
721             "password" => "test",
722             "client_id" => app.client_id,
723             "redirect_uri" => redirect_uri,
724             "state" => "statepassed",
725             "scope" => ""
726           }
727         })
728         |> html_response(:unauthorized)
729
730       # Keep the details
731       assert result =~ app.client_id
732       assert result =~ redirect_uri
733
734       # Error message
735       assert result =~ "This action is outside the authorized scopes"
736     end
737
738     test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
739       user = insert(:user)
740       app = insert(:oauth_app, scopes: ["read", "write"])
741       redirect_uri = OAuthController.default_redirect_uri(app)
742
743       result =
744         conn
745         |> post("/oauth/authorize", %{
746           "authorization" => %{
747             "name" => user.nickname,
748             "password" => "test",
749             "client_id" => app.client_id,
750             "redirect_uri" => redirect_uri,
751             "state" => "statepassed",
752             "scope" => "read write follow"
753           }
754         })
755         |> html_response(:unauthorized)
756
757       # Keep the details
758       assert result =~ app.client_id
759       assert result =~ redirect_uri
760
761       # Error message
762       assert result =~ "This action is outside the authorized scopes"
763     end
764   end
765
766   describe "POST /oauth/token" do
767     test "issues a token for an all-body request" do
768       user = insert(:user)
769       app = insert(:oauth_app, scopes: ["read", "write"])
770
771       {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
772
773       conn =
774         build_conn()
775         |> post("/oauth/token", %{
776           "grant_type" => "authorization_code",
777           "code" => auth.token,
778           "redirect_uri" => OAuthController.default_redirect_uri(app),
779           "client_id" => app.client_id,
780           "client_secret" => app.client_secret
781         })
782
783       assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200)
784
785       token = Repo.get_by(Token, token: token)
786       assert token
787       assert token.scopes == auth.scopes
788       assert user.ap_id == ap_id
789     end
790
791     test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do
792       password = "testpassword"
793       user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
794
795       app = insert(:oauth_app, scopes: ["read", "write"])
796
797       # Note: "scope" param is intentionally omitted
798       conn =
799         build_conn()
800         |> post("/oauth/token", %{
801           "grant_type" => "password",
802           "username" => user.nickname,
803           "password" => password,
804           "client_id" => app.client_id,
805           "client_secret" => app.client_secret
806         })
807
808       assert %{"id" => id, "access_token" => access_token} = json_response(conn, 200)
809
810       token = Repo.get_by(Token, token: access_token)
811       assert token
812       assert token.id == id
813       assert token.token == access_token
814       assert token.scopes == app.scopes
815     end
816
817     test "issues a mfa token for `password` grant_type, when MFA enabled" do
818       password = "testpassword"
819       otp_secret = TOTP.generate_secret()
820
821       user =
822         insert(:user,
823           password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
824           multi_factor_authentication_settings: %MFA.Settings{
825             enabled: true,
826             totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
827           }
828         )
829
830       app = insert(:oauth_app, scopes: ["read", "write"])
831
832       response =
833         build_conn()
834         |> post("/oauth/token", %{
835           "grant_type" => "password",
836           "username" => user.nickname,
837           "password" => password,
838           "client_id" => app.client_id,
839           "client_secret" => app.client_secret
840         })
841         |> json_response(403)
842
843       assert match?(
844                %{
845                  "supported_challenge_types" => "totp",
846                  "mfa_token" => _,
847                  "error" => "mfa_required"
848                },
849                response
850              )
851
852       token = Repo.get_by(MFA.Token, token: response["mfa_token"])
853       assert token.user_id == user.id
854       assert token.authorization_id
855     end
856
857     test "issues a token for request with HTTP basic auth client credentials" do
858       user = insert(:user)
859       app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"])
860
861       {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"])
862       assert auth.scopes == ["scope1", "scope2"]
863
864       app_encoded =
865         (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret))
866         |> Base.encode64()
867
868       conn =
869         build_conn()
870         |> put_req_header("authorization", "Basic " <> app_encoded)
871         |> post("/oauth/token", %{
872           "grant_type" => "authorization_code",
873           "code" => auth.token,
874           "redirect_uri" => OAuthController.default_redirect_uri(app)
875         })
876
877       assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200)
878
879       assert scope == "scope1 scope2"
880
881       token = Repo.get_by(Token, token: token)
882       assert token
883       assert token.scopes == ["scope1", "scope2"]
884     end
885
886     test "issue a token for client_credentials grant type" do
887       app = insert(:oauth_app, scopes: ["read", "write"])
888
889       conn =
890         build_conn()
891         |> post("/oauth/token", %{
892           "grant_type" => "client_credentials",
893           "client_id" => app.client_id,
894           "client_secret" => app.client_secret
895         })
896
897       assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
898                json_response(conn, 200)
899
900       assert token
901       token_from_db = Repo.get_by(Token, token: token)
902       assert token_from_db
903       assert refresh
904       assert scope == "read write"
905     end
906
907     test "rejects token exchange with invalid client credentials" do
908       user = insert(:user)
909       app = insert(:oauth_app)
910
911       {:ok, auth} = Authorization.create_authorization(app, user)
912
913       conn =
914         build_conn()
915         |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=")
916         |> post("/oauth/token", %{
917           "grant_type" => "authorization_code",
918           "code" => auth.token,
919           "redirect_uri" => OAuthController.default_redirect_uri(app)
920         })
921
922       assert resp = json_response(conn, 400)
923       assert %{"error" => _} = resp
924       refute Map.has_key?(resp, "access_token")
925     end
926
927     test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
928       clear_config([:instance, :account_activation_required], true)
929       password = "testpassword"
930
931       {:ok, user} =
932         insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
933         |> User.confirmation_changeset(set_confirmation: false)
934         |> User.update_and_set_cache()
935
936       refute Pleroma.User.account_status(user) == :active
937
938       app = insert(:oauth_app)
939
940       conn =
941         build_conn()
942         |> post("/oauth/token", %{
943           "grant_type" => "password",
944           "username" => user.nickname,
945           "password" => password,
946           "client_id" => app.client_id,
947           "client_secret" => app.client_secret
948         })
949
950       assert resp = json_response(conn, 403)
951       assert %{"error" => _} = resp
952       refute Map.has_key?(resp, "access_token")
953     end
954
955     test "rejects token exchange for valid credentials belonging to deactivated user" do
956       password = "testpassword"
957
958       user =
959         insert(:user,
960           password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
961           is_active: false
962         )
963
964       app = insert(:oauth_app)
965
966       resp =
967         build_conn()
968         |> post("/oauth/token", %{
969           "grant_type" => "password",
970           "username" => user.nickname,
971           "password" => password,
972           "client_id" => app.client_id,
973           "client_secret" => app.client_secret
974         })
975         |> json_response(403)
976
977       assert resp == %{
978                "error" => "Your account is currently disabled",
979                "identifier" => "account_is_disabled"
980              }
981     end
982
983     test "rejects token exchange for user with password_reset_pending set to true" do
984       password = "testpassword"
985
986       user =
987         insert(:user,
988           password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
989           password_reset_pending: true
990         )
991
992       app = insert(:oauth_app, scopes: ["read", "write"])
993
994       resp =
995         build_conn()
996         |> post("/oauth/token", %{
997           "grant_type" => "password",
998           "username" => user.nickname,
999           "password" => password,
1000           "client_id" => app.client_id,
1001           "client_secret" => app.client_secret
1002         })
1003         |> json_response(403)
1004
1005       assert resp == %{
1006                "error" => "Password reset is required",
1007                "identifier" => "password_reset_required"
1008              }
1009     end
1010
1011     test "rejects token exchange for user with confirmation_pending set to true" do
1012       clear_config([:instance, :account_activation_required], true)
1013       password = "testpassword"
1014
1015       user =
1016         insert(:user,
1017           password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
1018           is_confirmed: false
1019         )
1020
1021       app = insert(:oauth_app, scopes: ["read", "write"])
1022
1023       resp =
1024         build_conn()
1025         |> post("/oauth/token", %{
1026           "grant_type" => "password",
1027           "username" => user.nickname,
1028           "password" => password,
1029           "client_id" => app.client_id,
1030           "client_secret" => app.client_secret
1031         })
1032         |> json_response(403)
1033
1034       assert resp == %{
1035                "error" => "Your login is missing a confirmed e-mail address",
1036                "identifier" => "missing_confirmed_email"
1037              }
1038     end
1039
1040     test "rejects token exchange for valid credentials belonging to an unapproved user" do
1041       password = "testpassword"
1042
1043       user =
1044         insert(:user,
1045           password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
1046           is_approved: false
1047         )
1048
1049       refute Pleroma.User.account_status(user) == :active
1050
1051       app = insert(:oauth_app)
1052
1053       conn =
1054         build_conn()
1055         |> post("/oauth/token", %{
1056           "grant_type" => "password",
1057           "username" => user.nickname,
1058           "password" => password,
1059           "client_id" => app.client_id,
1060           "client_secret" => app.client_secret
1061         })
1062
1063       assert resp = json_response(conn, 403)
1064       assert %{"error" => _} = resp
1065       refute Map.has_key?(resp, "access_token")
1066     end
1067
1068     test "rejects an invalid authorization code" do
1069       app = insert(:oauth_app)
1070
1071       conn =
1072         build_conn()
1073         |> post("/oauth/token", %{
1074           "grant_type" => "authorization_code",
1075           "code" => "Imobviouslyinvalid",
1076           "redirect_uri" => OAuthController.default_redirect_uri(app),
1077           "client_id" => app.client_id,
1078           "client_secret" => app.client_secret
1079         })
1080
1081       assert resp = json_response(conn, 400)
1082       assert %{"error" => _} = json_response(conn, 400)
1083       refute Map.has_key?(resp, "access_token")
1084     end
1085   end
1086
1087   describe "POST /oauth/token - refresh token" do
1088     setup do: clear_config([:oauth2, :issue_new_refresh_token])
1089
1090     test "issues a new access token with keep fresh token" do
1091       clear_config([:oauth2, :issue_new_refresh_token], true)
1092       user = insert(:user)
1093       app = insert(:oauth_app, scopes: ["read", "write"])
1094
1095       {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
1096       {:ok, token} = Token.exchange_token(app, auth)
1097
1098       response =
1099         build_conn()
1100         |> post("/oauth/token", %{
1101           "grant_type" => "refresh_token",
1102           "refresh_token" => token.refresh_token,
1103           "client_id" => app.client_id,
1104           "client_secret" => app.client_secret
1105         })
1106         |> json_response(200)
1107
1108       ap_id = user.ap_id
1109
1110       assert match?(
1111                %{
1112                  "scope" => "write",
1113                  "token_type" => "Bearer",
1114                  "access_token" => _,
1115                  "refresh_token" => _,
1116                  "me" => ^ap_id
1117                },
1118                response
1119              )
1120
1121       refute Repo.get_by(Token, token: token.token)
1122       new_token = Repo.get_by(Token, token: response["access_token"])
1123       assert new_token.refresh_token == token.refresh_token
1124       assert new_token.scopes == auth.scopes
1125       assert new_token.user_id == user.id
1126       assert new_token.app_id == app.id
1127     end
1128
1129     test "issues a new access token with new fresh token" do
1130       clear_config([:oauth2, :issue_new_refresh_token], false)
1131       user = insert(:user)
1132       app = insert(:oauth_app, scopes: ["read", "write"])
1133
1134       {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
1135       {:ok, token} = Token.exchange_token(app, auth)
1136
1137       response =
1138         build_conn()
1139         |> post("/oauth/token", %{
1140           "grant_type" => "refresh_token",
1141           "refresh_token" => token.refresh_token,
1142           "client_id" => app.client_id,
1143           "client_secret" => app.client_secret
1144         })
1145         |> json_response(200)
1146
1147       ap_id = user.ap_id
1148
1149       assert match?(
1150                %{
1151                  "scope" => "write",
1152                  "token_type" => "Bearer",
1153                  "access_token" => _,
1154                  "refresh_token" => _,
1155                  "me" => ^ap_id
1156                },
1157                response
1158              )
1159
1160       refute Repo.get_by(Token, token: token.token)
1161       new_token = Repo.get_by(Token, token: response["access_token"])
1162       refute new_token.refresh_token == token.refresh_token
1163       assert new_token.scopes == auth.scopes
1164       assert new_token.user_id == user.id
1165       assert new_token.app_id == app.id
1166     end
1167
1168     test "returns 400 if we try use access token" do
1169       user = insert(:user)
1170       app = insert(:oauth_app, scopes: ["read", "write"])
1171
1172       {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
1173       {:ok, token} = Token.exchange_token(app, auth)
1174
1175       response =
1176         build_conn()
1177         |> post("/oauth/token", %{
1178           "grant_type" => "refresh_token",
1179           "refresh_token" => token.token,
1180           "client_id" => app.client_id,
1181           "client_secret" => app.client_secret
1182         })
1183         |> json_response(400)
1184
1185       assert %{"error" => "Invalid credentials"} == response
1186     end
1187
1188     test "returns 400 if refresh_token invalid" do
1189       app = insert(:oauth_app, scopes: ["read", "write"])
1190
1191       response =
1192         build_conn()
1193         |> post("/oauth/token", %{
1194           "grant_type" => "refresh_token",
1195           "refresh_token" => "token.refresh_token",
1196           "client_id" => app.client_id,
1197           "client_secret" => app.client_secret
1198         })
1199         |> json_response(400)
1200
1201       assert %{"error" => "Invalid credentials"} == response
1202     end
1203
1204     test "issues a new token if token expired" do
1205       user = insert(:user)
1206       app = insert(:oauth_app, scopes: ["read", "write"])
1207
1208       {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
1209       {:ok, token} = Token.exchange_token(app, auth)
1210
1211       change =
1212         Ecto.Changeset.change(
1213           token,
1214           %{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)}
1215         )
1216
1217       {:ok, access_token} = Repo.update(change)
1218
1219       response =
1220         build_conn()
1221         |> post("/oauth/token", %{
1222           "grant_type" => "refresh_token",
1223           "refresh_token" => access_token.refresh_token,
1224           "client_id" => app.client_id,
1225           "client_secret" => app.client_secret
1226         })
1227         |> json_response(200)
1228
1229       ap_id = user.ap_id
1230
1231       assert match?(
1232                %{
1233                  "scope" => "write",
1234                  "token_type" => "Bearer",
1235                  "access_token" => _,
1236                  "refresh_token" => _,
1237                  "me" => ^ap_id
1238                },
1239                response
1240              )
1241
1242       refute Repo.get_by(Token, token: token.token)
1243       token = Repo.get_by(Token, token: response["access_token"])
1244       assert token
1245       assert token.scopes == auth.scopes
1246       assert token.user_id == user.id
1247       assert token.app_id == app.id
1248     end
1249   end
1250
1251   describe "POST /oauth/token - bad request" do
1252     test "returns 500" do
1253       response =
1254         build_conn()
1255         |> post("/oauth/token", %{})
1256         |> json_response(500)
1257
1258       assert %{"error" => "Bad request"} == response
1259     end
1260   end
1261
1262   describe "POST /oauth/revoke" do
1263     test "when authenticated with request token, revokes it and clears it from session" do
1264       oauth_token = insert(:oauth_token)
1265
1266       conn =
1267         build_conn()
1268         |> Plug.Session.call(Plug.Session.init(@session_opts))
1269         |> fetch_session()
1270         |> AuthHelper.put_session_token(oauth_token.token)
1271         |> post("/oauth/revoke", %{"token" => oauth_token.token})
1272
1273       assert json_response(conn, 200)
1274
1275       refute AuthHelper.get_session_token(conn)
1276       assert Token.get_by_token(oauth_token.token) == {:error, :not_found}
1277     end
1278
1279     test "if request is authenticated with a different token, " <>
1280            "revokes requested token but keeps session token" do
1281       user = insert(:user)
1282       oauth_token = insert(:oauth_token, user: user)
1283       other_app_oauth_token = insert(:oauth_token, user: user)
1284
1285       conn =
1286         build_conn()
1287         |> Plug.Session.call(Plug.Session.init(@session_opts))
1288         |> fetch_session()
1289         |> AuthHelper.put_session_token(oauth_token.token)
1290         |> post("/oauth/revoke", %{"token" => other_app_oauth_token.token})
1291
1292       assert json_response(conn, 200)
1293
1294       assert AuthHelper.get_session_token(conn) == oauth_token.token
1295       assert Token.get_by_token(other_app_oauth_token.token) == {:error, :not_found}
1296     end
1297
1298     test "returns 500 on bad request" do
1299       response =
1300         build_conn()
1301         |> post("/oauth/revoke", %{})
1302         |> json_response(500)
1303
1304       assert %{"error" => "Bad request"} == response
1305     end
1306   end
1307 end