Skip to content

Commit

Permalink
Merge pull request #42 from danschultzer/pow-invitation
Browse files Browse the repository at this point in the history
Pow invitation
  • Loading branch information
danschultzer committed Mar 9, 2019
2 parents bf84492 + 2e72541 commit de39dff
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 9 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@ config :my_app, :pow_assent,

The e-mail fetched from the provider is assumed already confirmed, and the user will have `:email_confirmed_at` set when inserted. If a user enters an e-mail then the user will have to confirm their e-mail before they can sign in.

### PowInvitation

PowAssent works out of the box with PowInvitation. If a user identity is created, the an invited user will have the `:invitation_accepted_at` set.

Provider links will have an `invitation_token` query param if an invited user exists in the connection. This will be used in the authorization callback flow to load the invited user.

## Security concerns

All sessions created through PowAssent provider authentication are temporary. However, it's a good idea to do some housekeeping in your app and make sure that you have the level of security as warranted by the scope of your app. That may include requiring users to re-authenticate before viewing or editing their user details.
Expand Down
6 changes: 5 additions & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ config :pow_assent, PowAssent.Test.EmailConfirmation.Phoenix.Endpoint,
secret_key_base: String.duplicate("abcdefghijklmnopqrstuvxyz0123456789", 2),
render_errors: [view: PowAssent.Test.Phoenix.ErrorView, accepts: ~w(html json)]

config :pow_assent, PowAssent.Test.NoRegistration.Phoenix.Endpoint,
config :pow_assent, PowAssent.Test.Invitation.Phoenix.Endpoint,
secret_key_base: String.duplicate("abcdefghijklmnopqrstuvxyz0123456789", 2),
render_errors: [view: PowAssent.Test.Phoenix.ErrorView, accepts: ~w(html json)]

config :pow_assent, PowAssent.Test.NoRegistration.Phoenix.Endpoint,
secret_key_base: String.duplicate("abcdefghijklmnopqrstuvxyz0123456789", 2),
render_errors: [view: PowAssent.Test.Phoenix.ErrorView, accepts: ~w(html json)]

Expand Down
8 changes: 8 additions & 0 deletions lib/pow_assent/ecto/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,19 @@ defmodule PowAssent.Ecto.Schema do
def changeset(user_or_changeset, user_identity, attrs, user_id_attrs, _config) do
user_or_changeset
|> Changeset.change()
|> maybe_accept_invitation()
|> user_id_field_changeset(attrs, user_id_attrs)
|> Changeset.cast(%{user_identities: [user_identity]}, [])
|> Changeset.cast_assoc(:user_identities)
end

defp maybe_accept_invitation(%Changeset{data: %user_mod{invitation_token: token, invitation_accepted_at: nil} = changeset}) when not is_nil(token) do
accepted_at = Pow.Ecto.Schema.__timestamp_for__(user_mod, :invitation_accepted_at)

Changeset.change(changeset, invitation_accepted_at: accepted_at)
end
defp maybe_accept_invitation(changeset), do: changeset

defp user_id_field_changeset(changeset, attrs, nil) do
changeset
|> changeset.data.__struct__.pow_user_id_field_changeset(attrs)
Expand Down
30 changes: 25 additions & 5 deletions lib/pow_assent/phoenix/controllers/authorization_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule PowAssent.Phoenix.AuthorizationController do

plug :require_authenticated when action in [:delete]
plug :assign_callback_url when action in [:new, :callback]
plug :load_user_by_invitation_token when action in [:callback]

@spec process_new(Conn.t(), map()) :: {:ok, binary(), Conn.t()} | {:error, any(), Conn.t()}
def process_new(conn, %{"provider" => provider}) do
Expand All @@ -19,13 +20,17 @@ defmodule PowAssent.Phoenix.AuthorizationController do
def respond_new({:ok, url, conn}) do
conn
|> maybe_store_state()
|> maybe_store_invitation_token()
|> redirect(external: url)
end
def respond_new({:error, error, _conn}), do: handle_strategy_error(error)

defp maybe_store_state(%{private: %{pow_assent_state: state}} = conn), do: store_state(conn, state)
defp maybe_store_state(conn), do: conn

defp maybe_store_invitation_token(%{params: %{"invitation_token" => token}} = conn), do: store_invitation_token(conn, token)
defp maybe_store_invitation_token(conn), do: conn

@spec process_callback(Conn.t(), map()) :: {:ok, Conn.t()} | {:error, Conn.t()} | {:error, {atom(), map()} | map(), Conn.t()}
def process_callback(conn, %{"provider" => provider} = params) do
conn
Expand All @@ -41,10 +46,13 @@ defmodule PowAssent.Phoenix.AuthorizationController do
end
end

