1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.OAuth.OAuthControllerTest do
6 use Pleroma.Web.ConnCase
10 alias Pleroma.Registration
12 alias Pleroma.Web.OAuth.Authorization
13 alias Pleroma.Web.OAuth.Token
18 signing_salt: "cooldude"
21 describe "in OAuth consumer mode, " do
23 oauth_consumer_strategies_path = [:auth, :oauth_consumer_strategies]
24 oauth_consumer_strategies = Pleroma.Config.get(oauth_consumer_strategies_path)
25 Pleroma.Config.put(oauth_consumer_strategies_path, ~w(twitter facebook))
28 Pleroma.Config.put(oauth_consumer_strategies_path, oauth_consumer_strategies)
32 app: insert(:oauth_app),
35 |> Plug.Session.call(Plug.Session.init(@session_opts))
40 test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{
49 "response_type" => "code",
50 "client_id" => app.client_id,
51 "redirect_uri" => app.redirect_uris,
56 assert response = html_response(conn, 200)
57 assert response =~ "Sign in with Twitter"
58 assert response =~ o_auth_path(conn, :prepare_request)
61 test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{
68 "/oauth/prepare_request",
70 "provider" => "twitter",
71 "scope" => "read follow",
72 "client_id" => app.client_id,
73 "redirect_uri" => app.redirect_uris,
78 assert response = html_response(conn, 302)
80 redirect_query = URI.parse(redirected_to(conn)).query
81 assert %{"state" => state_param} = URI.decode_query(redirect_query)
82 assert {:ok, state_components} = Poison.decode(state_param)
84 expected_client_id = app.client_id
85 expected_redirect_uri = app.redirect_uris
88 "scope" => "read follow",
89 "client_id" => ^expected_client_id,
90 "redirect_uri" => ^expected_redirect_uri,
95 test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`",
96 %{app: app, conn: conn} do
97 registration = insert(:registration)
100 "scope" => Enum.join(app.scopes, " "),
101 "client_id" => app.client_id,
102 "redirect_uri" => app.redirect_uris,
106 with_mock Pleroma.Web.Auth.Authenticator,
107 get_registration: fn _, _ -> {:ok, registration} end do
111 "/oauth/twitter/callback",
113 "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
114 "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
115 "provider" => "twitter",
116 "state" => Poison.encode!(state_params)
120 assert response = html_response(conn, 302)
121 assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/
125 test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page",
126 %{app: app, conn: conn} do
127 registration = insert(:registration, user: nil)
130 "scope" => "read write",
131 "client_id" => app.client_id,
132 "redirect_uri" => app.redirect_uris,
136 with_mock Pleroma.Web.Auth.Authenticator,
137 get_registration: fn _, _ -> {:ok, registration} end do
141 "/oauth/twitter/callback",
143 "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
144 "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
145 "provider" => "twitter",
146 "state" => Poison.encode!(state_params)
150 assert response = html_response(conn, 200)
151 assert response =~ ~r/name="op" type="submit" value="register"/
152 assert response =~ ~r/name="op" type="submit" value="connect"/
153 assert response =~ Registration.email(registration)
154 assert response =~ Registration.nickname(registration)
158 test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{
163 "scope" => Enum.join(app.scopes, " "),
164 "client_id" => app.client_id,
165 "redirect_uri" => app.redirect_uris,
171 |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]})
173 "/oauth/twitter/callback",
175 "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
176 "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
177 "provider" => "twitter",
178 "state" => Poison.encode!(state_params)
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)."
187 test "GET /oauth/registration_details renders registration details form", %{
194 "/oauth/registration_details",
196 "scopes" => app.scopes,
197 "client_id" => app.client_id,
198 "redirect_uri" => app.redirect_uris,
199 "state" => "a_state",
201 "email" => "john@doe.com"
205 assert response = html_response(conn, 200)
206 assert response =~ ~r/name="op" type="submit" value="register"/
207 assert response =~ ~r/name="op" type="submit" value="connect"/
210 test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`",
215 registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
219 |> put_session(:registration_id, registration.id)
224 "scopes" => app.scopes,
225 "client_id" => app.client_id,
226 "redirect_uri" => app.redirect_uris,
227 "state" => "a_state",
228 "nickname" => "availablenick",
229 "email" => "available@email.com"
233 assert response = html_response(conn, 302)
234 assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/
237 test "with invalid params, POST /oauth/register?op=register renders registration_details page",
242 another_user = insert(:user)
243 registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
247 "scopes" => app.scopes,
248 "client_id" => app.client_id,
249 "redirect_uri" => app.redirect_uris,
250 "state" => "a_state",
251 "nickname" => "availablenickname",
252 "email" => "available@email.com"
255 for {bad_param, bad_param_value} <-
256 [{"nickname", another_user.nickname}, {"email", another_user.email}] do
257 bad_params = Map.put(params, bad_param, bad_param_value)
261 |> put_session(:registration_id, registration.id)
262 |> post("/oauth/register", bad_params)
264 assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/
265 assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken."
269 test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`",
274 user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt("testpassword"))
275 registration = insert(:registration, user: nil)
279 |> put_session(:registration_id, registration.id)
284 "scopes" => app.scopes,
285 "client_id" => app.client_id,
286 "redirect_uri" => app.redirect_uris,
287 "state" => "a_state",
288 "auth_name" => user.nickname,
289 "password" => "testpassword"
293 assert response = html_response(conn, 302)
294 assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/
297 test "with invalid params, POST /oauth/register?op=connect renders registration_details page",
303 registration = insert(:registration, user: nil)
307 "scopes" => app.scopes,
308 "client_id" => app.client_id,
309 "redirect_uri" => app.redirect_uris,
310 "state" => "a_state",
311 "auth_name" => user.nickname,
312 "password" => "wrong password"
317 |> put_session(:registration_id, registration.id)
318 |> post("/oauth/register", params)
320 assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/
321 assert get_flash(conn, :error) == "Invalid Username/Password"
325 describe "GET /oauth/authorize" do
328 app: insert(:oauth_app, redirect_uris: "https://redirect.url"),
331 |> Plug.Session.call(Plug.Session.init(@session_opts))
336 test "renders authentication page", %{app: app, conn: conn} do
342 "response_type" => "code",
343 "client_id" => app.client_id,
344 "redirect_uri" => app.redirect_uris,
349 assert html_response(conn, 200) =~ ~s(type="submit")
352 test "renders authentication page if user is already authenticated but `force_login` is tru-ish",
353 %{app: app, conn: conn} do
354 token = insert(:oauth_token, app_id: app.id)
358 |> put_session(:oauth_token, token.token)
362 "response_type" => "code",
363 "client_id" => app.client_id,
364 "redirect_uri" => app.redirect_uris,
366 "force_login" => "true"
370 assert html_response(conn, 200) =~ ~s(type="submit")
373 test "redirects to app if user is already authenticated", %{app: app, conn: conn} do
374 token = insert(:oauth_token, app_id: app.id)
378 |> put_session(:oauth_token, token.token)
382 "response_type" => "code",
383 "client_id" => app.client_id,
384 "redirect_uri" => app.redirect_uris,
389 assert redirected_to(conn) == "https://redirect.url"
393 describe "POST /oauth/authorize" do
394 test "redirects with oauth authorization" do
396 app = insert(:oauth_app, scopes: ["read", "write", "follow"])
400 |> post("/oauth/authorize", %{
401 "authorization" => %{
402 "name" => user.nickname,
403 "password" => "test",
404 "client_id" => app.client_id,
405 "redirect_uri" => app.redirect_uris,
406 "scope" => "read write",
407 "state" => "statepassed"
411 target = redirected_to(conn)
412 assert target =~ app.redirect_uris
414 query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
416 assert %{"state" => "statepassed", "code" => code} = query
417 auth = Repo.get_by(Authorization, token: code)
419 assert auth.scopes == ["read", "write"]
422 test "returns 401 for wrong credentials", %{conn: conn} do
424 app = insert(:oauth_app)
428 |> post("/oauth/authorize", %{
429 "authorization" => %{
430 "name" => user.nickname,
431 "password" => "wrong",
432 "client_id" => app.client_id,
433 "redirect_uri" => app.redirect_uris,
434 "state" => "statepassed",
435 "scope" => Enum.join(app.scopes, " ")
438 |> html_response(:unauthorized)
441 assert result =~ app.client_id
442 assert result =~ app.redirect_uris
445 assert result =~ "Invalid Username/Password"
448 test "returns 401 for missing scopes", %{conn: conn} do
450 app = insert(:oauth_app)
454 |> post("/oauth/authorize", %{
455 "authorization" => %{
456 "name" => user.nickname,
457 "password" => "test",
458 "client_id" => app.client_id,
459 "redirect_uri" => app.redirect_uris,
460 "state" => "statepassed",
464 |> html_response(:unauthorized)
467 assert result =~ app.client_id
468 assert result =~ app.redirect_uris
471 assert result =~ "This action is outside the authorized scopes"
474 test "returns 401 for scopes beyond app scopes", %{conn: conn} do
476 app = insert(:oauth_app, scopes: ["read", "write"])
480 |> post("/oauth/authorize", %{
481 "authorization" => %{
482 "name" => user.nickname,
483 "password" => "test",
484 "client_id" => app.client_id,
485 "redirect_uri" => app.redirect_uris,
486 "state" => "statepassed",
487 "scope" => "read write follow"
490 |> html_response(:unauthorized)
493 assert result =~ app.client_id
494 assert result =~ app.redirect_uris
497 assert result =~ "This action is outside the authorized scopes"
501 describe "POST /oauth/token" do
502 test "issues a token for an all-body request" do
504 app = insert(:oauth_app, scopes: ["read", "write"])
506 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
510 |> post("/oauth/token", %{
511 "grant_type" => "authorization_code",
512 "code" => auth.token,
513 "redirect_uri" => app.redirect_uris,
514 "client_id" => app.client_id,
515 "client_secret" => app.client_secret
518 assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200)
520 token = Repo.get_by(Token, token: token)
522 assert token.scopes == auth.scopes
523 assert user.ap_id == ap_id
526 test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do
527 password = "testpassword"
528 user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
530 app = insert(:oauth_app, scopes: ["read", "write"])
532 # Note: "scope" param is intentionally omitted
535 |> post("/oauth/token", %{
536 "grant_type" => "password",
537 "username" => user.nickname,
538 "password" => password,
539 "client_id" => app.client_id,
540 "client_secret" => app.client_secret
543 assert %{"access_token" => token} = json_response(conn, 200)
545 token = Repo.get_by(Token, token: token)
547 assert token.scopes == app.scopes
550 test "issues a token for request with HTTP basic auth client credentials" do
552 app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"])
554 {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"])
555 assert auth.scopes == ["scope1", "scope2"]
558 (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret))
563 |> put_req_header("authorization", "Basic " <> app_encoded)
564 |> post("/oauth/token", %{
565 "grant_type" => "authorization_code",
566 "code" => auth.token,
567 "redirect_uri" => app.redirect_uris
570 assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200)
572 assert scope == "scope1 scope2"
574 token = Repo.get_by(Token, token: token)
576 assert token.scopes == ["scope1", "scope2"]
579 test "rejects token exchange with invalid client credentials" do
581 app = insert(:oauth_app)
583 {:ok, auth} = Authorization.create_authorization(app, user)
587 |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=")
588 |> post("/oauth/token", %{
589 "grant_type" => "authorization_code",
590 "code" => auth.token,
591 "redirect_uri" => app.redirect_uris
594 assert resp = json_response(conn, 400)
595 assert %{"error" => _} = resp
596 refute Map.has_key?(resp, "access_token")
599 test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
600 setting = Pleroma.Config.get([:instance, :account_activation_required])
603 Pleroma.Config.put([:instance, :account_activation_required], true)
604 on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
607 password = "testpassword"
608 user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
609 info_change = Pleroma.User.Info.confirmation_changeset(user.info, :unconfirmed)
613 |> Ecto.Changeset.change()
614 |> Ecto.Changeset.put_embed(:info, info_change)
617 refute Pleroma.User.auth_active?(user)
619 app = insert(:oauth_app)
623 |> post("/oauth/token", %{
624 "grant_type" => "password",
625 "username" => user.nickname,
626 "password" => password,
627 "client_id" => app.client_id,
628 "client_secret" => app.client_secret
631 assert resp = json_response(conn, 403)
632 assert %{"error" => _} = resp
633 refute Map.has_key?(resp, "access_token")
636 test "rejects token exchange for valid credentials belonging to deactivated user" do
637 password = "testpassword"
641 password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
642 info: %{deactivated: true}
645 app = insert(:oauth_app)
649 |> post("/oauth/token", %{
650 "grant_type" => "password",
651 "username" => user.nickname,
652 "password" => password,
653 "client_id" => app.client_id,
654 "client_secret" => app.client_secret
657 assert resp = json_response(conn, 403)
658 assert %{"error" => _} = resp
659 refute Map.has_key?(resp, "access_token")
662 test "rejects an invalid authorization code" do
663 app = insert(:oauth_app)
667 |> post("/oauth/token", %{
668 "grant_type" => "authorization_code",
669 "code" => "Imobviouslyinvalid",
670 "redirect_uri" => app.redirect_uris,
671 "client_id" => app.client_id,
672 "client_secret" => app.client_secret
675 assert resp = json_response(conn, 400)
676 assert %{"error" => _} = json_response(conn, 400)
677 refute Map.has_key?(resp, "access_token")