diff --git a/lib/frenzy/approved_client.ex b/lib/frenzy/approved_client.ex new file mode 100644 index 0000000..456fcb2 --- /dev/null +++ b/lib/frenzy/approved_client.ex @@ -0,0 +1,21 @@ +defmodule Frenzy.ApprovedClient do + use Ecto.Schema + import Ecto.Changeset + + schema "approved_clients" do + field :client_id, :string + field :auth_code, :string + field :access_token, :string + + belongs_to :user, Frenzy.User + + timestamps() + end + + @doc false + def changeset(approved_client, attrs) do + approved_client + |> cast(attrs, [:client_id, :auth_code, :access_token]) + |> validate_required([:client_id]) + end +end diff --git a/lib/frenzy/fervor_client.ex b/lib/frenzy/fervor_client.ex new file mode 100644 index 0000000..08e6b1d --- /dev/null +++ b/lib/frenzy/fervor_client.ex @@ -0,0 +1,31 @@ +defmodule Frenzy.FervorClient do + use Ecto.Schema + import Ecto.Changeset + + def to_fervor(client) do + %{ + client_name: client.client_name, + website: client.website, + redirect_uri: client.redirect_uri, + client_id: client.client_id, + client_secret: client.client_secret + } + end + + schema "fervor_clients" do + field :client_name, :string + field :website, :string + field :redirect_uri, :string + field :client_id, :string + field :client_secret, :string + + timestamps() + end + + @doc false + def changeset(client, attrs) do + client + |> cast(attrs, [:client_name, :website, :redirect_uri, :client_id, :client_secret]) + |> validate_required([:client_name, :redirect_uri, :client_id, :client_secret]) + end +end diff --git a/lib/frenzy/user.ex b/lib/frenzy/user.ex index 5a09bc1..9b06b0b 100644 --- a/lib/frenzy/user.ex +++ b/lib/frenzy/user.ex @@ -9,6 +9,8 @@ defmodule Frenzy.User do field :fever_password, :string, virtual: true field :fever_auth_token, :string + has_many :approved_clients, Frenzy.ApprovedClient, on_delete: :delete_all + has_many :groups, Frenzy.Group, on_delete: :delete_all timestamps() diff --git a/lib/frenzy_web/controllers/fervor_controller.ex b/lib/frenzy_web/controllers/fervor_controller.ex new file mode 100644 index 0000000..e55a5c5 --- /dev/null +++ b/lib/frenzy_web/controllers/fervor_controller.ex @@ -0,0 +1,30 @@ +defmodule FrenzyWeb.FervorController do + use FrenzyWeb, :controller + alias Frenzy.{Repo, FervorClient, Group, Feed, Filter, Item} + alias FrenzyWeb.Router.Helpers, as: Routes + alias FrenzyWeb.Endpoint + import Ecto.Query + + plug Plug.Parsers, parsers: [:urlencoded, :multipart] + + def register(conn, _params) do + %{"client_name" => client_name, "redirect_uri" => redirect_uri} = conn.body_params + website = Map.get(conn.body_params, "website") + + changeset = + FervorClient.changeset( + %FervorClient{}, + %{ + "client_name" => client_name, + "website" => website, + "redirect_uri" => redirect_uri, + "client_id" => :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false), + "client_secret" => :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + } + ) + + {:ok, client} = Repo.insert(changeset) + + json(conn, FervorClient.to_fervor(client)) + end +end diff --git a/lib/frenzy_web/controllers/login_controller.ex b/lib/frenzy_web/controllers/login_controller.ex index a201f96..5a7f6c4 100644 --- a/lib/frenzy_web/controllers/login_controller.ex +++ b/lib/frenzy_web/controllers/login_controller.ex @@ -5,22 +5,24 @@ defmodule FrenzyWeb.LoginController do alias FrenzyWeb.Endpoint import Ecto.Query - def login(conn, _params) do - render(conn, "login.html") + def login(conn, params) do + render(conn, "login.html", %{ + continue: Map.get(params, "continue") + }) end @error_message "Invalid username or password" - def login_post(conn, %{"username" => username, "password" => password}) do + def login_post(conn, %{"username" => username, "password" => password} = params) do user = Repo.get_by(User, username: username) case Bcrypt.check_pass(user, password) do {:ok, user} -> user_token = Phoenix.Token.sign(Endpoint, "user token", user.id) + conn = put_session(conn, :user_token, user_token) - conn - |> put_session(:user_token, user_token) - |> redirect(to: Routes.group_path(Endpoint, :index)) + redirect_uri = Map.get(params, "continue") || Routes.group_path(Endpoint, :index) + redirect(conn, to: redirect_uri) {:error, _reason} -> conn diff --git a/lib/frenzy_web/controllers/oauth_controller.ex b/lib/frenzy_web/controllers/oauth_controller.ex new file mode 100644 index 0000000..296d220 --- /dev/null +++ b/lib/frenzy_web/controllers/oauth_controller.ex @@ -0,0 +1,191 @@ +defmodule FrenzyWeb.OauthController do + use FrenzyWeb, :controller + alias Frenzy.{Repo, FervorClient, User, ApprovedClient} + alias FrenzyWeb.Router.Helpers, as: Routes + alias FrenzyWeb.Endpoint + + def authorize_get(conn, params) do + user_token = get_session(conn, :user_token) + + case Phoenix.Token.verify(Endpoint, "user token", user_token, max_age: 24 * 60 * 60) do + {:error, _reason} -> + continue = "#{conn.request_path}?#{conn.query_string}" + redirect(conn, to: Routes.login_path(Endpoint, :login, continue: continue)) + + {:ok, user_id} -> + case Repo.get(User, user_id) do + nil -> + continue = "#{conn.request_path}?#{conn.query_string}" + redirect(conn, to: Routes.login_path(Endpoint, :login, continue: continue)) + + user -> + conn + |> assign(:user, user) + |> try_render_authorize(params) + end + end + end + + def try_render_authorize( + conn, + %{ + "response_type" => "code", + "client_id" => client_id, + "redirect_uri" => redirect_uri + } = params + ) do + case Repo.get_by(FervorClient, client_id: client_id) do + nil -> + conn + + client -> + if redirect_uri == client.redirect_uri do + render(conn, "authorize.html", %{ + client: client, + state: Map.get(params, "state") + }) + else + conn + |> put_status(400) + |> json(%{error: "mismatched redirect uri"}) + end + end + end + + def try_render_authorize(conn, _params) do + conn + |> put_status(400) + |> json(%{error: "invalid parameters"}) + end + + def authorize_post(conn, params) do + user_token = get_session(conn, :user_token) + + case Phoenix.Token.verify(Endpoint, "user token", user_token, max_age: 24 * 60 * 60) do + {:error, _reason} -> + continue = "#{conn.request_path}?#{conn.query_string}" + redirect(conn, to: Routes.login_path(Endpoint, :login, continue: continue)) + + {:ok, user_id} -> + case Repo.get(User, user_id) do + nil -> + continue = "#{conn.request_path}?#{conn.query_string}" + redirect(conn, to: Routes.login_path(Endpoint, :login, continue: continue)) + + user -> + conn + |> assign(:user, user) + |> try_authorize(params) + end + end + end + + def try_authorize(conn, %{"client_id" => client_id} = params) do + user = conn.assigns[:user] + client = Repo.get_by(FervorClient, client_id: client_id) + + state = Map.get(params, "state") + + auth_code = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + + changeset = + Ecto.build_assoc(user, :approved_clients, %{ + auth_code: auth_code, + client_id: client_id + }) + + {:ok, _approved_client} = Repo.insert(changeset) + + case client.redirect_uri do + "urn:ietf:wg:oauth:2.0:oob" -> + render(conn, "successfully_authorized.html", %{ + auth_code: auth_code + }) + + redirect_uri -> + parsed = URI.parse(redirect_uri) + + query = + URI.encode_query( + if state, do: %{code: auth_code, state: state}, else: %{code: auth_code} + ) + + uri = %URI{parsed | query: query} + redirect(conn, to: uri) + end + end + + def try_authorize(conn, _params) do + conn + |> put_status(400) + |> json(%{error: "invalid parameters"}) + end + + def token( + conn, + %{ + "redirect_uri" => redirect_uri, + "client_id" => client_id, + "client_secret" => client_secret + } = params + ) do + case Repo.get_by(FervorClient, client_id: client_id) do + nil -> + conn + |> put_status(401) + |> json(%{error: "invalid_client", error_description: "incorrect client information"}) + + client -> + if client_secret == client.client_secret and redirect_uri == client.redirect_uri do + conn + |> assign(:client, client) + |> try_generate_token(params) + else + conn + |> put_status(400) + |> json(%{error: "invalid_grant", error_description: "incorrect client information"}) + end + end + end + + def token(conn, _params) do + json(conn, %{error: "invalid_request", error_description: "missing parameters"}) + end + + def try_generate_token(conn, %{ + "grant_type" => "authorization_code", + "authorization_code" => auth_code + }) do + case Repo.get_by(ApprovedClient, auth_code: auth_code) do + nil -> + conn + |> put_status(400) + |> json(%{error: "invalid_grant", error_description: "invalid authorization code"}) + + approved_client -> + access_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + + changeset = + ApprovedClient.changeset(approved_client, %{ + auth_code: nil, + access_token: access_token + }) + + {:ok, _approved_client} = Repo.update(changeset) + + json(conn, %{ + access_token: access_token, + token_type: "bearer" + }) + end + end + + def try_generate_token(conn, _params) do + conn + |> put_status(400) + |> json(%{ + error: "unsupported_grant_type", + error_description: "only grant_type=authorization_code is supported" + }) + end +end diff --git a/lib/frenzy_web/router.ex b/lib/frenzy_web/router.ex index d9d7d14..2e97382 100644 --- a/lib/frenzy_web/router.ex +++ b/lib/frenzy_web/router.ex @@ -10,11 +10,6 @@ defmodule FrenzyWeb.Router do end pipeline :authenticate do - # plug :accepts, ["html"] - # plug :fetch_session - # plug :fetch_flash - # plug :protect_from_forgery - # plug :put_secure_browser_headers plug FrenzyWeb.Plug.Authenticate end @@ -59,8 +54,18 @@ defmodule FrenzyWeb.Router do post "/api/fever.php", FeverController, :post end - # Other scopes may use custom stacks. - # scope "/api", FrenzyWeb do - # pipe_through :api - # end + scope "/", FrenzyWeb do + pipe_through :api + + post "/api/v1/register", FervorController, :register + + post "/oauth/token", OauthController, :token + end + + scope "/", FrenzyWeb do + pipe_through :browser + + get "/oauth/authorize", OauthController, :authorize_get + post "/oauth/authorize", OauthController, :authorize_post + end end diff --git a/lib/frenzy_web/templates/login/login.html.eex b/lib/frenzy_web/templates/login/login.html.eex index 3c61165..9cfec7c 100644 --- a/lib/frenzy_web/templates/login/login.html.eex +++ b/lib/frenzy_web/templates/login/login.html.eex @@ -1,6 +1,7 @@ -<% IO.inspect(get_flash(@conn, :error)) %> - <%= form_tag Routes.login_path(@conn, :login_post), method: :post do %> + <%= if @continue do %> + + <% end %>
diff --git a/lib/frenzy_web/templates/oauth/authorize.html.eex b/lib/frenzy_web/templates/oauth/authorize.html.eex new file mode 100644 index 0000000..936c296 --- /dev/null +++ b/lib/frenzy_web/templates/oauth/authorize.html.eex @@ -0,0 +1,11 @@ +