defp handle_callback({:ok, user, conn}, provider) do
defp handle_callback({:ok, user_params, %{assigns: %{invited_user: invited_user}} = conn}, provider) do
authenticate_or_create_identity(invited_user, provider, user_params, conn)
end
defp handle_callback({:ok, user_params, conn}, provider) do
conn
|> Pow.Plug.current_user()
|> authenticate_or_create_identity(provider, user, conn)
|> authenticate_or_create_identity(provider, user_params, conn)
end
defp handle_callback({:error, error, _conn}, _provider), do: handle_strategy_error(error)

Expand Down Expand Up @@ -154,14 +162,26 @@ defmodule PowAssent.Phoenix.AuthorizationController do
assign(conn, :callback_url, url)
end

defp store_state(conn, state) do
Conn.put_session(conn, :pow_assent_state, state)
end
defp store_state(conn, state), do: Conn.put_session(conn, :pow_assent_state, state)

defp fetch_state(%{private: %{plug_session: %{"pow_assent_state" => state}}} = conn) do
{state, Conn.put_session(conn, :pow_assent_state, nil)}
end
defp fetch_state(conn), do: conn

defp store_invitation_token(conn, token), do: Conn.put_session(conn, :pow_assent_invitation_token, token)

defp load_user_by_invitation_token(%{private: %{plug_session: %{"pow_assent_invitation_token" => token}}} = conn, _opts) do
conn = Conn.delete_session(conn,:pow_assent_invitation_token)

conn
|> PowInvitation.Plug.invited_user_from_token(token)
|> case do
nil -> conn
user -> PowInvitation.Plug.assign_invited_user(conn, user)
end
end
defp load_user_by_invitation_token(conn, _opts), do: conn

defp handle_strategy_error(error), do: raise error
end
9 changes: 7 additions & 2 deletions lib/pow_assent/phoenix/views/view_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@ defmodule PowAssent.Phoenix.ViewHelpers do
end
end

defp oauth_signin_link(conn, provider) do
defp oauth_signin_link(%{assigns: %{invited_user: %{invitation_token: token}}} = conn, provider) when not is_nil(token) do
do_oauth_signin_link(conn, provider, invitation_token: token)
end
defp oauth_signin_link(conn, provider), do: do_oauth_signin_link(conn, provider)

defp do_oauth_signin_link(conn, provider, query_params \\[]) do
msg = AuthorizationController.messages(conn).login_with_provider(%{conn | params: %{"provider" => provider}})
path = AuthorizationController.routes(conn).path_for(conn, AuthorizationController, :new, [provider])
path = AuthorizationController.routes(conn).path_for(conn, AuthorizationController, :new, [provider], query_params)

Link.link(msg, to: path)
end
Expand Down
12 changes: 12 additions & 0 deletions test/pow_assent/ecto/schema_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule PowAssent.Ecto.SchemaTest do

alias PowAssent.Test.Ecto.{Repo, Users.User}
alias PowAssent.Test.EmailConfirmation.Users.User, as: UserConfirmEmail
alias PowAssent.Test.Invitation.Users.User, as: InvitationUser

test "user_schema/1" do
user = %User{}
Expand Down Expand Up @@ -65,6 +66,17 @@ defmodule PowAssent.Ecto.SchemaTest do
changeset = User.user_identity_changeset(%User{}, @user_identity, %{email: "[email protected]"}, nil)
refute changeset.changes[:email_confirmed_at]
end

test "sets :invitation_accepted_at when is invited user" do
changeset = InvitationUser.user_identity_changeset(%InvitationUser{}, @user_identity, %{}, %{email: "[email protected]"})
refute changeset.changes[:invitation_accepted_at]

changeset = InvitationUser.user_identity_changeset(%InvitationUser{invitation_token: "token", invitation_accepted_at: DateTime.utc_now()}, @user_identity, %{}, %{email: "[email protected]"})
refute changeset.changes[:invitation_accepted_at]

changeset = InvitationUser.user_identity_changeset(%InvitationUser{invitation_token: "token"}, @user_identity, %{}, %{email: "[email protected]"})
assert changeset.changes[:invitation_accepted_at]
end
end

defmodule OverrideAssocUser do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ defmodule PowAssent.Phoenix.AuthorizationControllerTest do
assert Plug.Conn.get_session(conn, :pow_assent_state)
end

test "redirects with invitation_token saved", %{conn: conn} do
conn = get conn, Routes.pow_assent_authorization_path(conn, :new, @provider, invitation_token: "token")

assert Plug.Conn.get_session(conn, :pow_assent_invitation_token) == "token"
end

test "with error", %{conn: conn, bypass: bypass} do
put_oauth2_env(bypass, fail_authorize_url: true)

Expand Down Expand Up @@ -86,7 +92,6 @@ defmodule PowAssent.Phoenix.AuthorizationControllerTest do

assert redirected_to(conn) == "/session_created"
assert get_flash(conn, :info) == "signed_in_test_provider"
assert Pow.Plug.current_user(conn) == user
refute Plug.Conn.get_session(conn, :pow_assent_state)
end

Expand Down Expand Up @@ -167,6 +172,24 @@ defmodule PowAssent.Phoenix.AuthorizationControllerTest do
end
end

