Skip to content
This repository has been archived by the owner on Aug 26, 2024. It is now read-only.

Grapevine auth #91

Merged
merged 3 commits into from
Nov 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,9 @@ config :mime, :types, %{

config :phoenix, :format_encoders, [collection: Poison, hal: Poison, mason: Poison, siren: Poison, jsonapi: Poison]

config :ueberauth, Ueberauth,
providers: [
grapevine: { Grapevine.Ueberauth.Strategy, [scope: "profile email"] }
]

import_config "#{Mix.env()}.exs"
33 changes: 31 additions & 2 deletions lib/data/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ defmodule Data.User do
field(:token, Ecto.UUID)
field(:notes, :string)

field(:provider, :string)
field(:provider_uid, :string)

field(:totp_secret, :string)
field(:totp_verified_at, Timex.Ecto.DateTime)

Expand Down Expand Up @@ -59,6 +62,29 @@ defmodule Data.User do
|> unique_constraint(:email)
end

def grapevine_changeset(struct, params) do
struct
|> cast(params, [:name, :email, :provider, :provider_uid])
|> validate_required([:provider, :provider_uid])
|> validate_name()
|> validate_format(:email, ~r/.+@.+\..+/)
|> ensure(:flags, [])
|> ensure(:token, UUID.uuid4())
|> unique_constraint(:name, name: :users_lower_name_index)
|> unique_constraint(:email)
|> unique_constraint(:provider_uid, name: :users_provider_provider_uid_index)
end

def finalize_changeset(struct, params) do
struct
|> cast(params, [:name, :email])
|> validate_required([:name])
|> validate_name()
|> validate_format(:email, ~r/.+@.+\..+/)
|> unique_constraint(:name, name: :users_lower_name_index)
|> unique_constraint(:email)
end

def email_changeset(struct, params) do
struct
|> cast(params, [:email])
Expand Down Expand Up @@ -118,8 +144,11 @@ defmodule Data.User do
case changeset do
%{changes: %{name: name}} ->
case Regex.match?(~r/ /, name) do
true -> add_error(changeset, :name, "cannot contain spaces")
false -> changeset
true ->
add_error(changeset, :name, "cannot contain spaces")

false ->
changeset
end

_ ->
Expand Down
150 changes: 150 additions & 0 deletions lib/grapevine/ueberauth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
defmodule Grapevine.Ueberauth.Strategy do
@moduledoc """
Grapevine authentication strategy for Ueberauth
"""

use Ueberauth.Strategy, default_scope: "profile email"

alias Grapevine.Ueberauth.Strategy.OAuth

defmodule OAuth do
@moduledoc """
OAuth client used by the Grapevine Ueberauth strategy
"""

use OAuth2.Strategy

@defaults [
strategy: __MODULE__,
site: "https://grapevine.haus",
authorize_url: "/oauth/authorize",
token_url: "/oauth/token"
]

def client(opts \\ []) do
client_id = Application.get_env(:gossip, :client_id)
client_secret = Application.get_env(:gossip, :client_secret)

opts = Enum.reject(opts, fn {_key, val} -> is_nil(val) end)

opts =
@defaults
|> Keyword.merge(opts)
|> Keyword.merge([client_id: client_id, client_secret: client_secret])

OAuth2.Client.new(opts)
end

def authorize_url!(params \\ [], opts \\ []) do
opts
|> client()
|> OAuth2.Client.authorize_url!(params)
end

def get(token, url, opts \\ []) do
[token: token]
|> Keyword.merge(opts)
|> client()
|> OAuth2.Client.get(url)
end

def get_access_token(params \\ [], opts \\ []) do
case opts |> client() |> OAuth2.Client.get_token(params) do
{:error, %{body: %{"error" => error, "error_description" => description}}} ->
{:error, {error, description}}

{:ok, %{token: %{access_token: nil} = token}} ->
%{"error" => error, "error_description" => description} = token.other_params
{:error, {error, description}}

{:ok, %{token: token}} ->
{:ok, token}
end
end

# Strategy Callbacks

def authorize_url(client, params) do
OAuth2.Strategy.AuthCode.authorize_url(client, params)
end

def get_token(client, params, headers) do
client
|> put_header("Accept", "application/json")
|> put_header("Content-Type", "application/json")
|> OAuth2.Strategy.AuthCode.get_token(params, headers)
end
end

@impl true
def handle_request!(conn) do
scopes = Keyword.get(options(conn), :scope, Keyword.get(default_options(), :default_scope))

params = [scope: scopes, state: UUID.uuid4()]
opts = [site: site(conn), redirect_uri: callback_url(conn)]

redirect!(conn, OAuth.authorize_url!(params, opts))
end

@impl true
def handle_callback!(conn = %Plug.Conn{params: %{"code" => code}}) do
params = [code: code]
opts = [site: site(conn), redirect_uri: callback_url(conn)]

case OAuth.get_access_token(params, opts) do
{:ok, token} ->
fetch_user(conn, token)

{:error, {error_code, error_description}} ->
set_errors!(conn, [error(error_code, error_description)])
end
end

@impl true
def credentials(conn) do
token = conn.private.grapevine_token

%Ueberauth.Auth.Credentials{
expires: true,
expires_at: token.expires_at,
refresh_token: token.refresh_token,
token: token.access_token
}
end

@impl true
def uid(conn) do
conn.private.grapevine_user["uid"]
end

@impl true
def info(conn) do
%Ueberauth.Auth.Info{
email: conn.private.grapevine_user["email"],
name: conn.private.grapevine_user["username"],
}
end

defp fetch_user(conn, token) do
conn = put_private(conn, :grapevine_token, token)

opts = [site: site(conn)]
response = OAuth.get(token, "/users/me", opts)

case response do
{:ok, %OAuth2.Response{status_code: 401, body: _body}} ->
set_errors!(conn, [error("token", "unauthorized")])

{:ok, %OAuth2.Response{status_code: 200, body: user}} ->
put_private(conn, :grapevine_user, user)

{:error, %OAuth2.Response{status_code: status_code}} ->
set_errors!(conn, [error("OAuth2", status_code)])

{:error, %OAuth2.Error{reason: reason}} ->
set_errors!(conn, [error("OAuth2", reason)])
end
end

defp site(conn), do: Keyword.get(options(conn), :site)
end
40 changes: 40 additions & 0 deletions lib/web/controllers/auth_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule Web.AuthController do
use Web, :controller

plug Ueberauth

alias Web.User

def request(conn, _params) do
conn
|> put_flash(:error, "There was an error authenticating.")
|> redirect(to: public_page_path(conn, :index))
end

def callback(conn = %{assigns: %{ueberauth_failure: _fails}}, _params) do
conn
|> put_flash(:error, "Failed to authenticate.")
|> redirect(to: public_page_path(conn, :index))
end

def callback(conn = %{assigns: %{ueberauth_auth: auth}}, _params) do
case User.from_grapevine(auth) do
{:ok, user} ->
conn
|> put_flash(:info, "Successfully authenticated.")
|> put_session(:user_token, user.token)
|> redirect(to: public_page_path(conn, :index))

{:ok, :finalize_registration, user} ->
conn
|> put_flash(:info, "Please finish registration.")
|> put_session(:user_token, user.token)
|> redirect(to: public_registration_path(conn, :finalize))

{:error, _changeset} ->
conn
|> put_flash(:error, "There was a problem signing in. Please contact an administrator.")
|> redirect(to: public_page_path(conn, :index))
end
end
end
1 change: 1 addition & 0 deletions lib/web/controllers/chat_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule Web.ChatController do
use Web, :controller

plug(Web.Plug.PublicEnsureUser)
plug(Web.Plug.PublicEnsureCharacter)

def show(conn, _params) do
conn |> render("show.html")
Expand Down
1 change: 1 addition & 0 deletions lib/web/controllers/mail_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Web.MailController do
alias Web.Mail

plug(Web.Plug.PublicEnsureUser)
plug(Web.Plug.PublicEnsureCharacter)
plug(Web.Plug.FetchPage when action in [:index])

plug(:load_mail when action in [:show])
Expand Down
2 changes: 2 additions & 0 deletions lib/web/controllers/play_controller.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Web.PlayController do
use Web, :controller

plug(Web.Plug.PublicEnsureUser)
plug(Web.Plug.PublicEnsureCharacter)
plug(:put_layout, "play.html")

def show(conn, _params) do
Expand Down
34 changes: 34 additions & 0 deletions lib/web/controllers/registration_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule Web.RegistrationController do
alias Game.Config
alias Web.User

plug(Web.Plug.PublicEnsureUser when action in [:finalize, :update])

def new(conn, _params) do
changeset = User.new()

Expand All @@ -27,4 +29,36 @@ defmodule Web.RegistrationController do
|> render("new.html")
end
end

def finalize(conn, _params) do
%{user: user} = conn.assigns

with true <- User.finalize_registration?(user) do
changeset = User.finalize(user)

conn
|> assign(:changeset, changeset)
|> render("finalize.html")
else
_ ->
redirect(conn, to: public_page_path(conn, :index))
end
end

def update(conn, %{"user" => params}) do
%{user: user} = conn.assigns

with true <- User.finalize_registration?(user),
{:ok, _user} <- User.finalize_user(user, params) do
redirect(conn, to: public_page_path(conn, :index))
else
{:error, changeset} ->
conn
|> assign(:changeset, changeset)
|> render("finalize.html")

_ ->
redirect(conn, to: public_page_path(conn, :index))
end
end
end
22 changes: 16 additions & 6 deletions lib/web/plug/load_character.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,25 @@ defmodule Web.Plug.LoadCharacter do
end

defp load_any_character(conn, user) do
character = List.first(user.characters)
case List.first(user.characters) do
nil ->
conn

conn
|> assign(:current_character, character)
|> put_session(:current_character_id, character.id)
character ->
conn
|> assign(:current_character, character)
|> put_session(:current_character_id, character.id)
end
end

defp assign_token(conn) do
token = Phoenix.Token.sign(conn, "character socket", conn.assigns.current_character.id)
assign(conn, :character_token, token)
case Map.fetch(conn.assigns, :current_character) do
{:ok, current_character} ->
token = Phoenix.Token.sign(conn, "character socket", current_character.id)
assign(conn, :character_token, token)

:error ->
conn
end
end
end
25 changes: 25 additions & 0 deletions lib/web/plug/public_ensure_character.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule Web.Plug.PublicEnsureCharacter do
@moduledoc false

import Plug.Conn
import Phoenix.Controller, only: [redirect: 2]

alias Web.Router.Helpers, as: Routes

def init(opts), do: opts

def call(conn, _opts) do
case Map.has_key?(conn.assigns, :current_character) do
true ->
conn

false ->
uri = %URI{path: conn.request_path, query: conn.query_string}

conn
|> put_session(:last_path, URI.to_string(uri))
|> redirect(to: Routes.public_character_path(conn, :new))
|> halt()
end
end
end
Loading