gds-sso 21.0.0 → 22.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config/routes.rb +4 -3
- data/lib/gds-sso/api_access.rb +6 -1
- data/lib/gds-sso/bearer_token.rb +1 -0
- data/lib/gds-sso/config.rb +5 -0
- data/lib/gds-sso/failure_app.rb +8 -1
- data/lib/gds-sso/version.rb +1 -1
- data/spec/internal/config/routes.rb +1 -1
- data/spec/request/api/user_spec.rb +144 -0
- data/spec/request/authentication_spec.rb +77 -0
- data/spec/request/demo_app_spec.rb +264 -0
- data/spec/spec_helper.rb +16 -7
- data/spec/support/request_helpers.rb +45 -0
- data/spec/unit/api_access_spec.rb +37 -19
- data/spec/unit/failure_app_spec.rb +31 -0
- data/spec/unit/mock_bearer_token_spec.rb +6 -0
- data/spec/unit/session_serialisation_spec.rb +5 -4
- metadata +20 -30
- data/spec/controller/api_user_controller_spec.rb +0 -114
- data/spec/support/timecop.rb +0 -7
- data/spec/system/authentication_and_authorisation_spec.rb +0 -245
- /data/spec/{controller → unit}/controller_methods_spec.rb +0 -0
@@ -1,245 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
RSpec.describe "Authenication and authorisation" do
|
4
|
-
context "omniauth request phase" do
|
5
|
-
let(:redirect_url) { URI.parse(page.response_headers["Location"]) }
|
6
|
-
let(:authorize_params) { Rack::Utils.parse_query(redirect_url.query) }
|
7
|
-
|
8
|
-
before do
|
9
|
-
visit "/auth/gds"
|
10
|
-
end
|
11
|
-
|
12
|
-
it "includes pkce code_challenge_method in request for /oauth/authorize" do
|
13
|
-
expect(redirect_url.path).to eql("/oauth/authorize")
|
14
|
-
expect(authorize_params["code_challenge_method"]).to eq("S256")
|
15
|
-
end
|
16
|
-
|
17
|
-
it "includes pkce code_challenge in request for /oauth/authorize" do
|
18
|
-
expect(redirect_url.path).to eql("/oauth/authorize")
|
19
|
-
expect(authorize_params["code_challenge"]).to be_present
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
context "omniauth callback phase" do
|
24
|
-
it "includes pkce code_verifier in request for /oauth/access_token" do
|
25
|
-
visit "/auth/gds"
|
26
|
-
|
27
|
-
redirect_url = URI.parse(page.response_headers["Location"])
|
28
|
-
expect(redirect_url.path).to eql("/oauth/authorize")
|
29
|
-
state = Rack::Utils.parse_query(redirect_url.query)["state"]
|
30
|
-
|
31
|
-
stub_request(:post, "http://signon/oauth/access_token")
|
32
|
-
|
33
|
-
visit "/auth/gds/callback?state=#{state}"
|
34
|
-
|
35
|
-
expect(WebMock).to have_requested(:post, "http://signon/oauth/access_token")
|
36
|
-
.with(body: hash_including({ "code_verifier" => /.*/ }))
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
context "when accessing a route that doesn't require permissions or authentication" do
|
41
|
-
it "allows access" do
|
42
|
-
visit "/not-restricted"
|
43
|
-
expect(page).to have_content("jabberwocky")
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
context "when accessing a route that requires authentication" do
|
48
|
-
it "redirects an unauthenticated request to signon" do
|
49
|
-
# We manually follow the redirects because we have configured capybara
|
50
|
-
# to not follow redirects (and thus allow testing an external redirect)
|
51
|
-
visit "/restricted"
|
52
|
-
expect(page.response_headers["Location"]).to match("/auth/gds")
|
53
|
-
visit page.response_headers["Location"]
|
54
|
-
expect(page.response_headers["Location"]).to match("http://signon/oauth/authorize")
|
55
|
-
end
|
56
|
-
|
57
|
-
it "allows access for an authenticated user" do
|
58
|
-
stub_signon_authenticated
|
59
|
-
|
60
|
-
visit "/restricted"
|
61
|
-
expect(page).to have_content("restricted kablooie")
|
62
|
-
end
|
63
|
-
|
64
|
-
it "restricts access if a user is authenticated but remotely signed out" do
|
65
|
-
stub_signon_authenticated
|
66
|
-
User.last.set_remotely_signed_out!
|
67
|
-
|
68
|
-
visit "/restricted"
|
69
|
-
expect(page.status_code).to eql(302)
|
70
|
-
expect(page.response_headers["Location"]).to match("/auth/gds")
|
71
|
-
end
|
72
|
-
|
73
|
-
it "restricts access if a user is authenticated but session has expired" do
|
74
|
-
stub_signon_authenticated
|
75
|
-
|
76
|
-
Timecop.travel(Time.now.utc + GDS::SSO::Config.auth_valid_for + 5.minutes) do
|
77
|
-
visit "/restricted"
|
78
|
-
expect(page.status_code).to eql(302)
|
79
|
-
expect(page.response_headers["Location"]).to match("/auth/gds")
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
it "restricts access when the request doesn't match the api_request_matcher" do
|
84
|
-
allow(GDS::SSO::Config)
|
85
|
-
.to receive(:api_request_matcher)
|
86
|
-
.and_return(->(_request) { false })
|
87
|
-
|
88
|
-
visit "/restricted"
|
89
|
-
expect(page.status_code).to eql(302)
|
90
|
-
expect(page.response_headers["Location"]).to match("/auth/gds")
|
91
|
-
end
|
92
|
-
|
93
|
-
it "allows access when given a valid bearer token" do
|
94
|
-
stub_signon_user_request
|
95
|
-
page.driver.header("Authorization", "Bearer 123")
|
96
|
-
|
97
|
-
visit "/restricted"
|
98
|
-
expect(page).to have_content("restricted kablooie")
|
99
|
-
end
|
100
|
-
|
101
|
-
it "restricts access when given an invalid bearer token" do
|
102
|
-
stub_request(:get, "http://signon/user.json?client_id=gds-sso-test")
|
103
|
-
.to_return(status: 401)
|
104
|
-
page.driver.header("Authorization", "Bearer 123")
|
105
|
-
|
106
|
-
visit "/restricted"
|
107
|
-
expect(page.status_code).to eq(401)
|
108
|
-
expect(page.response_headers["WWW-Authenticate"]).to eq('Bearer error="invalid_token"')
|
109
|
-
expect_json_response({ "message" => "Bearer token does not appear to be valid" })
|
110
|
-
end
|
111
|
-
|
112
|
-
it "returns a JSON 401 when a bearer token is missing and the app is api_only" do
|
113
|
-
allow(GDS::SSO::Config).to receive(:api_only).and_return(true)
|
114
|
-
|
115
|
-
visit "/restricted"
|
116
|
-
expect(page.status_code).to eq(401)
|
117
|
-
expect(page.response_headers["WWW-Authenticate"]).to eq('Bearer error="invalid_request"')
|
118
|
-
expect_json_response({ "message" => "No bearer token was provided" })
|
119
|
-
end
|
120
|
-
|
121
|
-
it "returns a JSON 401 when a bearer token is missing and the request matches the api_request_matcher" do
|
122
|
-
allow(GDS::SSO::Config)
|
123
|
-
.to receive(:api_request_matcher)
|
124
|
-
.and_return(->(request) { request.path == "/restricted" })
|
125
|
-
|
126
|
-
visit "/restricted"
|
127
|
-
expect(page.status_code).to eq(401)
|
128
|
-
expect(page.response_headers["WWW-Authenticate"]).to eq('Bearer error="invalid_request"')
|
129
|
-
expect_json_response({ "message" => "No bearer token was provided" })
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
context "when accessing a route that requires authentication with the mock strategies" do
|
134
|
-
before do
|
135
|
-
# Using allow_any_instance_of because it's hard to access the instance
|
136
|
-
# of the class used within the Rails middleware
|
137
|
-
allow_any_instance_of(Warden::Config).to receive(:[]).and_call_original
|
138
|
-
allow_any_instance_of(Warden::Config)
|
139
|
-
.to receive(:[])
|
140
|
-
.with(:default_strategies)
|
141
|
-
.and_return({ _all: %i[mock_gds_sso gds_bearer_token] })
|
142
|
-
|
143
|
-
allow(Warden::OAuth2.config).to receive(:token_model).and_return(GDS::SSO::MockBearerToken)
|
144
|
-
allow(GDS::SSO).to receive(:test_user).and_return(TestUser.new)
|
145
|
-
end
|
146
|
-
|
147
|
-
it "allows access without being logged in" do
|
148
|
-
visit "/restricted"
|
149
|
-
expect(page.status_code).to eq(200)
|
150
|
-
expect(page.body).to have_content("restricted kablooie")
|
151
|
-
end
|
152
|
-
|
153
|
-
it "allows access to an API mock user" do
|
154
|
-
allow(GDS::SSO::Config).to receive(:api_only).and_return(true)
|
155
|
-
|
156
|
-
visit "/restricted"
|
157
|
-
expect(page.status_code).to eq(200)
|
158
|
-
expect(page.body).to have_content("restricted kablooie")
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
context "when accessing a route that requires a permission" do
|
163
|
-
it "allows access when an authenticated user has the permission" do
|
164
|
-
stub_signon_authenticated(permissions: %w[execute])
|
165
|
-
visit "/this-requires-execute-permission"
|
166
|
-
expect(page).to have_content("you have execute permission")
|
167
|
-
end
|
168
|
-
|
169
|
-
it "restricts access when an authenticated user lacks the permission" do
|
170
|
-
stub_signon_authenticated
|
171
|
-
visit "/this-requires-execute-permission"
|
172
|
-
expect(page.status_code).to eq(403)
|
173
|
-
expect(page).to have_content("Sorry, you don't seem to have the execute permission for this app.")
|
174
|
-
end
|
175
|
-
|
176
|
-
it "returns a JSON response when it's an API call" do
|
177
|
-
allow(GDS::SSO::Config)
|
178
|
-
.to receive(:api_request_matcher)
|
179
|
-
.and_return(->(request) { request.path == "/this-requires-execute-permission" })
|
180
|
-
|
181
|
-
stub_signon_user_request
|
182
|
-
page.driver.header("Authorization", "Bearer 123")
|
183
|
-
|
184
|
-
visit "/this-requires-execute-permission"
|
185
|
-
expect(page.status_code).to eq(403)
|
186
|
-
expect_json_response({ "message" => "Sorry, you don't seem to have the execute permission for this app." })
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
context "when accessing a route that is restricted by the authorised user constraint" do
|
191
|
-
it "allows access when an authenticated user has correct permissions" do
|
192
|
-
stub_signon_authenticated(permissions: %w[execute])
|
193
|
-
visit "/constraint-restricted"
|
194
|
-
expect(page).to have_content("constraint restricted")
|
195
|
-
end
|
196
|
-
|
197
|
-
it "redirects an unauthenticated request to signon" do
|
198
|
-
visit "/constraint-restricted"
|
199
|
-
expect(page.response_headers["Location"]).to match("/auth/gds")
|
200
|
-
visit page.response_headers["Location"]
|
201
|
-
expect(page.response_headers["Location"]).to match("http://signon/oauth/authorize")
|
202
|
-
end
|
203
|
-
|
204
|
-
it "restricts access when an authenticated user does not have the correct permissions" do
|
205
|
-
stub_signon_authenticated(permissions: %w[no-access])
|
206
|
-
visit "/constraint-restricted"
|
207
|
-
expect(page.status_code).to eq(403)
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
def stub_signon_authenticated(permissions: [])
|
212
|
-
# visit restricted page to trigger redirect URL to record state attribute
|
213
|
-
visit "/auth/gds"
|
214
|
-
state = CGI.parse(URI.parse(page.response_headers["Location"]).query)
|
215
|
-
.then { |query| query["state"].first }
|
216
|
-
|
217
|
-
stub_request(:post, "http://signon/oauth/access_token")
|
218
|
-
.to_return(body: { access_token: "token" }.to_json,
|
219
|
-
headers: { content_type: "application/json" })
|
220
|
-
|
221
|
-
stub_signon_user_request(permissions:)
|
222
|
-
|
223
|
-
visit "/auth/gds/callback?code=code&state=#{state}"
|
224
|
-
end
|
225
|
-
|
226
|
-
def stub_signon_user_request(permissions: [])
|
227
|
-
stub_request(:get, "http://signon/user.json?client_id=gds-sso-test")
|
228
|
-
.to_return(
|
229
|
-
body: {
|
230
|
-
user: {
|
231
|
-
uid: "123",
|
232
|
-
email: "test-user@example.com",
|
233
|
-
name: "Test User",
|
234
|
-
permissions:,
|
235
|
-
},
|
236
|
-
}.to_json,
|
237
|
-
headers: { content_type: "application/json" },
|
238
|
-
)
|
239
|
-
end
|
240
|
-
|
241
|
-
def expect_json_response(json_match)
|
242
|
-
expect(page.response_headers["content-type"]).to match(/application\/json/)
|
243
|
-
expect(JSON.parse(page.body)).to match(json_match)
|
244
|
-
end
|
245
|
-
end
|
File without changes
|