OAuth implementation
This commit is contained in:
parent
9fa6968f6f
commit
73dee19294
|
@ -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
|
|
@ -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
|
|
@ -9,6 +9,8 @@ defmodule Frenzy.User do
|
||||||
field :fever_password, :string, virtual: true
|
field :fever_password, :string, virtual: true
|
||||||
field :fever_auth_token, :string
|
field :fever_auth_token, :string
|
||||||
|
|
||||||
|
has_many :approved_clients, Frenzy.ApprovedClient, on_delete: :delete_all
|
||||||
|
|
||||||
has_many :groups, Frenzy.Group, on_delete: :delete_all
|
has_many :groups, Frenzy.Group, on_delete: :delete_all
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
|
|
|
@ -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
|
|
@ -5,22 +5,24 @@ defmodule FrenzyWeb.LoginController do
|
||||||
alias FrenzyWeb.Endpoint
|
alias FrenzyWeb.Endpoint
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
def login(conn, _params) do
|
def login(conn, params) do
|
||||||
render(conn, "login.html")
|
render(conn, "login.html", %{
|
||||||
|
continue: Map.get(params, "continue")
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@error_message "Invalid username or password"
|
@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)
|
user = Repo.get_by(User, username: username)
|
||||||
|
|
||||||
case Bcrypt.check_pass(user, password) do
|
case Bcrypt.check_pass(user, password) do
|
||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
user_token = Phoenix.Token.sign(Endpoint, "user token", user.id)
|
user_token = Phoenix.Token.sign(Endpoint, "user token", user.id)
|
||||||
|
conn = put_session(conn, :user_token, user_token)
|
||||||
|
|
||||||
conn
|
redirect_uri = Map.get(params, "continue") || Routes.group_path(Endpoint, :index)
|
||||||
|> put_session(:user_token, user_token)
|
redirect(conn, to: redirect_uri)
|
||||||
|> redirect(to: Routes.group_path(Endpoint, :index))
|
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
conn
|
conn
|
||||||
|
|
|
@ -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
|
|
@ -10,11 +10,6 @@ defmodule FrenzyWeb.Router do
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :authenticate do
|
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
|
plug FrenzyWeb.Plug.Authenticate
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -59,8 +54,18 @@ defmodule FrenzyWeb.Router do
|
||||||
post "/api/fever.php", FeverController, :post
|
post "/api/fever.php", FeverController, :post
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
scope "/", FrenzyWeb do
|
||||||
# scope "/api", FrenzyWeb do
|
pipe_through :api
|
||||||
# pipe_through :api
|
|
||||||
# end
|
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
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<% IO.inspect(get_flash(@conn, :error)) %>
|
|
||||||
|
|
||||||
<%= form_tag Routes.login_path(@conn, :login_post), method: :post do %>
|
<%= form_tag Routes.login_path(@conn, :login_post), method: :post do %>
|
||||||
|
<%= if @continue do %>
|
||||||
|
<input type="hidden" name="continue" value="<%= @continue %>">
|
||||||
|
<% end %>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" name="username" id="username">
|
<input type="text" name="username" id="username">
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<h1><%= @client.client_name %> wants to access your account</h1>
|
||||||
|
|
||||||
|
<%= form_tag Routes.oauth_path(@conn, :authorize_post), method: :post do %>
|
||||||
|
<input type="hidden" name="client_id" value="<%= @client.client_id %>">
|
||||||
|
<%= if @state do %>
|
||||||
|
<input type="hidden" name="state" value="<%= @state %>">
|
||||||
|
<% end %>
|
||||||
|
<div class="form-group">
|
||||||
|
<%= submit "Grant access" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<h1>Authorization Successful</h1>
|
||||||
|
<p>Authorization Code:</p>
|
||||||
|
<pre><%= @auth_code %></pre>
|
|
@ -0,0 +1,3 @@
|
||||||
|
defmodule FrenzyWeb.OauthView do
|
||||||
|
use FrenzyWeb, :view
|
||||||
|
end
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue