Skip to content

Commit 6a7b68e

Browse files
author
Al Davidson
authored
Merge pull request #4260 from nhsuk/add-token-auth-concern-and-spec
Add token_authentication_concern and spec
2 parents c6abeb5 + 98639b0 commit 6a7b68e

File tree

2 files changed

+378
-0
lines changed

2 files changed

+378
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
module ReportingAPI::TokenAuthenticationConcern
4+
extend ActiveSupport::Concern
5+
6+
included do
7+
private
8+
9+
def client_id_error!(token)
10+
if token.blank?
11+
render json: { errors: "invalid_request" }, status: :unauthorized
12+
else
13+
render json: { errors: "unauthorized_client" }, status: :forbidden
14+
end
15+
end
16+
17+
def authenticate_app_by_client_id!
18+
if Flipper.enabled?(:reporting_api)
19+
# ...as per the spec at https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
20+
given_client_id = params.fetch("client_id", nil)
21+
unless given_client_id == Settings.reporting_api.client_app.client_id
22+
client_id_error!(given_client_id)
23+
end
24+
end
25+
end
26+
27+
def jwt_if_given
28+
params[:jwt] ||
29+
request.headers["Authorization"]&.gsub(/(Bearer\s+)?([:alnum:]*)/, '\2')
30+
end
31+
32+
def authenticate_user_by_jwt!
33+
jwt = jwt_if_given
34+
jwt_info = decode_jwt!(jwt)
35+
if jwt_info
36+
data = jwt_info.first["data"]
37+
@current_user =
38+
User.find_by(
39+
data.fetch("user", {}).slice(
40+
"id",
41+
"session_token",
42+
"reporting_api_session_token"
43+
)
44+
)
45+
if @current_user
46+
session["user"] = data["user"]
47+
session["cis2_info"] = data["cis2_info"]
48+
authenticate_user!
49+
else
50+
session.clear
51+
client_id_error!(jwt)
52+
Rails.logger.warn "Couldn't find user id #{data.dig("user", "id")} with tokens"
53+
end
54+
end
55+
rescue JWT::DecodeError, NoMethodError
56+
Rails.logger.warn "invalid JWT"
57+
client_id_error!(jwt)
58+
end
59+
end
60+
61+
def decode_jwt!(jwt)
62+
if jwt
63+
JWT.decode(
64+
jwt,
65+
Settings.reporting_api.client_app.secret,
66+
true,
67+
{ algorithm: "HS512" }
68+
)
69+
end
70+
end
71+
end
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
# frozen_string_literal: true
2+
3+
describe ReportingAPI::TokenAuthenticationConcern do
4+
let(:user) { @user = build(:user) }
5+
let(:mock_request) { instance_double(request.class, headers: {}) }
6+
let(:an_object_which_includes_the_concern) do
7+
Class
8+
.new do # rubocop:disable Style/BlockDelimiters
9+
include ReportingAPI::TokenAuthenticationConcern
10+
attr_accessor :request, :session
11+
12+
def authenticate_user!
13+
end
14+
15+
def initialize(request: nil, session: {})
16+
@request = request
17+
@session = session
18+
end
19+
20+
def params
21+
{}
22+
end
23+
24+
def render(content = {}, args = {})
25+
end
26+
27+
def current_user
28+
@user
29+
end
30+
end # rubocop:disable Style/MethodCalledOnDoEndBlock
31+
.new(request: mock_request)
32+
end
33+
34+
describe "#jwt_if_given" do
35+
context "when there is a jwt param" do
36+
before do
37+
allow(an_object_which_includes_the_concern).to receive(
38+
:params
39+
).and_return({ jwt: "myjwt" })
40+
end
41+
42+
it "returns the jwt param" do
43+
expect(an_object_which_includes_the_concern.send(:jwt_if_given)).to eq(
44+
"myjwt"
45+
)
46+
end
47+
end
48+
49+
context "when there is no jwt param" do
50+
context "but there is an Authorization header" do
51+
before do
52+
an_object_which_includes_the_concern.request =
53+
instance_double(
54+
request.class,
55+
headers: {
56+
"Authorization" => "Bearer myjwt"
57+
}
58+
)
59+
end
60+
61+
it "returns the value of the Authorization header, without the leading 'Bearer'" do
62+
result = an_object_which_includes_the_concern.send(:jwt_if_given)
63+
expect(result).to eq("myjwt")
64+
end
65+
end
66+
67+
context "and there is no Authorization header" do
68+
it "returns nil" do
69+
expect(
70+
an_object_which_includes_the_concern.send(:jwt_if_given)
71+
).to be_nil
72+
end
73+
end
74+
end
75+
end
76+
77+
describe "#authenticate_app_by_client_id!" do
78+
let(:client_id) { "something" }
79+
80+
context "when the :reporting_api feature flag is enabled" do
81+
before { Flipper.enable(:reporting_api) }
82+
83+
context "and the client_id param is provided" do
84+
before do
85+
allow(an_object_which_includes_the_concern).to receive(
86+
:params
87+
).and_return({ client_id: client_id }.with_indifferent_access)
88+
end
89+
90+
context "and the client_id param contains the reporting app's client_id" do
91+
let(:client_id) { Settings.reporting_api.client_app.client_id }
92+
93+
it "does not cause a token error" do
94+
expect(an_object_which_includes_the_concern).not_to receive(
95+
:client_id_error!
96+
)
97+
an_object_which_includes_the_concern.send(
98+
:authenticate_app_by_client_id!
99+
)
100+
end
101+
end
102+
103+
context "and the client_id param does not contain the reporting app client_id" do
104+
it "causes a token error" do
105+
expect(an_object_which_includes_the_concern).to receive(
106+
:client_id_error!
107+
)
108+
an_object_which_includes_the_concern.send(
109+
:authenticate_app_by_client_id!
110+
)
111+
end
112+
end
113+
end
114+
end
115+
end
116+
117+
describe "#client_id_error!" do
118+
context "given an empty token" do
119+
let(:token) { "" }
120+
121+
it "renders a invalid_request error, with status :unauthorized" do
122+
expect(an_object_which_includes_the_concern).to receive(:render).with(
123+
json: {
124+
errors: "invalid_request"
125+
},
126+
status: :unauthorized
127+
)
128+
an_object_which_includes_the_concern.send(:client_id_error!, token)
129+
end
130+
end
131+
132+
context "given a token that is not empty, but does not match the reporting app's client_id" do
133+
let(:token) { "unmatched token" }
134+
135+
it "renders a unauthorized_client error, with status forbidden" do
136+
expect(an_object_which_includes_the_concern).to receive(:render).with(
137+
json: {
138+
errors: "unauthorized_client"
139+
},
140+
status: :forbidden
141+
)
142+
an_object_which_includes_the_concern.send(:client_id_error!, token)
143+
end
144+
end
145+
end
146+
147+
describe "#authenticate_user_by_jwt!" do
148+
let(:jwt) { "" }
149+
let(:user_id) { 0 }
150+
let(:session_token) { "123456abcdef" }
151+
let(:reporting_api_session_token) { "0987654321123456abcdef" }
152+
153+
let(:user) do
154+
create(
155+
:user,
156+
session_token: session_token,
157+
reporting_api_session_token: reporting_api_session_token
158+
)
159+
end
160+
161+
before do
162+
an_object_which_includes_the_concern.request =
163+
instance_double(request.class, headers: { "Authorization" => jwt })
164+
end
165+
166+
context "when a valid jwt is given" do
167+
let(:jwt) { "validjwt" }
168+
let(:user_info) do
169+
[
170+
{
171+
"data" => {
172+
"user" => {
173+
"id" => user_id,
174+
"session_token" => session_token,
175+
"reporting_api_session_token" => reporting_api_session_token
176+
},
177+
"cis2_info" => {
178+
"some_key" => "some value"
179+
}
180+
}
181+
}
182+
]
183+
end
184+
185+
before do
186+
allow(an_object_which_includes_the_concern).to receive(
187+
:decode_jwt!
188+
).with(jwt).and_return(user_info)
189+
allow(an_object_which_includes_the_concern).to receive(
190+
:authenticate_user!
191+
)
192+
end
193+
194+
it "decodes the JWT" do
195+
expect(an_object_which_includes_the_concern).to receive(
196+
:decode_jwt!
197+
).with(jwt)
198+
an_object_which_includes_the_concern.send(:authenticate_user_by_jwt!)
199+
end
200+
201+
context "when a User exists with the values of id, session_token and reporting_api_session_token" do
202+
let(:user_id) { user.id }
203+
204+
it "copies the user key into session['user']" do
205+
an_object_which_includes_the_concern.send(:authenticate_user_by_jwt!)
206+
expect(an_object_which_includes_the_concern.session["user"]).to eq(
207+
user_info.first["data"]["user"]
208+
)
209+
end
210+
211+
it "copies the cis2_info key into session['cis2_info']" do
212+
an_object_which_includes_the_concern.send(:authenticate_user_by_jwt!)
213+
expect(
214+
an_object_which_includes_the_concern.session["cis2_info"]
215+
).to eq(user_info.first["data"]["cis2_info"])
216+
end
217+
218+
it "calls authenticate_user!" do
219+
expect(an_object_which_includes_the_concern).to receive(
220+
:authenticate_user!
221+
)
222+
an_object_which_includes_the_concern.send(:authenticate_user_by_jwt!)
223+
end
224+
end
225+
226+
context "when a User does not exist with the values of id, session_token and reporting_api_session_token" do
227+
let(:user_id) { user.id }
228+
229+
before do
230+
user.update!(
231+
session_token: "someothersessiontoken",
232+
reporting_api_session_token: "someotherpwdauthsessiontoken"
233+
)
234+
an_object_which_includes_the_concern.session = {
235+
user_id: user.id,
236+
some_other_session_var: "some value"
237+
}
238+
end
239+
240+
it "clears the session" do
241+
an_object_which_includes_the_concern.send(:authenticate_user_by_jwt!)
242+
expect(an_object_which_includes_the_concern.session).to be_empty
243+
end
244+
245+
it "calls client_id_error!" do
246+
expect(an_object_which_includes_the_concern).to receive(
247+
:client_id_error!
248+
)
249+
an_object_which_includes_the_concern.send(:authenticate_user_by_jwt!)
250+
end
251+
end
252+
end
253+
254+
context "when a valid jwt is not given" do
255+
it "causes a client_id_error!" do
256+
expect(an_object_which_includes_the_concern).to receive(
257+
:client_id_error!
258+
)
259+
an_object_which_includes_the_concern.send(:authenticate_user_by_jwt!)
260+
end
261+
end
262+
end
263+
264+
describe "decode_jwt!" do
265+
context "given a jwt" do
266+
let(:jwt) { "somejwt" }
267+
let(:decoded_jwt) do
268+
{ "some_key" => { "some nested_key" => "some nested value" } }
269+
end
270+
271+
it "tries to decode it with the mavis reporting app secret" do
272+
expect(JWT).to receive(:decode).with(
273+
jwt,
274+
Settings.reporting_api.client_app.secret,
275+
true,
276+
{ algorithm: "HS512" }
277+
) #.and_return(decoded_jwt)
278+
an_object_which_includes_the_concern.send(:decode_jwt!, jwt)
279+
end
280+
281+
context "when decoding works" do
282+
before do
283+
allow(JWT).to receive(:decode).with(
284+
jwt,
285+
Settings.reporting_api.client_app.secret,
286+
true,
287+
{ algorithm: "HS512" }
288+
).and_return(decoded_jwt)
289+
end
290+
291+
it "returns the decoded JWT" do
292+
expect(
293+
an_object_which_includes_the_concern.send(:decode_jwt!, jwt)
294+
).to eq(decoded_jwt)
295+
end
296+
end
297+
298+
context "when decoding does not work" do
299+
it "raises an exception" do
300+
expect {
301+
an_object_which_includes_the_concern.send(:decode_jwt!, jwt)
302+
}.to raise_error(JWT::DecodeError)
303+
end
304+
end
305+
end
306+
end
307+
end

0 commit comments

Comments
 (0)