alias PowAssent.Test.Invitation.Phoenix.Endpoint, as: InvitationEndpoint
describe "GET /auth/:provider/callback as authentication with invitation" do
test "with invitation_token updates user as accepted invtation", %{conn: conn, bypass: bypass} do
expect_oauth2_flow(bypass, user: %{uid: "new_identity"})

conn =
conn
|> Plug.Conn.put_session(:pow_assent_state, "token")
|> Plug.Conn.put_session(:pow_assent_invitation_token, "token")
|> Phoenix.ConnTest.dispatch(InvitationEndpoint, :get, Routes.pow_assent_authorization_path(conn, :callback, @provider, @callback_params))

assert redirected_to(conn) == "/session_created"
assert get_flash(conn, :info) == "signed_in_test_provider"
refute Plug.Conn.get_session(conn, :pow_assent_invitation_token)
refute Plug.Conn.get_session(conn, :pow_assent_state)
end
end

alias PowAssent.Test.NoRegistration.Phoenix.Endpoint, as: NoRegistrationEndpoint
describe "GET /auth/:provider/callback as authentication with missing registration routes" do
test "can't register", %{conn: conn, bypass: bypass} do
Expand Down
7 changes: 7 additions & 0 deletions test/pow_assent/phoenix/views/view_helpers_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,11 @@ defmodule PowAssent.ViewHelpersTest do
[safe: iodata] = ViewHelpers.provider_links(conn)
assert {:safe, iodata} == Link.link("Remove Test provider authentication", to: "/auth/test_provider", method: "delete")
end

test "provider_links/1 with invited_user", %{conn: conn} do
conn = PowInvitation.Plug.assign_invited_user(conn, %PowAssent.Test.Invitation.Users.User{invitation_token: "token"})

[safe: iodata] = ViewHelpers.provider_links(conn)
assert {:safe, iodata} == Link.link("Sign in with Test provider", to: "/auth/test_provider/new?invitation_token=token")
end
end
33 changes: 33 additions & 0 deletions test/support/extensions/invitation/phoenix/endpoint.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule PowAssent.Test.Invitation.Phoenix.Endpoint do
@moduledoc false
use Phoenix.Endpoint, otp_app: :pow_assent

plug Plug.RequestId
plug Plug.Logger

plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()

plug Plug.MethodOverride
plug Plug.Head

plug Plug.Session,
store: :cookie,
key: "_binaryid_key",
signing_salt: "secret"

plug Pow.Plug.Session,
user: PowAssent.Test.Invitation.Users.User,
routes_backend: PowAssent.Test.Phoenix.Routes,
messages_backend: PowAssent.Test.Phoenix.Messages,
mailer_backend: PowAssent.Test.Phoenix.MailerMock,
repo: PowAssent.Test.Invitation.RepoMock,
otp_app: :pow_assent,
pow_assent: [
user_identities_context: PowAssent.Test.UserIdentitiesMock
]

plug PowAssent.Test.Phoenix.Router
end
9 changes: 9 additions & 0 deletions test/support/extensions/invitation/repo_mock.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule PowAssent.Test.Invitation.RepoMock do
@moduledoc false

alias PowAssent.Test.Invitation.Users.User

@user %User{id: 1, email: "[email protected]"}

def get_by(User, [invitation_token: "token"]), do: %{@user | invitation_token: "token"}
end
19 changes: 19 additions & 0 deletions test/support/extensions/invitation/user.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule PowAssent.Test.Invitation.Users.User do
@moduledoc false
use Ecto.Schema
use Pow.Ecto.Schema
use Pow.Extension.Ecto.Schema,
extensions: [PowInvitation]
use PowAssent.Ecto.Schema

schema "users" do
has_many :user_identities,
PowAssent.Test.Invitation.UserIdentities.UserIdentity,
on_delete: :delete_all,
foreign_key: :user_id

pow_user_fields()

timestamps()
end
end
10 changes: 10 additions & 0 deletions test/support/extensions/invitation/user_identity.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule PowAssent.Test.Invitation.UserIdentities.UserIdentity do
@moduledoc false
use Ecto.Schema
use PowAssent.Ecto.UserIdentities.Schema,
user: PowAssent.Test.Invitation.Users.User

schema "user_identities" do
pow_assent_user_identity_fields()
end
end
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ Ecto.Adapters.SQL.Sandbox.mode(PowAssent.Test.Ecto.Repo, :manual)

{:ok, _pid} = PowAssent.Test.Phoenix.Endpoint.start_link()
{:ok, _pid} = PowAssent.Test.EmailConfirmation.Phoenix.Endpoint.start_link()
{:ok, _pid} = PowAssent.Test.Invitation.Phoenix.Endpoint.start_link()
{:ok, _pid} = PowAssent.Test.NoRegistration.Phoenix.Endpoint.start_link()

0 comments on commit de39dff

Please sign in to comment.