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