-
Notifications
You must be signed in to change notification settings - Fork 36
/
helpers.ex
201 lines (166 loc) · 6.44 KB
/
helpers.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
defmodule AshAuthentication.Plug.Helpers do
@moduledoc """
Authentication helpers for use in your router, etc.
"""
alias Ash.{PlugHelpers, Resource}
alias AshAuthentication.{Info, Jwt, TokenResource}
alias Plug.Conn
@doc """
Store the user in the connections' session.
"""
@spec store_in_session(Conn.t(), Resource.record()) :: Conn.t()
def store_in_session(conn, user) when is_struct(user) do
subject_name = Info.authentication_subject_name!(user.__struct__)
subject = AshAuthentication.user_to_subject(user)
Conn.put_session(conn, subject_name, subject)
end
def store_in_session(conn, _), do: conn
@doc """
Given a list of subjects, turn as many as possible into users.
"""
@spec load_subjects([AshAuthentication.subject()], module) :: map
def load_subjects(subjects, otp_app) when is_list(subjects) do
resources =
otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(&{to_string(Info.authentication_subject_name!(&1)), &1})
|> Map.new()
subjects
|> Enum.reduce(%{}, fn subject, result ->
subject = URI.parse(subject)
with {:ok, resource} <- Map.fetch(resources, subject.path),
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource),
{:ok, subject_name} <- Info.authentication_subject_name(resource) do
current_subject_name = current_subject_name(subject_name)
Map.put(result, current_subject_name, user)
else
_ -> result
end
end)
end
@doc """
Attempt to retrieve all users from the connections' session.
Iterates through all configured authentication resources for `otp_app` and
retrieves any users stored in the session, loads them and stores them in the
assigns under their subject name (with the prefix `current_`).
If there is no user present for a resource then the assign is set to `nil`.
"""
@spec retrieve_from_session(Conn.t(), module) :: Conn.t()
def retrieve_from_session(conn, otp_app) do
otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(&{&1, Info.authentication_options(&1)})
|> Enum.reduce(conn, fn {resource, options}, conn ->
current_subject_name = current_subject_name(options.subject_name)
with subject when is_binary(subject) <- Conn.get_session(conn, options.subject_name),
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource) do
Conn.assign(conn, current_subject_name, user)
else
_ ->
Conn.assign(conn, current_subject_name, nil)
end
end)
end
@doc """
Validate authorization header(s).
Assumes that your clients are sending a bearer-style authorization header with
your request. If a valid bearer token is present then the subject is loaded
into the assigns under their subject name (with the prefix `current_`).
If there is no user present for a resource then the assign is set to `nil`.
"""
@spec retrieve_from_bearer(Conn.t(), module) :: Conn.t()
def retrieve_from_bearer(conn, otp_app) do
conn
|> Conn.get_req_header("authorization")
|> Stream.filter(&String.starts_with?(&1, "Bearer "))
|> Stream.map(&String.replace_leading(&1, "Bearer ", ""))
|> Enum.reduce(conn, fn token, conn ->
with {:ok, %{"sub" => subject}, resource} <- Jwt.verify(token, otp_app),
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource),
{:ok, subject_name} <- Info.authentication_subject_name(resource),
current_subject_name <- current_subject_name(subject_name) do
conn
|> Conn.assign(current_subject_name, user)
else
_ -> conn
end
end)
end
@doc """
Revoke all authorization header(s).
Any bearer-style authorization headers will have their tokens revoked.
"""
@spec revoke_bearer_tokens(Conn.t(), module) :: Conn.t()
def revoke_bearer_tokens(conn, otp_app) do
conn
|> Conn.get_req_header("authorization")
|> Stream.filter(&String.starts_with?(&1, "Bearer "))
|> Stream.map(&String.replace_leading(&1, "Bearer ", ""))
|> Enum.reduce(conn, fn token, conn ->
with {:ok, resource} <- Jwt.token_to_resource(token, otp_app),
{:ok, token_resource} <- Info.authentication_tokens_token_resource(resource),
:ok <- TokenResource.Actions.revoke(token_resource, token) do
conn
else
_ -> conn
end
end)
end
@doc """
Set a subject as the request actor.
Presumes that you have already loaded your user resource(s) into the
connection's assigns.
Uses `Ash.PlugHelpers` to streamline integration with `AshGraphql` and
`AshJsonApi`.
## Examples
Setting the actor for a AshGraphql API using `Plug.Router`.
```elixir
defmodule MyApp.ApiRouter do
use Plug.Router
import MyApp.AuthPlug
plug :retrieve_from_bearer
plug :set_actor, :user
forward "/gql",
to: Absinthe.Plug,
init_opts: [schema: MyApp.Schema]
end
```
"""
@spec set_actor(Conn.t(), subject_name :: atom) :: Conn.t()
def set_actor(conn, subject_name) do
current_subject_name =
subject_name
|> current_subject_name()
actor =
conn
|> Map.get(:assigns, %{})
|> Map.get(current_subject_name)
conn
|> PlugHelpers.set_actor(actor)
end
@doc """
Store result in private.
This is used by authentication plug handlers to store their result for passing
back to the dispatcher.
"""
@spec store_authentication_result(
Conn.t(),
:ok | {:ok, Resource.record()} | :error | {:error, any}
) ::
Conn.t()
def store_authentication_result(conn, :ok),
do: Conn.put_private(conn, :authentication_result, {:ok, nil})
def store_authentication_result(conn, {:ok, record}),
do: Conn.put_private(conn, :authentication_result, {:ok, record})
def store_authentication_result(conn, :error),
do: Conn.put_private(conn, :authentication_result, :error)
def store_authentication_result(conn, {:error, reason}),
do: Conn.put_private(conn, :authentication_result, {:error, reason})
def get_authentication_result(%{private: %{authentication_result: result}} = conn),
do: {conn, result}
def get_authentication_result(conn), do: conn
# Dyanamically generated atoms are generally frowned upon, but in this case
# the `subject_name` is a statically configured atom, so should be fine.
defp current_subject_name(subject_name) when is_atom(subject_name),
do: String.to_atom("current_#{subject_name}")
end