Skip to content

Commit de39dff

Browse files
authored
Merge pull request #42 from danschultzer/pow-invitation
Pow invitation
2 parents bf84492 + 2e72541 commit de39dff

File tree

13 files changed

+166
-9
lines changed

13 files changed

+166
-9
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,12 @@ config :my_app, :pow_assent,
308308

309309
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.
310310

311+
### PowInvitation
312+
313+
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.
314+
315+
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.
316+
311317
## Security concerns
312318

313319
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.

config/test.exs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ config :pow_assent, PowAssent.Test.EmailConfirmation.Phoenix.Endpoint,
1313
secret_key_base: String.duplicate("abcdefghijklmnopqrstuvxyz0123456789", 2),
1414
render_errors: [view: PowAssent.Test.Phoenix.ErrorView, accepts: ~w(html json)]
1515

16-
config :pow_assent, PowAssent.Test.NoRegistration.Phoenix.Endpoint,
16+
config :pow_assent, PowAssent.Test.Invitation.Phoenix.Endpoint,
17+
secret_key_base: String.duplicate("abcdefghijklmnopqrstuvxyz0123456789", 2),
18+
render_errors: [view: PowAssent.Test.Phoenix.ErrorView, accepts: ~w(html json)]
19+
20+
config :pow_assent, PowAssent.Test.NoRegistration.Phoenix.Endpoint,
1721
secret_key_base: String.duplicate("abcdefghijklmnopqrstuvxyz0123456789", 2),
1822
render_errors: [view: PowAssent.Test.Phoenix.ErrorView, accepts: ~w(html json)]
1923

lib/pow_assent/ecto/schema.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,19 @@ defmodule PowAssent.Ecto.Schema do
8888
def changeset(user_or_changeset, user_identity, attrs, user_id_attrs, _config) do
8989
user_or_changeset
9090
|> Changeset.change()
91+
|> maybe_accept_invitation()
9192
|> user_id_field_changeset(attrs, user_id_attrs)
9293
|> Changeset.cast(%{user_identities: [user_identity]}, [])
9394
|> Changeset.cast_assoc(:user_identities)
9495
end
9596

97+
defp maybe_accept_invitation(%Changeset{data: %user_mod{invitation_token: token, invitation_accepted_at: nil} = changeset}) when not is_nil(token) do
98+
accepted_at = Pow.Ecto.Schema.__timestamp_for__(user_mod, :invitation_accepted_at)
99+
100+
Changeset.change(changeset, invitation_accepted_at: accepted_at)
101+
end
102+
defp maybe_accept_invitation(changeset), do: changeset
103+
96104
defp user_id_field_changeset(changeset, attrs, nil) do
97105
changeset
98106
|> changeset.data.__struct__.pow_user_id_field_changeset(attrs)

lib/pow_assent/phoenix/controllers/authorization_controller.ex

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule PowAssent.Phoenix.AuthorizationController do
99

1010
plug :require_authenticated when action in [:delete]
1111
plug :assign_callback_url when action in [:new, :callback]
12+
plug :load_user_by_invitation_token when action in [:callback]
1213

1314
@spec process_new(Conn.t(), map()) :: {:ok, binary(), Conn.t()} | {:error, any(), Conn.t()}
1415
def process_new(conn, %{"provider" => provider}) do
@@ -19,13 +20,17 @@ defmodule PowAssent.Phoenix.AuthorizationController do
1920
def respond_new({:ok, url, conn}) do
2021
conn
2122
|> maybe_store_state()
23+
|> maybe_store_invitation_token()
2224
|> redirect(external: url)
2325
end
2426
def respond_new({:error, error, _conn}), do: handle_strategy_error(error)
2527

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

31+
defp maybe_store_invitation_token(%{params: %{"invitation_token" => token}} = conn), do: store_invitation_token(conn, token)
32+
defp maybe_store_invitation_token(conn), do: conn
33+
2934
@spec process_callback(Conn.t(), map()) :: {:ok, Conn.t()} | {:error, Conn.t()} | {:error, {atom(), map()} | map(), Conn.t()}
3035
def process_callback(conn, %{"provider" => provider} = params) do
3136
conn
@@ -41,10 +46,13 @@ defmodule PowAssent.Phoenix.AuthorizationController do
4146
end
4247
end
4348

44-
defp handle_callback({:ok, user, conn}, provider) do
49+
defp handle_callback({:ok, user_params, %{assigns: %{invited_user: invited_user}} = conn}, provider) do
50+
authenticate_or_create_identity(invited_user, provider, user_params, conn)
51+
end
52+
defp handle_callback({:ok, user_params, conn}, provider) do
4553
conn
4654
|> Pow.Plug.current_user()
47-
|> authenticate_or_create_identity(provider, user, conn)
55+
|> authenticate_or_create_identity(provider, user_params, conn)
4856
end
4957
defp handle_callback({:error, error, _conn}, _provider), do: handle_strategy_error(error)
5058

@@ -154,14 +162,26 @@ defmodule PowAssent.Phoenix.AuthorizationController do
154162
assign(conn, :callback_url, url)
155163
end
156164

157-
defp store_state(conn, state) do
158-
Conn.put_session(conn, :pow_assent_state, state)
159-
end
165+
defp store_state(conn, state), do: Conn.put_session(conn, :pow_assent_state, state)
160166

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

172+
defp store_invitation_token(conn, token), do: Conn.put_session(conn, :pow_assent_invitation_token, token)
173+
174+
defp load_user_by_invitation_token(%{private: %{plug_session: %{"pow_assent_invitation_token" => token}}} = conn, _opts) do
175+
conn = Conn.delete_session(conn,:pow_assent_invitation_token)
176+
177+
conn
178+
|> PowInvitation.Plug.invited_user_from_token(token)
179+
|> case do
180+
nil -> conn
181+
user -> PowInvitation.Plug.assign_invited_user(conn, user)
182+
end
183+
end
184+
defp load_user_by_invitation_token(conn, _opts), do: conn
185+
166186
defp handle_strategy_error(error), do: raise error
167187
end

lib/pow_assent/phoenix/views/view_helpers.ex

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,14 @@ defmodule PowAssent.Phoenix.ViewHelpers do
3333
end
3434
end
3535

36-
defp oauth_signin_link(conn, provider) do
36+
defp oauth_signin_link(%{assigns: %{invited_user: %{invitation_token: token}}} = conn, provider) when not is_nil(token) do
37+
do_oauth_signin_link(conn, provider, invitation_token: token)
38+
end
39+
defp oauth_signin_link(conn, provider), do: do_oauth_signin_link(conn, provider)
40+
41+
defp do_oauth_signin_link(conn, provider, query_params \\[]) do
3742
msg = AuthorizationController.messages(conn).login_with_provider(%{conn | params: %{"provider" => provider}})
38-
path = AuthorizationController.routes(conn).path_for(conn, AuthorizationController, :new, [provider])
43+
path = AuthorizationController.routes(conn).path_for(conn, AuthorizationController, :new, [provider], query_params)
3944

4045
Link.link(msg, to: path)
4146
end

test/pow_assent/ecto/schema_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ defmodule PowAssent.Ecto.SchemaTest do
1717

1818
alias PowAssent.Test.Ecto.{Repo, Users.User}
1919
alias PowAssent.Test.EmailConfirmation.Users.User, as: UserConfirmEmail
20+
alias PowAssent.Test.Invitation.Users.User, as: InvitationUser
2021

2122
test "user_schema/1" do
2223
user = %User{}
@@ -65,6 +66,17 @@ defmodule PowAssent.Ecto.SchemaTest do
6566
changeset = User.user_identity_changeset(%User{}, @user_identity, %{email: "test@example.com"}, nil)
6667
refute changeset.changes[:email_confirmed_at]
6768
end
69+
70+
test "sets :invitation_accepted_at when is invited user" do
71+
changeset = InvitationUser.user_identity_changeset(%InvitationUser{}, @user_identity, %{}, %{email: "test@example.com"})
72+
refute changeset.changes[:invitation_accepted_at]
73+
74+
changeset = InvitationUser.user_identity_changeset(%InvitationUser{invitation_token: "token", invitation_accepted_at: DateTime.utc_now()}, @user_identity, %{}, %{email: "test@example.com"})
75+
refute changeset.changes[:invitation_accepted_at]
76+
77+
changeset = InvitationUser.user_identity_changeset(%InvitationUser{invitation_token: "token"}, @user_identity, %{}, %{email: "test@example.com"})
78+
assert changeset.changes[:invitation_accepted_at]
79+
end
6880
end
6981

7082
defmodule OverrideAssocUser do

test/pow_assent/phoenix/controllers/authorization_controller_test.exs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ defmodule PowAssent.Phoenix.AuthorizationControllerTest do
2525
assert Plug.Conn.get_session(conn, :pow_assent_state)
2626
end
2727

