-
Notifications
You must be signed in to change notification settings - Fork 64
/
session_blacklist.ex
190 lines (152 loc) · 5.35 KB
/
session_blacklist.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
defmodule RigApi.V2.SessionBlacklist do
@moduledoc """
Controller that allows blocking "sessions" for a specific period of time.
What a session is depends on your business context and the `JWT_SESSION_FIELD`
setting. For example, a session ID could be a random ID assigned to a token upon
login, or the id of the user the token belongs to.
"""
use RigApi, :controller
use PhoenixSwagger
require Logger
alias RIG.Session
@prefix "/v2"
@cors_origins "*"
# ---
@doc false
def handle_preflight(%{method: "OPTIONS"} = conn, _params) do
conn
|> with_allow_origin()
|> put_resp_header("access-control-allow-methods", "POST")
|> put_resp_header("access-control-allow-headers", "content-type")
|> send_resp(:no_content, "")
end
# ---
defp with_allow_origin(conn) do
put_resp_header(conn, "access-control-allow-origin", @cors_origins)
end
# ---
swagger_path :check_status do
get(@prefix <> "/session-blacklist/{sessionId}")
summary("Check whether a given session is currently blacklisted.")
parameters do
sessionId(:path, :string, "The JWT ID (jti) claim that identifies the session.",
required: true
)
end
response(200, "This session is currently blacklisted.")
response(404, "There is no entry in the blacklist that matches this session name.")
end
@doc "Check blacklist status for a specific session id."
def check_status(%{method: "GET"} = conn, %{"session_id" => session_id}) do
if Session.blacklisted?(session_id) do
conn
|> put_resp_header("content-type", "application/json; charset=utf-8")
|> send_resp(:ok, "{}")
else
conn
|> send_resp(:not_found, "Not found.")
end
end
# ---
swagger_path :blacklist_session do
post(@prefix <> "/session-blacklist")
summary("Add a session to the session blacklist.")
description("""
When successful, the given session is no longer considered valid, regardless of \
the token's expiration timestamp. This has the following consequences:
- Any existing connection related to the session is terminated immediately.
- The related authorization token is no longer valid when a client establishes a connection.
- The related authorization token is no longer valid when a client creates a subscription.
""")
parameters do
sessionBlacklist(
:body,
Schema.ref(:SessionBlacklistRequest),
"The details for blacklisting a session",
required: true
)
end
response(
201,
"The session is now blacklisted. The location header points to the newly created entry."
)
response(400, "Bad request.")
end
@doc "Plug action to add a session id to the session blacklist."
def blacklist_session(%{method: "POST"} = conn, _params) do
conn = with_allow_origin(conn)
case parse(conn.body_params) do
{:error, reason} ->
send_resp(conn, :bad_request, reason)
{:ok, %{session_id: session_id, ttl_s: ttl_s}} ->
Session.blacklist(session_id, ttl_s)
location = Path.join(conn.request_path, "/#{session_id}")
conn
|> put_resp_header("location", location)
|> put_resp_header("content-type", "application/json; charset=utf-8")
|> send_resp(:created, "{}")
end
end
# ---
defp parse(body) do
Result.ok(%{})
|> Result.and_then(&parse_and_add_session_id(&1, body))
|> Result.and_then(&parse_and_add_ttl_s(&1, body))
end
# ---
defp parse_and_add_session_id(into, from) do
case Map.fetch(from, "sessionId") do
{:ok, value} when byte_size(value) > 0 -> {:ok, Map.merge(into, %{session_id: value})}
{:ok, value} -> {:error, "Expected non-empty string, got #{inspect(value)}"}
:error -> {:error, "Missing value for \"sessionId\""}
end
end
# ---
defp parse_and_add_ttl_s(into, from) do
case Map.fetch(from, "validityInSeconds") do
{:ok, value} when is_number(value) and value > 0 ->
{:ok, Map.merge(into, %{ttl_s: value})}
{:ok, value} when byte_size(value) > 0 ->
case Integer.parse(value) do
{value, ""} -> parse_and_add_ttl_s(into, Map.put(from, "validityInSeconds", value))
_ -> {:error, "Expected a number, got #{inspect(value)}"}
end
{:ok, value} ->
{:error, "Expected a positive number, got #{inspect(value)}"}
:error ->
{:error, "Missing value for \"validityInSeconds\""}
end
end
# ---
def swagger_definitions do
%{
SessionBlacklistRequest:
swagger_schema do
title("Session Blacklist Request")
properties do
sessionId(
:string,
"""
The JWT claim that defines a "session". For details see the \
`JWT_SESSION_FIELD` setting in the operator's guide \
(https://accenture.github.io/reactive-interaction-gateway/docs/rig-ops-guide.html).
""",
required: true
)
validityInSeconds(
:number,
"""
Defines how long the sessionId will stay on the blacklist. \
Typically set to the JWT's remaining lifetime.
""",
required: true
)
end
example(%{
sessionId: "SomeSessionID123",
validityInSeconds: 60
})
end
}
end
end