<%= @client.client_name %> wants to access your account

+ +<%= form_tag Routes.oauth_path(@conn, :authorize_post), method: :post do %> + + <%= if @state do %> + + <% end %> +
+ <%= submit "Grant access" %> +
+<% end %> \ No newline at end of file diff --git a/lib/frenzy_web/templates/oauth/successfully_authorized.html.eex b/lib/frenzy_web/templates/oauth/successfully_authorized.html.eex new file mode 100644 index 0000000..247bca1 --- /dev/null +++ b/lib/frenzy_web/templates/oauth/successfully_authorized.html.eex @@ -0,0 +1,3 @@ +

Authorization Successful

+

Authorization Code:

+
<%= @auth_code %>
\ No newline at end of file diff --git a/lib/frenzy_web/views/oauth_view.ex b/lib/frenzy_web/views/oauth_view.ex new file mode 100644 index 0000000..af5ad0e --- /dev/null +++ b/lib/frenzy_web/views/oauth_view.ex @@ -0,0 +1,3 @@ +defmodule FrenzyWeb.OauthView do + use FrenzyWeb, :view +end diff --git a/priv/repo/migrations/20190327233837_create_fervor_clients.exs b/priv/repo/migrations/20190327233837_create_fervor_clients.exs new file mode 100644 index 0000000..d291ed6 --- /dev/null +++ b/priv/repo/migrations/20190327233837_create_fervor_clients.exs @@ -0,0 +1,15 @@ +defmodule Frenzy.Repo.Migrations.CreateFervorClients do + use Ecto.Migration + + def change do + create table(:fervor_clients) do + add :client_name, :string + add :website, :string + add :redirect_uri, :string + add :client_id, :string + add :client_secret, :string + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20190328203923_create_approved_clients.exs b/priv/repo/migrations/20190328203923_create_approved_clients.exs new file mode 100644 index 0000000..6e7ee5e --- /dev/null +++ b/priv/repo/migrations/20190328203923_create_approved_clients.exs @@ -0,0 +1,15 @@ +defmodule Frenzy.Repo.Migrations.CreateApprovedClients do + use Ecto.Migration + + def change do + create table(:approved_clients) do + add :client_id, :string + add :auth_code, :string + add :access_token, :string + + add :user_id, references(:users, on_delete: :delete_all) + + timestamps() + end + end +end