28+
test "redirects with invitation_token saved", %{conn: conn} do
29+
conn = get conn, Routes.pow_assent_authorization_path(conn, :new, @provider, invitation_token: "token")
30+
31+
assert Plug.Conn.get_session(conn, :pow_assent_invitation_token) == "token"
32+
end
33+
2834
test "with error", %{conn: conn, bypass: bypass} do
2935
put_oauth2_env(bypass, fail_authorize_url: true)
3036

@@ -86,7 +92,6 @@ defmodule PowAssent.Phoenix.AuthorizationControllerTest do
8692

8793
assert redirected_to(conn) == "/session_created"
8894
assert get_flash(conn, :info) == "signed_in_test_provider"
89-
assert Pow.Plug.current_user(conn) == user
9095
refute Plug.Conn.get_session(conn, :pow_assent_state)
9196
end
9297

@@ -167,6 +172,24 @@ defmodule PowAssent.Phoenix.AuthorizationControllerTest do
167172
end
168173
end
169174

175+
alias PowAssent.Test.Invitation.Phoenix.Endpoint, as: InvitationEndpoint
176+
describe "GET /auth/:provider/callback as authentication with invitation" do
177+
test "with invitation_token updates user as accepted invtation", %{conn: conn, bypass: bypass} do
178+
expect_oauth2_flow(bypass, user: %{uid: "new_identity"})
179+
180+
conn =
181+
conn
182+
|> Plug.Conn.put_session(:pow_assent_state, "token")
183+
|> Plug.Conn.put_session(:pow_assent_invitation_token, "token")
184+
|> Phoenix.ConnTest.dispatch(InvitationEndpoint, :get, Routes.pow_assent_authorization_path(conn, :callback, @provider, @callback_params))
185+
186+
assert redirected_to(conn) == "/session_created"
187+
assert get_flash(conn, :info) == "signed_in_test_provider"
188+
refute Plug.Conn.get_session(conn, :pow_assent_invitation_token)
189+
refute Plug.Conn.get_session(conn, :pow_assent_state)
190+
end
191+
end
192+
170193
alias PowAssent.Test.NoRegistration.Phoenix.Endpoint, as: NoRegistrationEndpoint
171194
describe "GET /auth/:provider/callback as authentication with missing registration routes" do
172195
test "can't register", %{conn: conn, bypass: bypass} do

test/pow_assent/phoenix/views/view_helpers_test.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,11 @@ defmodule PowAssent.ViewHelpersTest do
3535
[safe: iodata] = ViewHelpers.provider_links(conn)
3636
assert {:safe, iodata} == Link.link("Remove Test provider authentication", to: "/auth/test_provider", method: "delete")
3737
end
38+
39+
test "provider_links/1 with invited_user", %{conn: conn} do
40+
conn = PowInvitation.Plug.assign_invited_user(conn, %PowAssent.Test.Invitation.Users.User{invitation_token: "token"})
41+
42+
[safe: iodata] = ViewHelpers.provider_links(conn)
43+
assert {:safe, iodata} == Link.link("Sign in with Test provider", to: "/auth/test_provider/new?invitation_token=token")
44+
end
3845
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule PowAssent.Test.Invitation.Phoenix.Endpoint do
2+
@moduledoc false
3+
use Phoenix.Endpoint, otp_app: :pow_assent
4+
5+
plug Plug.RequestId
6+
plug Plug.Logger
7+
8+
plug Plug.Parsers,
9+
parsers: [:urlencoded, :multipart, :json],
10+
pass: ["*/*"],
11+
json_decoder: Phoenix.json_library()
12+
13+
plug Plug.MethodOverride
14+
plug Plug.Head
15+
16+
plug Plug.Session,
17+
store: :cookie,
18+
key: "_binaryid_key",
19+
signing_salt: "secret"
20+
21+
plug Pow.Plug.Session,
22+
user: PowAssent.Test.Invitation.Users.User,
23+
routes_backend: PowAssent.Test.Phoenix.Routes,
24+
messages_backend: PowAssent.Test.Phoenix.Messages,
25+
mailer_backend: PowAssent.Test.Phoenix.MailerMock,
26+
repo: PowAssent.Test.Invitation.RepoMock,
27+
otp_app: :pow_assent,
28+
pow_assent: [
29+
user_identities_context: PowAssent.Test.UserIdentitiesMock
30+
]
31+
32+
plug PowAssent.Test.Phoenix.Router
33+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule PowAssent.Test.Invitation.RepoMock do
2+
@moduledoc false
3+
4+
alias PowAssent.Test.Invitation.Users.User
5+
6+
@user %User{id: 1, email: "test@example.com"}
7+
8+
def get_by(User, [invitation_token: "token"]), do: %{@user | invitation_token: "token"}
9+
end

0 commit comments

Comments
 (0)