defmodule Mobilizon.Web.ApplicationControllerTest do use Mobilizon.Web.ConnCase alias Mobilizon.Service.Auth.Applications use Mobilizon.Web, :verified_routes import Mobilizon.Factory describe "create application" do test "requires all parameters", %{conn: conn} do conn = conn |> post("/apps", %{"name" => "hello"}) assert error = json_response(conn, 400) assert error["error"] == "invalid_request" assert error["error_description"] == "All of name, scope and redirect_uri parameters are required to create an application" end test "requires valid scopes", %{conn: conn} do conn = conn |> post("/apps", %{ "name" => "hello", "redirect_uri" => "hello", "scope" => "write nothing" }) assert error = json_response(conn, 400) assert error["error"] == "invalid_scope" assert error["error_description"] == "The scope parameter is not a space separated list of valid scopes" end test "works", %{conn: conn} do name = "hello" redirect_uris = ["hello", "world"] scope = "read write:event:create" website = "hi" conn = conn |> post("/apps", %{ "name" => name, "redirect_uri" => Enum.join(redirect_uris, "\n"), "scope" => scope, "website" => website }) assert %{ "name" => ^name, "redirect_uri" => ^redirect_uris, "scope" => ^scope, "website" => ^website, "client_id" => _client_id, "client_secret" => _client_secret } = json_response(conn, 200) end end describe "authorize" do test "without a valid URI", %{conn: conn} do conn = get(conn, "/oauth/authorize?client_id=hello&redirect_uri=toto") assert response(conn, 400) == "You need to provide a valid redirect_uri to autorize an application" end test "without all valid params", %{conn: conn} do conn = get( conn, url( ~p"/oauth/authorize?client_id=hello&redirect_uri=#{URI.encode("https://somewhere.org/callback")}" ) ) assert redirected_to(conn) =~ "error=invalid_request&error_description=#{URI.encode_www_form("You need to specify client_id, redirect_uri, scope and state to autorize an application")}" end test "with all required params redirects to authorization page", %{conn: conn} do conn = get( conn, url( ~p"/oauth/authorize?client_id=hello&scope=everything&state=something&redirect_uri=#{URI.encode("https://somewhere.org/callback")}" ) ) assert redirected_to(conn) =~ "/oauth/autorize_approve" end end describe "generate device code" do test "without all required params", %{conn: conn} do conn = post(conn, "/login/device/code", client_id: "hello") assert error = json_response(conn, 400) assert error["error"] == "invalid_request" assert error["error_description"] == "You need to pass both client_id and scope as parameters to obtain a device code" end test "with an invalid client_id", %{conn: conn} do conn = post(conn, "/login/device/code", client_id: "hello", scope: "write:event:create") assert error = json_response(conn, 400) assert error["error"] == "invalid_client" assert error["error_description"] == "No application was found with this client_id" end test "with a scope not matching app registered scopes", %{conn: conn} do {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") conn = post(conn, "/login/device/code", client_id: app.client_id, scope: "write:event:delete") assert error = json_response(conn, 400) assert error["error"] == "invalid_scope" assert error["error_description"] == "The given scope is not in the list of the app declared scopes" end test "with valid params gives a URL-encoded code", %{conn: conn} do {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") conn = post(conn, "/login/device/code", client_id: app.client_id, scope: "write:event:create") res = json_response(conn, 200) verification_uri = url(~p"/login/device") assert %{ "device_code" => _device_code, "expires_in" => 900, "interval" => 5, "user_code" => user_code, "verification_uri" => ^verification_uri } = res assert Regex.match?(~r/^[A-Z]{4}-[A-Z]{4}$/, user_code) end test "with valid params and a JSON Accept header gives a JSON-encoded struct", %{conn: conn} do {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") conn = conn |> Plug.Conn.put_req_header("accept", "application/json") |> post("/login/device/code", client_id: app.client_id, scope: "write:event:create") res = json_response(conn, 200) verification_uri = url(~p"/login/device") assert %{ "device_code" => _device_code, "expires_in" => 900, "interval" => 5, "user_code" => user_code, "verification_uri" => ^verification_uri } = res assert Regex.match?(~r/^[A-Z]{4}-[A-Z]{4}$/, user_code) end end describe "generate access code for device flow" do test "without valid parameters", %{conn: conn} do conn = post(conn, "/oauth/token") assert error = json_response(conn, 400) assert error["error"] == "invalid_request" assert error["error_description"] == "Incorrect parameters sent. You need to provide at least the grant_type and client_id parameters, depending on the grant type being used." end test "with invalid client_id", %{conn: conn} do conn = post(conn, "/oauth/token", grant_type: "urn:ietf:params:oauth:grant-type:device_code", client_id: "some_client_id", device_code: "hello" ) assert error = json_response(conn, 400) assert error["error"] == "invalid_grant" assert error["error_description"] == "The client_id provided or the device_code associated is invalid" end test "with rejected authorization", %{conn: conn} do {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") assert {:ok, _res} = Mobilizon.Applications.create_application_device_activation(%{ device_code: "hello", user_code: "world", expires_in: 900, application_id: app.id, scope: "write:event:create write:event:update", status: :access_denied }) conn = post(conn, "/oauth/token", grant_type: "urn:ietf:params:oauth:grant-type:device_code", client_id: app.client_id, device_code: "hello" ) assert error = json_response(conn, 400) assert error["error"] == "access_denied" assert error["error_description"] == "The user rejected the requested authorization" end test "with incorrect device code", %{conn: conn} do {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") assert {:ok, _res} = Mobilizon.Applications.create_application_device_activation(%{ device_code: "hello", user_code: "world", expires_in: 900, application_id: app.id, scope: "write:event:create write:event:update", status: "incorrect_device_code" }) conn = post(conn, "/oauth/token", grant_type: "urn:ietf:params:oauth:grant-type:device_code", client_id: app.client_id, device_code: "hello" ) assert error = json_response(conn, 400) assert error["error"] == "invalid_grant" assert error["error_description"] == "The client_id provided or the device_code associated is invalid" end test "with an expired device activation", %{conn: conn} do user = insert(:user) {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") assert {:ok, _res} = Mobilizon.Applications.create_application_device_activation(%{ device_code: "hello", user_code: "world", expires_in: -40, application_id: app.id, scope: "write:event:create write:event:update", status: "success", user_id: user.id }) conn = post(conn, "/oauth/token", grant_type: "urn:ietf:params:oauth:grant-type:device_code", client_id: app.client_id, device_code: "hello" ) assert error = json_response(conn, 400) assert error["error"] == "expired_token" assert error["error_description"] == "The given device_code has expired" end test "with a pending authorization", %{conn: conn} do user = insert(:user) {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") assert {:ok, _res} = Mobilizon.Applications.create_application_device_activation(%{ device_code: "hello", user_code: "world", expires_in: 600, application_id: app.id, scope: "write:event:create write:event:update", status: "pending", user_id: user.id }) conn = conn |> Plug.Conn.put_req_header("accept", "application/json") |> post("/oauth/token", grant_type: "urn:ietf:params:oauth:grant-type:device_code", client_id: app.client_id, device_code: "hello" ) error = json_response(conn, 400) assert error["error"] == "authorization_pending" assert error["error_description"] == "The authorization request is still pending" conn = Phoenix.ConnTest.build_conn() |> Plug.Conn.put_req_header("accept", "application/json") |> post("/oauth/token", grant_type: "urn:ietf:params:oauth:grant-type:device_code", client_id: app.client_id, device_code: "hello" ) error = json_response(conn, 400) assert error["error"] == "slow_down" assert error["error_description"] == "Please slow down the rate of your requests" end test "with valid params as JSON", %{conn: conn} do user = insert(:user) {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") assert {:ok, _res} = Mobilizon.Applications.create_application_device_activation(%{ device_code: "hello", user_code: "world", expires_in: 600, application_id: app.id, scope: "write:event:create write:event:update", status: "success", user_id: user.id }) conn = conn |> Plug.Conn.put_req_header("accept", "application/json") |> post("/oauth/token", grant_type: "urn:ietf:params:oauth:grant-type:device_code", client_id: app.client_id, device_code: "hello" ) res = json_response(conn, 200) assert %{ "access_token" => _access_token, "expires_in" => 28_800, "refresh_token" => _refresh_token, "refresh_token_expires_in" => 15_724_800, "scope" => "write:event:create write:event:update", "token_type" => "bearer" } = res end end describe "generate access code for authorization flow" do test "with invalid client_id", %{conn: conn} do conn = post(conn, "/oauth/token", grant_type: "authorization_code", client_id: "some_client_id", client_secret: "some_client_secret", code: "hello", redirect_uri: "some redirect uri", scope: "hello" ) assert error = json_response(conn, 400) assert error["error"] == "invalid_request" assert json_response(conn, 400)["error_description"] == "No application was found with this client_id" end test "with invalid redirect_uri", %{conn: conn} do {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") conn = post(conn, "/oauth/token", grant_type: "authorization_code", client_id: app.client_id, client_secret: app.client_secret, code: "hello", redirect_uri: "nope", scope: "write:event:create" ) assert error = json_response(conn, 400) assert error["error"] == "invalid_request" assert error["error_description"] == "This redirect URI is not allowed" end test "with invalid code", %{conn: conn} do {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") conn = post(conn, "/oauth/token", grant_type: "authorization_code", client_id: app.client_id, client_secret: app.client_secret, code: "hello", redirect_uri: "hello", scope: "write:event:create" ) assert error = json_response(conn, 400) assert error["error"] == "invalid_grant" assert error["error_description"] == "The provided code is invalid or expired" end test "with invalid client secret", %{conn: conn} do user = insert(:user) {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") Mobilizon.Applications.create_application_token(%{ user_id: user.id, application_id: app.id, authorization_code: "hi there", scope: "write:event:create write:event:update" }) conn = post(conn, "/oauth/token", grant_type: "authorization_code", client_id: app.client_id, client_secret: "not the client secret", code: "hi there", redirect_uri: "hello", scope: "write:event:create write:event:update" ) assert error = json_response(conn, 400) assert error["error"] == "invalid_client" assert error["error_description"] == "The provided client_secret is invalid" end test "with an authorization code matching a different app", %{conn: conn} do user = insert(:user) {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") {:ok, app2} = Applications.create("My other app", ["hello"], "write:event:create write:event:update") Mobilizon.Applications.create_application_token(%{ user_id: user.id, application_id: app2.id, authorization_code: "hi there", scope: "write:event:create write:event:update" }) conn = post(conn, "/oauth/token", grant_type: "authorization_code", client_id: app.client_id, client_secret: app.client_id, code: "hi there", redirect_uri: "hello", scope: "write:event:create write:event:update" ) assert error = json_response(conn, 400) assert error["error"] == "invalid_grant" assert error["error_description"] == "The provided client_id does not match the provided code" end test "with valid params", %{conn: conn} do user = insert(:user) {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") Mobilizon.Applications.create_application_token(%{ user_id: user.id, application_id: app.id, authorization_code: "hi there", scope: "write:event:create write:event:update" }) conn = post(conn, "/oauth/token", grant_type: "authorization_code", client_id: app.client_id, client_secret: app.client_secret, code: "hi there", redirect_uri: "hello", scope: "write:event:create write:event:update" ) res = json_response(conn, 200) assert %{ "access_token" => _access_token, "expires_in" => 28_800, "refresh_token" => _refresh_token, "refresh_token_expires_in" => 15_724_800, "scope" => "write:event:create write:event:update", "token_type" => "bearer" } = res end end describe "generate new access code from refresh code" do test "with invalid refresh token", %{conn: conn} do conn = post(conn, "/oauth/token", grant_type: "refresh_token", client_id: "hello", client_secret: "secret", refresh_token: "none" ) assert error = json_response(conn, 400) assert error["error"] == "invalid_grant" assert error["error_description"] == "Invalid refresh token provided" end test "with invalid client credentials", %{conn: conn} do user = insert(:user) {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") Mobilizon.Applications.create_application_token(%{ user_id: user.id, application_id: app.id, authorization_code: "hi there", scope: "write:event:create write:event:update" }) conn = post(conn, "/oauth/token", grant_type: "authorization_code", client_id: app.client_id, client_secret: app.client_secret, code: "hi there", redirect_uri: "hello", scope: "write:event:create write:event:update" ) res = json_response(conn, 200) conn = post(conn, "/oauth/token", grant_type: "refresh_token", client_id: "hello", client_secret: "secret", refresh_token: res["refresh_token"] ) assert error = json_response(conn, 400) assert error["error"] == "invalid_client" assert error["error_description"] == "Invalid client credentials provided" end test "with valid params", %{conn: conn} do user = insert(:user) {:ok, app} = Applications.create("My app", ["hello"], "write:event:create write:event:update") Mobilizon.Applications.create_application_token(%{ user_id: user.id, application_id: app.id, authorization_code: "hi there", scope: "write:event:create write:event:update" }) conn = post(conn, "/oauth/token", grant_type: "authorization_code", client_id: app.client_id, client_secret: app.client_secret, code: "hi there", redirect_uri: "hello", scope: "write:event:create write:event:update" ) res = json_response(conn, 200) conn = post(conn, "/oauth/token", grant_type: "refresh_token", client_id: app.client_id, client_secret: app.client_secret, refresh_token: res["refresh_token"] ) res = json_response(conn, 200) assert %{ "access_token" => _access_token, "expires_in" => 28_800, "refresh_token" => _refresh_token, "refresh_token_expires_in" => 15_724_800, "scope" => "write:event:create write:event:update", "token_type" => "bearer" } = res end end end