gds-sso 21.0.0 → 21.1.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/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/mock_bearer_token_spec.rb +6 -0
- data/spec/unit/session_serialisation_spec.rb +5 -4
- metadata +16 -28
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72938e39cf2466b8a2dc2d22aca03d64ff33ab2a8c5d11c0c57f3710a2935205
|
4
|
+
data.tar.gz: 042b5c966919ab345d9ea3dc9e47197947fdd75e51ad263b0f0f038397c09fa9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fdca3de72981e79aa420da0b3d6c4d689fba034d3d3cda4874082efe58bcf056128242ab09a4266b8d11f91da500d47b665e434ccb805a3f6a799d6d29f765f4
|
7
|
+
data.tar.gz: ad252eb2aeaf3986b68b54a15f7f4622f4d56e730d509690e1e80c77be6411201ebf618ba68efebaff825fe731a111de186b439b6834a4c1de91782745658335
|
data/config/routes.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
Rails.application.routes.draw do
|
2
|
+
put "/auth/gds/api/users/:uid", to: "api/user#update"
|
3
|
+
post "/auth/gds/api/users/:uid/reauth", to: "api/user#reauth"
|
4
|
+
|
2
5
|
next if GDS::SSO::Config.api_only
|
3
6
|
|
4
7
|
get "/auth/gds/callback", to: "authentications#callback", as: :gds_sign_in
|
5
8
|
get "/auth/gds/sign_out", to: "authentications#sign_out", as: :gds_sign_out
|
6
|
-
get
|
7
|
-
put "/auth/gds/api/users/:uid", to: "api/user#update"
|
8
|
-
post "/auth/gds/api/users/:uid/reauth", to: "api/user#reauth"
|
9
|
+
get "/auth/failure", to: "authentications#failure", as: :auth_failure
|
9
10
|
end
|
data/lib/gds-sso/api_access.rb
CHANGED
@@ -8,7 +8,12 @@ module GDS
|
|
8
8
|
return true if GDS::SSO::Config.api_only
|
9
9
|
|
10
10
|
if GDS::SSO::Config.api_request_matcher
|
11
|
-
|
11
|
+
request = Rack::Request.new(env)
|
12
|
+
|
13
|
+
gds_sso_api_request_matcher = GDS::SSO::Config.gds_sso_api_request_matcher
|
14
|
+
return true if gds_sso_api_request_matcher&.call(request)
|
15
|
+
|
16
|
+
return GDS::SSO::Config.api_request_matcher.call(request)
|
12
17
|
end
|
13
18
|
|
14
19
|
!bearer_token(env).nil?
|
data/lib/gds-sso/bearer_token.rb
CHANGED
@@ -58,6 +58,7 @@ module GDS
|
|
58
58
|
|
59
59
|
module MockBearerToken
|
60
60
|
def self.locate(_token_string)
|
61
|
+
return unless ENV["GDS_SSO_MOCK_INVALID"].to_s.empty?
|
61
62
|
return GDS::SSO.test_user if GDS::SSO.test_user
|
62
63
|
|
63
64
|
dummy_api_user = GDS::SSO::Config.user_klass.where(email: "dummyapiuser@domain.com").first
|
data/lib/gds-sso/config.rb
CHANGED
@@ -31,6 +31,11 @@ module GDS
|
|
31
31
|
|
32
32
|
mattr_accessor :api_request_matcher
|
33
33
|
|
34
|
+
# A matcher for GDS SSO's own API for updating user details. Shouldn't
|
35
|
+
# need configuring unless you mount the API at a different location.
|
36
|
+
mattr_accessor :gds_sso_api_request_matcher
|
37
|
+
@@gds_sso_api_request_matcher = ->(request) { request.path.start_with?("/auth/gds/api/") }
|
38
|
+
|
34
39
|
mattr_accessor :intercept_401_responses
|
35
40
|
@@intercept_401_responses = true
|
36
41
|
|
data/lib/gds-sso/version.rb
CHANGED
@@ -5,7 +5,7 @@ Rails.application.routes.draw do
|
|
5
5
|
get "/restricted" => "example#restricted"
|
6
6
|
get "/this-requires-execute-permission" => "example#this_requires_execute_permission"
|
7
7
|
|
8
|
-
constraints(GDS::SSO::AuthorisedUserConstraint.new("
|
8
|
+
constraints(GDS::SSO::AuthorisedUserConstraint.new("constraint")) do
|
9
9
|
get "/constraint-restricted" => "example#constraint_restricted"
|
10
10
|
end
|
11
11
|
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Api::UserController", type: :request do
|
4
|
+
shared_examples "rejects a request from an unauthenticated user" do |method, path|
|
5
|
+
it "rejects the request when a user is unauthenticated" do
|
6
|
+
stub_failed_signon_user_request
|
7
|
+
public_send(method, path, headers: { "Authorization" => "Bearer anything" })
|
8
|
+
expect(response).to have_http_status(:unauthorized)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
shared_examples "rejects a request from an authenticated user lacking permission" do |method, path|
|
13
|
+
it "rejects the request when a user is authenticated but lacking permission" do
|
14
|
+
stub_successful_signon_user_request(permissions: [])
|
15
|
+
public_send(method, path, headers: { "Authorization" => "Bearer anything" })
|
16
|
+
expect(response).to have_http_status(:forbidden)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
shared_examples "operates as an API endpoint if api_request_matcher doesn't match it" do |method, path|
|
21
|
+
it "doesn't redirect to /auth/gds because it's recognised as an API request" do
|
22
|
+
allow(GDS::SSO::Config)
|
23
|
+
.to receive(:api_request_matcher)
|
24
|
+
.and_return(->(_request) { false })
|
25
|
+
|
26
|
+
stub_failed_signon_user_request
|
27
|
+
public_send(method, path, headers: { "Authorization" => "Bearer anything" })
|
28
|
+
expect(response.media_type).to eq("application/json")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
shared_examples "redirects to /auth/gds if gds_sso_api_request_matcher is configured to not match" do |method, path|
|
33
|
+
it "fails if gds_sso_api_request_matcher is configured to not match" do
|
34
|
+
allow(GDS::SSO::Config)
|
35
|
+
.to receive(:api_request_matcher)
|
36
|
+
.and_return(->(_request) { false })
|
37
|
+
|
38
|
+
allow(GDS::SSO::Config)
|
39
|
+
.to receive(:gds_sso_api_request_matcher)
|
40
|
+
.and_return(nil)
|
41
|
+
|
42
|
+
public_send(method, path, headers: { "Authorization" => "Bearer anything" })
|
43
|
+
expect(response).to redirect_to("/auth/gds")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "PUT /auth/gds/api/users/:uid" do
|
48
|
+
shared_params = [:put, "/auth/gds/api/users/#{SecureRandom.uuid}"]
|
49
|
+
it_behaves_like "rejects a request from an unauthenticated user", *shared_params
|
50
|
+
it_behaves_like "rejects a request from an authenticated user lacking permission", *shared_params
|
51
|
+
it_behaves_like "operates as an API endpoint if api_request_matcher doesn't match it", *shared_params
|
52
|
+
it_behaves_like "redirects to /auth/gds if gds_sso_api_request_matcher is configured to not match", *shared_params
|
53
|
+
|
54
|
+
it "updates an existing user" do
|
55
|
+
stub_successful_signon_user_request(permissions: %w[user_update_permission])
|
56
|
+
|
57
|
+
user = User.create!(
|
58
|
+
uid: SecureRandom.uuid,
|
59
|
+
email: "user@example.com",
|
60
|
+
name: "Example User",
|
61
|
+
permissions: [],
|
62
|
+
)
|
63
|
+
|
64
|
+
put "/auth/gds/api/users/#{user.uid}",
|
65
|
+
headers: { "Authorization" => "Bearer anything" },
|
66
|
+
params: user_update_params(user, { "name" => "John Matrix" }),
|
67
|
+
as: :json
|
68
|
+
|
69
|
+
expect(response).to have_http_status(:success)
|
70
|
+
expect(response.body).to eq("")
|
71
|
+
expect(user.reload.name).to eq("John Matrix")
|
72
|
+
end
|
73
|
+
|
74
|
+
it "creates a new user if a user does not exist" do
|
75
|
+
stub_successful_signon_user_request(permissions: %w[user_update_permission])
|
76
|
+
|
77
|
+
user = User.new(
|
78
|
+
uid: SecureRandom.uuid,
|
79
|
+
email: "user@example.com",
|
80
|
+
name: "Example User",
|
81
|
+
permissions: [],
|
82
|
+
)
|
83
|
+
|
84
|
+
put "/auth/gds/api/users/#{user.uid}",
|
85
|
+
headers: { "Authorization" => "Bearer anything" },
|
86
|
+
params: user_update_params(user),
|
87
|
+
as: :json
|
88
|
+
|
89
|
+
expect(response).to have_http_status(:success)
|
90
|
+
expect(response.body).to eq("")
|
91
|
+
expect(User.last.uid).to eq(user.uid)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "POST /auth/gds/api/users/:uid/reauth" do
|
96
|
+
shared_params = [:post, "/auth/gds/api/users/#{SecureRandom.uuid}/reauth"]
|
97
|
+
it_behaves_like "rejects a request from an unauthenticated user", *shared_params
|
98
|
+
it_behaves_like "rejects a request from an authenticated user lacking permission", *shared_params
|
99
|
+
it_behaves_like "operates as an API endpoint if api_request_matcher doesn't match it", *shared_params
|
100
|
+
it_behaves_like "redirects to /auth/gds if gds_sso_api_request_matcher is configured to not match", *shared_params
|
101
|
+
|
102
|
+
it "flags a user that exists as remotely signed out" do
|
103
|
+
stub_successful_signon_user_request(permissions: %w[user_update_permission])
|
104
|
+
|
105
|
+
user = User.create!(
|
106
|
+
uid: SecureRandom.uuid,
|
107
|
+
email: "user@example.com",
|
108
|
+
name: "Example User",
|
109
|
+
permissions: [],
|
110
|
+
)
|
111
|
+
|
112
|
+
expect {
|
113
|
+
post "/auth/gds/api/users/#{user.uid}/reauth",
|
114
|
+
headers: { "Authorization" => "Bearer anything" }
|
115
|
+
}.to change { user.reload.remotely_signed_out }.to(true)
|
116
|
+
|
117
|
+
expect(response).to have_http_status(:success)
|
118
|
+
expect(response.body).to eq("")
|
119
|
+
end
|
120
|
+
|
121
|
+
it "responds successfully even if the user doesn't exist" do
|
122
|
+
stub_successful_signon_user_request(permissions: %w[user_update_permission])
|
123
|
+
|
124
|
+
user = User.new(
|
125
|
+
uid: SecureRandom.uuid,
|
126
|
+
email: "user@example.com",
|
127
|
+
name: "Example User",
|
128
|
+
permissions: [],
|
129
|
+
)
|
130
|
+
|
131
|
+
post "/auth/gds/api/users/#{user.uid}/reauth",
|
132
|
+
headers: { "Authorization" => "Bearer anything" }
|
133
|
+
|
134
|
+
expect(response).to have_http_status(:success)
|
135
|
+
expect(response.body).to eq("")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def user_update_params(user, modifications = {})
|
140
|
+
fields = %i[uid name email permissions organisation_slug organisation_content_id disabled]
|
141
|
+
user_details = user.as_json(only: fields).merge(modifications)
|
142
|
+
{ "user" => user_details }
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "AuthenticationController", type: :request do
|
4
|
+
describe "GET /auth/gds/callback" do
|
5
|
+
it "fails without a valid state param" do
|
6
|
+
get "/auth/gds/callback"
|
7
|
+
|
8
|
+
expect(response).to redirect_to("/auth/failure?message=csrf_detected&strategy=gds")
|
9
|
+
end
|
10
|
+
|
11
|
+
it "redirects to the attempted url if a user was restricted earlier in the session" do
|
12
|
+
get "/restricted"
|
13
|
+
|
14
|
+
state = request_to_establish_oauth_state
|
15
|
+
|
16
|
+
stub_signon_oauth_token_request
|
17
|
+
stub_successful_signon_user_request
|
18
|
+
|
19
|
+
get "/auth/gds/callback?state=#{state}"
|
20
|
+
expect(response).to redirect_to("/restricted")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "redirects to the root path if the user hadn't tried to access a restricted url" do
|
24
|
+
state = request_to_establish_oauth_state
|
25
|
+
|
26
|
+
stub_signon_oauth_token_request
|
27
|
+
stub_successful_signon_user_request
|
28
|
+
|
29
|
+
get "/auth/gds/callback?state=#{state}"
|
30
|
+
expect(response).to redirect_to("/")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "uses the OAuth2 proof key for code exchange feature for increased security" do
|
34
|
+
get "/auth/gds"
|
35
|
+
|
36
|
+
expect(response).to have_http_status(:redirect)
|
37
|
+
location = URI.parse(response.location)
|
38
|
+
query = Rack::Utils.parse_query(location.query)
|
39
|
+
|
40
|
+
expect(location.path).to eq("/oauth/authorize")
|
41
|
+
expect(query).to include("code_challenge", "code_challenge_method")
|
42
|
+
|
43
|
+
token_request = stub_request(:post, "http://signon/oauth/access_token")
|
44
|
+
.with(body: hash_including("code_verifier"))
|
45
|
+
|
46
|
+
get "/auth/gds/callback?state=#{query['state']}"
|
47
|
+
|
48
|
+
expect(token_request).to have_been_made
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "GET /auth/failure" do
|
53
|
+
it "responds successfully" do
|
54
|
+
get "/auth/failure"
|
55
|
+
|
56
|
+
expect(response).to have_http_status(:success)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "GET /auth/gds/sign_out" do
|
61
|
+
it "redirects to signon sign out" do
|
62
|
+
get "/auth/gds/sign_out"
|
63
|
+
|
64
|
+
expect(response).to redirect_to("http://signon/users/sign_out")
|
65
|
+
end
|
66
|
+
|
67
|
+
it "logs an authenticated user out" do
|
68
|
+
authenticate_with_stub_signon
|
69
|
+
|
70
|
+
get "/auth/gds/sign_out"
|
71
|
+
|
72
|
+
# access a restricted route to assert we're logged out
|
73
|
+
get "/restricted"
|
74
|
+
expect(response).to redirect_to("/auth/gds")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,264 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Integration tests with a demo app", type: :request do
|
4
|
+
describe "accessing a route that doesn't require authentication" do
|
5
|
+
it "allows access" do
|
6
|
+
get "/not-restricted"
|
7
|
+
expect(response).to have_http_status(:success)
|
8
|
+
expect(response.body).to eq("jabberwocky")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "accessing a route the requires authentication" do
|
13
|
+
context "when GDS::SSO isn't configured to treat this as an explicit API request" do
|
14
|
+
it "redirects an unauthenticated request to sign-in" do
|
15
|
+
get "/restricted"
|
16
|
+
expect(response).to redirect_to("/auth/gds")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "responds successfully for an authenticated user" do
|
20
|
+
authenticate_with_stub_signon
|
21
|
+
|
22
|
+
get "/restricted"
|
23
|
+
expect(response).to have_http_status(:success)
|
24
|
+
expect(response.body).to eq("restricted kablooie")
|
25
|
+
end
|
26
|
+
|
27
|
+
it "redirects to sign-in if a user is authenticated but remotely signed out" do
|
28
|
+
authenticate_with_stub_signon
|
29
|
+
User.last.set_remotely_signed_out!
|
30
|
+
|
31
|
+
get "/restricted"
|
32
|
+
expect(response).to redirect_to("/auth/gds")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "redirects to sign-in if a user's session has expired" do
|
36
|
+
authenticate_with_stub_signon
|
37
|
+
|
38
|
+
travel_to(Time.now.utc + GDS::SSO::Config.auth_valid_for + 1.second) do
|
39
|
+
get "/restricted"
|
40
|
+
expect(response).to redirect_to("/auth/gds")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it "allows access when given a valid bearer token" do
|
45
|
+
stub_successful_signon_user_request
|
46
|
+
|
47
|
+
get "/restricted", headers: { "Authorization" => "Bearer 123" }
|
48
|
+
expect(response).to have_http_status(:success)
|
49
|
+
expect(response.body).to eq("restricted kablooie")
|
50
|
+
end
|
51
|
+
|
52
|
+
it "restricts access when given an invalid bearer token" do
|
53
|
+
stub_failed_signon_user_request
|
54
|
+
|
55
|
+
get "/restricted", headers: { "Authorization" => "Bearer invalid" }
|
56
|
+
expect(response).to have_http_status(:unauthorized)
|
57
|
+
expect_invalid_bearer_token_response(response)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when an application is configured as API only" do
|
62
|
+
before { allow(GDS::SSO::Config).to receive(:api_only).and_return(true) }
|
63
|
+
|
64
|
+
it "allows access when given a valid bearer token" do
|
65
|
+
stub_successful_signon_user_request
|
66
|
+
|
67
|
+
get "/restricted", headers: { "Authorization" => "Bearer 123" }
|
68
|
+
expect(response).to have_http_status(:success)
|
69
|
+
expect(response.body).to eq("restricted kablooie")
|
70
|
+
end
|
71
|
+
|
72
|
+
it "rejects a request without a bearer token" do
|
73
|
+
get "/restricted"
|
74
|
+
expect(response).to have_http_status(:unauthorized)
|
75
|
+
expect_missing_bearer_token_response(response)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "restricts access for an invalid bearer token" do
|
79
|
+
stub_failed_signon_user_request
|
80
|
+
|
81
|
+
get "/restricted", headers: { "Authorization" => "Bearer invalid" }
|
82
|
+
expect(response).to have_http_status(:unauthorized)
|
83
|
+
expect_invalid_bearer_token_response(response)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "when API requests are differentiated by api_request_matcher" do
|
88
|
+
it "treats a match as an API request" do
|
89
|
+
allow(GDS::SSO::Config)
|
90
|
+
.to receive(:api_request_matcher)
|
91
|
+
.and_return(->(request) { request.path == "/restricted" })
|
92
|
+
|
93
|
+
get "/restricted"
|
94
|
+
expect(response).to have_http_status(:unauthorized)
|
95
|
+
expect_missing_bearer_token_response(response)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "treats a non-match as a non-API request" do
|
99
|
+
allow(GDS::SSO::Config)
|
100
|
+
.to receive(:api_request_matcher)
|
101
|
+
.and_return(->(_request) { false })
|
102
|
+
|
103
|
+
get "/restricted"
|
104
|
+
expect(response).to redirect_to("/auth/gds")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "when accessing routes without authentication and using the mock strategies" do
|
110
|
+
before { use_mock_strategies }
|
111
|
+
|
112
|
+
it "allows a user access without authentication" do
|
113
|
+
# non-bearer token mock requests require a user to exist
|
114
|
+
User.create!(
|
115
|
+
uid: SecureRandom.uuid,
|
116
|
+
email: "user@example.com",
|
117
|
+
name: "Example User",
|
118
|
+
permissions: [],
|
119
|
+
)
|
120
|
+
|
121
|
+
get "/restricted"
|
122
|
+
expect(response).to have_http_status(:success)
|
123
|
+
expect(response.body).to eq("restricted kablooie")
|
124
|
+
end
|
125
|
+
|
126
|
+
it "can be configured to fail authentication with an env var" do
|
127
|
+
ClimateControl.modify("GDS_SSO_MOCK_INVALID" => "1") do
|
128
|
+
get "/restricted"
|
129
|
+
expect(response).to redirect_to("/auth/gds")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it "allows an API request without a bearer token" do
|
134
|
+
allow(GDS::SSO::Config).to receive(:api_only).and_return(true)
|
135
|
+
|
136
|
+
get "/restricted"
|
137
|
+
expect(response).to have_http_status(:success)
|
138
|
+
expect(response.body).to eq("restricted kablooie")
|
139
|
+
end
|
140
|
+
|
141
|
+
it "can be configured to fail API authentication with an env var" do
|
142
|
+
allow(GDS::SSO::Config).to receive(:api_only).and_return(true)
|
143
|
+
|
144
|
+
ClimateControl.modify("GDS_SSO_MOCK_INVALID" => "1") do
|
145
|
+
get "/restricted"
|
146
|
+
expect(response).to have_http_status(:unauthorized)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "when accessing a route that requires permission" do
|
152
|
+
context "when GDS::SSO isn't configured to treat this as an explicit API request" do
|
153
|
+
it "allows a user with the permission to access the resource" do
|
154
|
+
authenticate_with_stub_signon(permissions: %w[execute])
|
155
|
+
|
156
|
+
get "/this-requires-execute-permission"
|
157
|
+
expect(response).to have_http_status(:success)
|
158
|
+
expect(response.body).to eq("you have execute permission")
|
159
|
+
end
|
160
|
+
|
161
|
+
it "restricts a user lacking the permission" do
|
162
|
+
authenticate_with_stub_signon
|
163
|
+
|
164
|
+
get "/this-requires-execute-permission"
|
165
|
+
expect(response).to have_http_status(:forbidden)
|
166
|
+
expect(response.body).to include("Sorry, you don't seem to have the execute permission for this app.")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "when GDS::SSO is configured to treat the request as an API request" do
|
171
|
+
before { allow(GDS::SSO::Config).to receive(:api_only).and_return(true) }
|
172
|
+
|
173
|
+
it "allows a user with the permission to access the resource" do
|
174
|
+
stub_successful_signon_user_request(permissions: %w[execute])
|
175
|
+
|
176
|
+
get "/this-requires-execute-permission", headers: { "Authorization" => "Bearer 123" }
|
177
|
+
expect(response).to have_http_status(:success)
|
178
|
+
expect(response.body).to eq("you have execute permission")
|
179
|
+
end
|
180
|
+
|
181
|
+
it "restricts a user lacking the permission" do
|
182
|
+
stub_successful_signon_user_request
|
183
|
+
|
184
|
+
get "/this-requires-execute-permission", headers: { "Authorization" => "Bearer 123" }
|
185
|
+
expect(response).to have_http_status(:forbidden)
|
186
|
+
expect_json_response(response, { "message" => "Sorry, you don't seem to have the execute permission for this app." })
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context "when using bearer token for auth with the mock strategy" do
|
191
|
+
before { use_mock_strategies }
|
192
|
+
|
193
|
+
it "automatically grants permissions configured in GDS:SSO::Config.additional_mock_permissions_required" do
|
194
|
+
allow(GDS::SSO::Config).to receive(:additional_mock_permissions_required).and_return(%w[execute])
|
195
|
+
|
196
|
+
stub_successful_signon_user_request
|
197
|
+
|
198
|
+
get "/this-requires-execute-permission", headers: { "Authorization" => "Bearer 123" }
|
199
|
+
expect(response).to have_http_status(:success)
|
200
|
+
expect(response.body).to eq("you have execute permission")
|
201
|
+
end
|
202
|
+
|
203
|
+
it "doesn't grant access without that config" do
|
204
|
+
allow(GDS::SSO::Config).to receive(:additional_mock_permissions_required).and_return(nil)
|
205
|
+
|
206
|
+
stub_successful_signon_user_request
|
207
|
+
|
208
|
+
get "/this-requires-execute-permission", headers: { "Authorization" => "Bearer 123" }
|
209
|
+
expect(response).to have_http_status(:forbidden)
|
210
|
+
expect_json_response(response, { "message" => "Sorry, you don't seem to have the execute permission for this app." })
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
context "when accessing a route that is restricted by the authorised user constraint" do
|
216
|
+
it "allows access when an authenticated user has correct permissions" do
|
217
|
+
authenticate_with_stub_signon(permissions: %w[constraint])
|
218
|
+
|
219
|
+
get "/constraint-restricted"
|
220
|
+
expect(response).to have_http_status(:success)
|
221
|
+
expect(response.body).to eq("constraint restricted")
|
222
|
+
end
|
223
|
+
|
224
|
+
it "redirects an unauthenticated request to signon" do
|
225
|
+
get "/constraint-restricted"
|
226
|
+
|
227
|
+
expect(response).to redirect_to("/auth/gds")
|
228
|
+
end
|
229
|
+
|
230
|
+
it "restricts access when an authenticated user does not have the correct permissions" do
|
231
|
+
authenticate_with_stub_signon
|
232
|
+
|
233
|
+
get "/constraint-restricted"
|
234
|
+
expect(response).to have_http_status(:forbidden)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def expect_missing_bearer_token_response(response)
|
239
|
+
expect(response.headers).to include("WWW-Authenticate" => 'Bearer error="invalid_request"')
|
240
|
+
expect_json_response(response, { "message" => "No bearer token was provided" })
|
241
|
+
end
|
242
|
+
|
243
|
+
def expect_invalid_bearer_token_response(response)
|
244
|
+
expect(response.headers).to include("WWW-Authenticate" => 'Bearer error="invalid_token"')
|
245
|
+
expect_json_response(response, { "message" => "Bearer token does not appear to be valid" })
|
246
|
+
end
|
247
|
+
|
248
|
+
def expect_json_response(response, json)
|
249
|
+
expect(response.media_type).to eq("application/json")
|
250
|
+
expect(response.parsed_body).to eq(json)
|
251
|
+
end
|
252
|
+
|
253
|
+
def use_mock_strategies
|
254
|
+
# Using allow_any_instance_of because it's hard to access the instance
|
255
|
+
# of the class used within the Rails middleware
|
256
|
+
allow_any_instance_of(Warden::Config).to receive(:[]).and_call_original
|
257
|
+
allow_any_instance_of(Warden::Config)
|
258
|
+
.to receive(:[])
|
259
|
+
.with(:default_strategies)
|
260
|
+
.and_return({ _all: %i[mock_gds_sso gds_bearer_token] })
|
261
|
+
|
262
|
+
allow(Warden::OAuth2.config).to receive(:token_model).and_return(GDS::SSO::MockBearerToken)
|
263
|
+
end
|
264
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
# Bad things happen if we don't ;-)
|
3
3
|
ENV["GDS_SSO_STRATEGY"] = "real"
|
4
4
|
|
5
|
-
require "
|
5
|
+
require "climate_control"
|
6
6
|
require "webmock/rspec"
|
7
7
|
require "combustion"
|
8
8
|
|
@@ -12,15 +12,10 @@ Combustion.initialize! :all do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
require "rspec/rails"
|
15
|
-
require "capybara/rails"
|
16
15
|
WebMock.disable_net_connect!
|
17
16
|
|
18
17
|
Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].sort.each { |f| require f }
|
19
18
|
|
20
|
-
Capybara.register_driver :rack_test do |app|
|
21
|
-
Capybara::RackTest::Driver.new(app, follow_redirects: false)
|
22
|
-
end
|
23
|
-
|
24
19
|
RSpec.configure do |config|
|
25
20
|
config.run_all_when_everything_filtered = true
|
26
21
|
config.filter_run :focus
|
@@ -32,6 +27,20 @@ RSpec.configure do |config|
|
|
32
27
|
# --seed 1234
|
33
28
|
config.order = "random"
|
34
29
|
|
30
|
+
config.include ActiveSupport::Testing::TimeHelpers
|
35
31
|
config.include Warden::Test::Helpers
|
36
|
-
config.include
|
32
|
+
config.include RequestHelpers, type: :request
|
33
|
+
config.before(:each, type: :request) do
|
34
|
+
# we reload routes each test as GDS::SSO::Config affects what routes are
|
35
|
+
# available, we only want to run this once routes are loaded otherwise
|
36
|
+
# we can lose app routes
|
37
|
+
routes_reloader = Rails.application.routes_reloader
|
38
|
+
|
39
|
+
# Routes changed in Rails 8 to be lazily loaded so this wasn't a problem
|
40
|
+
# before Rails 8.
|
41
|
+
# TODO: remove this line once Rails 7 support is removed
|
42
|
+
next unless routes_reloader.respond_to?(:loaded)
|
43
|
+
|
44
|
+
routes_reloader.reload! if routes_reloader.loaded
|
45
|
+
end
|
37
46
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module RequestHelpers
|
2
|
+
def authenticate_with_stub_signon(permissions: [])
|
3
|
+
state = request_to_establish_oauth_state
|
4
|
+
|
5
|
+
stub_signon_oauth_token_request
|
6
|
+
stub_successful_signon_user_request(permissions:)
|
7
|
+
|
8
|
+
get "/auth/gds/callback?code=code&state=#{state}"
|
9
|
+
expect(response).to have_http_status(:redirect)
|
10
|
+
end
|
11
|
+
|
12
|
+
def request_to_establish_oauth_state
|
13
|
+
get "/auth/gds"
|
14
|
+
expect(response).to have_http_status(:redirect)
|
15
|
+
location = URI.parse(response.location)
|
16
|
+
query = Rack::Utils.parse_query(location.query)
|
17
|
+
query.fetch("state")
|
18
|
+
end
|
19
|
+
|
20
|
+
def stub_signon_oauth_token_request
|
21
|
+
stub_request(:post, "http://signon/oauth/access_token")
|
22
|
+
.to_return(body: { access_token: "token" }.to_json,
|
23
|
+
headers: { content_type: "application/json" })
|
24
|
+
end
|
25
|
+
|
26
|
+
def stub_successful_signon_user_request(permissions: [])
|
27
|
+
stub_request(:get, "http://signon/user.json?client_id=gds-sso-test")
|
28
|
+
.to_return(
|
29
|
+
body: {
|
30
|
+
user: {
|
31
|
+
uid: "123",
|
32
|
+
email: "test-user@example.com",
|
33
|
+
name: "Test User",
|
34
|
+
permissions:,
|
35
|
+
},
|
36
|
+
}.to_json,
|
37
|
+
headers: { content_type: "application/json" },
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def stub_failed_signon_user_request
|
42
|
+
stub_request(:get, "http://signon/user.json?client_id=gds-sso-test")
|
43
|
+
.to_return(status: 401)
|
44
|
+
end
|
45
|
+
end
|
@@ -18,31 +18,49 @@ describe GDS::SSO::ApiAccess do
|
|
18
18
|
expect(described_class.api_call?({})).to be(true)
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
context "when an api_request_matcher has been configured" do
|
22
|
+
before do
|
23
|
+
allow(GDS::SSO::Config)
|
24
|
+
.to receive(:api_request_matcher)
|
25
|
+
.and_return(->(request) { request.path == "/api" })
|
26
|
+
end
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
|
28
|
+
it "returns true if the request matches the api_request_matcher" do
|
29
|
+
env = Rack::MockRequest.env_for("/api")
|
30
|
+
expect(described_class.api_call?(env)).to be(true)
|
31
|
+
end
|
29
32
|
|
30
|
-
|
31
|
-
|
32
|
-
.to
|
33
|
-
|
33
|
+
it "returns true if the request is for GDS SSO API at default location" do
|
34
|
+
env = Rack::MockRequest.env_for("/auth/gds/api/#{SecureRandom.uuid}")
|
35
|
+
expect(described_class.api_call?(env)).to be(true)
|
36
|
+
end
|
34
37
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
+
it "returns true if it matches a configured gds_sso_api_request_matcher" do
|
39
|
+
allow(GDS::SSO::Config)
|
40
|
+
.to receive(:gds_sso_api_request_matcher)
|
41
|
+
.and_return(->(request) { request.path == "/special/gds-sso/route" })
|
38
42
|
|
39
|
-
|
40
|
-
|
41
|
-
|
43
|
+
env = Rack::MockRequest.env_for("/special/gds-sso/route")
|
44
|
+
expect(described_class.api_call?(env)).to be(true)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "returns false if the request doesn't match the gds_sso_api_request_matcher or api_request_matcher" do
|
48
|
+
allow(GDS::SSO::Config).to receive(:gds_sso_api_request_matcher).and_return(nil)
|
49
|
+
|
50
|
+
env = Rack::MockRequest.env_for("/other")
|
51
|
+
expect(described_class.api_call?(env)).to be(false)
|
52
|
+
end
|
42
53
|
end
|
43
54
|
|
44
|
-
|
45
|
-
|
55
|
+
context "when an api_request_matcher has not been configured" do
|
56
|
+
it "returns true if a bearer token is present" do
|
57
|
+
env = { "HTTP_AUTHORIZATION" => "Bearer 1234:5678" }
|
58
|
+
expect(described_class.api_call?(env)).to be(true)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "returns false if nothing indicates an API call" do
|
62
|
+
expect(described_class.api_call?({})).to be(false)
|
63
|
+
end
|
46
64
|
end
|
47
65
|
end
|
48
66
|
|
@@ -10,6 +10,12 @@ describe GDS::SSO::MockBearerToken do
|
|
10
10
|
expect(described_class.locate("anything")).to be(test_user)
|
11
11
|
end
|
12
12
|
|
13
|
+
it "returns nil if ENV['GDS_SSO_MOCK_INVALID'] is set" do
|
14
|
+
ClimateControl.modify("GDS_SSO_MOCK_INVALID" => "1") do
|
15
|
+
expect(described_class.locate("anything")).to be_nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
13
19
|
it "doesn't modify the permissions of GDS::SSO.test_user" do
|
14
20
|
test_user = TestUser.new(permissions: [])
|
15
21
|
allow(GDS::SSO).to receive(:test_user).and_return(test_user)
|
@@ -14,11 +14,12 @@ describe Warden::SessionSerializer do
|
|
14
14
|
|
15
15
|
describe "serializing a user" do
|
16
16
|
it "should return the uid and an ISO 8601 string timestamp" do
|
17
|
-
|
18
|
-
|
17
|
+
freeze_time do
|
18
|
+
result = @serializer.serialize(@user)
|
19
19
|
|
20
|
-
|
21
|
-
|
20
|
+
expect(result).to eq([1234, Time.now.utc.iso8601])
|
21
|
+
expect(result.last).to be_a(String)
|
22
|
+
end
|
22
23
|
end
|
23
24
|
|
24
25
|
it "should return nil if the user has no uid" do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gds-sso
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 21.
|
4
|
+
version: 21.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GOV.UK Dev
|
@@ -122,19 +122,19 @@ dependencies:
|
|
122
122
|
- !ruby/object:Gem::Version
|
123
123
|
version: 0.0.1
|
124
124
|
- !ruby/object:Gem::Dependency
|
125
|
-
name:
|
125
|
+
name: climate_control
|
126
126
|
requirement: !ruby/object:Gem::Requirement
|
127
127
|
requirements:
|
128
128
|
- - "~>"
|
129
129
|
- !ruby/object:Gem::Version
|
130
|
-
version: '
|
130
|
+
version: '1'
|
131
131
|
type: :development
|
132
132
|
prerelease: false
|
133
133
|
version_requirements: !ruby/object:Gem::Requirement
|
134
134
|
requirements:
|
135
135
|
- - "~>"
|
136
136
|
- !ruby/object:Gem::Version
|
137
|
-
version: '
|
137
|
+
version: '1'
|
138
138
|
- !ruby/object:Gem::Dependency
|
139
139
|
name: combustion
|
140
140
|
requirement: !ruby/object:Gem::Requirement
|
@@ -183,14 +183,14 @@ dependencies:
|
|
183
183
|
requirements:
|
184
184
|
- - '='
|
185
185
|
- !ruby/object:Gem::Version
|
186
|
-
version: 5.
|
186
|
+
version: 5.1.18
|
187
187
|
type: :development
|
188
188
|
prerelease: false
|
189
189
|
version_requirements: !ruby/object:Gem::Requirement
|
190
190
|
requirements:
|
191
191
|
- - '='
|
192
192
|
- !ruby/object:Gem::Version
|
193
|
-
version: 5.
|
193
|
+
version: 5.1.18
|
194
194
|
- !ruby/object:Gem::Dependency
|
195
195
|
name: sqlite3
|
196
196
|
requirement: !ruby/object:Gem::Requirement
|
@@ -205,20 +205,6 @@ dependencies:
|
|
205
205
|
- - "~>"
|
206
206
|
- !ruby/object:Gem::Version
|
207
207
|
version: '2.1'
|
208
|
-
- !ruby/object:Gem::Dependency
|
209
|
-
name: timecop
|
210
|
-
requirement: !ruby/object:Gem::Requirement
|
211
|
-
requirements:
|
212
|
-
- - "~>"
|
213
|
-
- !ruby/object:Gem::Version
|
214
|
-
version: '0.9'
|
215
|
-
type: :development
|
216
|
-
prerelease: false
|
217
|
-
version_requirements: !ruby/object:Gem::Requirement
|
218
|
-
requirements:
|
219
|
-
- - "~>"
|
220
|
-
- !ruby/object:Gem::Version
|
221
|
-
version: '0.9'
|
222
208
|
- !ruby/object:Gem::Dependency
|
223
209
|
name: webmock
|
224
210
|
requirement: !ruby/object:Gem::Requirement
|
@@ -264,8 +250,6 @@ files:
|
|
264
250
|
- lib/gds-sso/version.rb
|
265
251
|
- lib/gds-sso/warden_config.rb
|
266
252
|
- lib/omniauth/strategies/gds.rb
|
267
|
-
- spec/controller/api_user_controller_spec.rb
|
268
|
-
- spec/controller/controller_methods_spec.rb
|
269
253
|
- spec/internal/app/assets/config/manifest.js
|
270
254
|
- spec/internal/app/controllers/application_controller.rb
|
271
255
|
- spec/internal/app/controllers/example_controller.rb
|
@@ -274,17 +258,20 @@ files:
|
|
274
258
|
- spec/internal/config/initializers/gds-sso.rb
|
275
259
|
- spec/internal/config/routes.rb
|
276
260
|
- spec/internal/db/schema.rb
|
261
|
+
- spec/request/api/user_spec.rb
|
262
|
+
- spec/request/authentication_spec.rb
|
263
|
+
- spec/request/demo_app_spec.rb
|
277
264
|
- spec/spec_helper.rb
|
278
265
|
- spec/support/controller_spy.rb
|
266
|
+
- spec/support/request_helpers.rb
|
279
267
|
- spec/support/serializable_user.rb
|
280
268
|
- spec/support/test_user.rb
|
281
|
-
- spec/support/timecop.rb
|
282
|
-
- spec/system/authentication_and_authorisation_spec.rb
|
283
269
|
- spec/unit/api_access_spec.rb
|
284
270
|
- spec/unit/authorise_user_spec.rb
|
285
271
|
- spec/unit/authorised_user_constraint_spec.rb
|
286
272
|
- spec/unit/bearer_token_spec.rb
|
287
273
|
- spec/unit/config_spec.rb
|
274
|
+
- spec/unit/controller_methods_spec.rb
|
288
275
|
- spec/unit/gds_sso_spec.rb
|
289
276
|
- spec/unit/mock_bearer_token_spec.rb
|
290
277
|
- spec/unit/railtie_spec.rb
|
@@ -312,8 +299,6 @@ rubygems_version: 3.7.1
|
|
312
299
|
specification_version: 4
|
313
300
|
summary: Client for GDS' OAuth 2-based SSO
|
314
301
|
test_files:
|
315
|
-
- spec/controller/api_user_controller_spec.rb
|
316
|
-
- spec/controller/controller_methods_spec.rb
|
317
302
|
- spec/internal/app/assets/config/manifest.js
|
318
303
|
- spec/internal/app/controllers/application_controller.rb
|
319
304
|
- spec/internal/app/controllers/example_controller.rb
|
@@ -322,17 +307,20 @@ test_files:
|
|
322
307
|
- spec/internal/config/initializers/gds-sso.rb
|
323
308
|
- spec/internal/config/routes.rb
|
324
309
|
- spec/internal/db/schema.rb
|
310
|
+
- spec/request/api/user_spec.rb
|
311
|
+
- spec/request/authentication_spec.rb
|
312
|
+
- spec/request/demo_app_spec.rb
|
325
313
|
- spec/spec_helper.rb
|
326
314
|
- spec/support/controller_spy.rb
|
315
|
+
- spec/support/request_helpers.rb
|
327
316
|
- spec/support/serializable_user.rb
|
328
317
|
- spec/support/test_user.rb
|
329
|
-
- spec/support/timecop.rb
|
330
|
-
- spec/system/authentication_and_authorisation_spec.rb
|
331
318
|
- spec/unit/api_access_spec.rb
|
332
319
|
- spec/unit/authorise_user_spec.rb
|
333
320
|
- spec/unit/authorised_user_constraint_spec.rb
|
334
321
|
- spec/unit/bearer_token_spec.rb
|
335
322
|
- spec/unit/config_spec.rb
|
323
|
+
- spec/unit/controller_methods_spec.rb
|
336
324
|
- spec/unit/gds_sso_spec.rb
|
337
325
|
- spec/unit/mock_bearer_token_spec.rb
|
338
326
|
- spec/unit/railtie_spec.rb
|
@@ -1,114 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
def user_update_json
|
4
|
-
{
|
5
|
-
"user" => {
|
6
|
-
"uid" => @user_to_update.uid,
|
7
|
-
"name" => "Joshua Marshall",
|
8
|
-
"email" => "user@domain.com",
|
9
|
-
"permissions" => ["signin", "new permission"],
|
10
|
-
"organisation_slug" => "justice-league",
|
11
|
-
"organisation_content_id" => "aae1319e-5788-4677-998c-f1a53af528d0",
|
12
|
-
"disabled" => false,
|
13
|
-
},
|
14
|
-
}.to_json
|
15
|
-
end
|
16
|
-
|
17
|
-
describe Api::UserController, type: :controller do
|
18
|
-
before :each do
|
19
|
-
user_to_update_attrs = [{
|
20
|
-
uid: "a1s2d3#{rand(10_000)}",
|
21
|
-
email: "old@domain.com",
|
22
|
-
name: "Moshua Jarshall",
|
23
|
-
permissions: %w[signin],
|
24
|
-
}]
|
25
|
-
|
26
|
-
signon_sso_push_user_attrs = [{
|
27
|
-
uid: "a1s2d3#{rand(10_000)}",
|
28
|
-
email: "ssopushuser@legit.com",
|
29
|
-
name: "SSO Push user",
|
30
|
-
permissions: %w[signin user_update_permission],
|
31
|
-
}]
|
32
|
-
|
33
|
-
@user_to_update = User.create!(*user_to_update_attrs)
|
34
|
-
@signon_sso_push_user = User.create!(*signon_sso_push_user_attrs)
|
35
|
-
end
|
36
|
-
|
37
|
-
describe "PUT update" do
|
38
|
-
it "should deny access to anybody but the API user (or a user with 'user_update_permission')" do
|
39
|
-
malicious_user = User.new({
|
40
|
-
uid: "2",
|
41
|
-
name: "User",
|
42
|
-
permissions: %w[signin],
|
43
|
-
})
|
44
|
-
|
45
|
-
request.env["warden"] = double("stub warden", authenticate!: true, authenticated?: true, user: malicious_user)
|
46
|
-
|
47
|
-
request.env["RAW_POST_DATA"] = user_update_json
|
48
|
-
put :update, body: user_update_json, params: { uid: @user_to_update.uid }
|
49
|
-
|
50
|
-
expect(response.status).to eq(403)
|
51
|
-
end
|
52
|
-
|
53
|
-
it "should create/update the user record in the same way as the OAuth callback" do
|
54
|
-
# Test that it authenticates
|
55
|
-
request.env["warden"] = double("mock warden")
|
56
|
-
expect(request.env["warden"]).to receive(:authenticate!).at_least(:once).and_return(true)
|
57
|
-
expect(request.env["warden"]).to receive(:authenticated?).at_least(:once).and_return(true)
|
58
|
-
expect(request.env["warden"]).to receive(:user).at_least(:once).and_return(@signon_sso_push_user)
|
59
|
-
|
60
|
-
request.env["RAW_POST_DATA"] = user_update_json
|
61
|
-
put :update, body: user_update_json, params: { uid: @user_to_update.uid }
|
62
|
-
|
63
|
-
@user_to_update.reload
|
64
|
-
expect(@user_to_update.name).to eq("Joshua Marshall")
|
65
|
-
expect(@user_to_update.email).to eq("user@domain.com")
|
66
|
-
expect(@user_to_update.permissions).to eq(["signin", "new permission"])
|
67
|
-
expect(@user_to_update.organisation_slug).to eq("justice-league")
|
68
|
-
expect(@user_to_update.organisation_content_id).to eq("aae1319e-5788-4677-998c-f1a53af528d0")
|
69
|
-
expect(response.content_type).to eq("text/plain")
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
describe "POST reauth" do
|
74
|
-
it "should deny access to anybody but the API user (or a user with 'user_update_permission')" do
|
75
|
-
malicious_user = User.new({
|
76
|
-
uid: "2",
|
77
|
-
name: "User",
|
78
|
-
permissions: %w[signin],
|
79
|
-
})
|
80
|
-
|
81
|
-
request.env["warden"] = double("stub warden", authenticate!: true, authenticated?: true, user: malicious_user)
|
82
|
-
|
83
|
-
post :reauth, params: { uid: @user_to_update.uid }
|
84
|
-
|
85
|
-
expect(response.status).to eq(403)
|
86
|
-
end
|
87
|
-
|
88
|
-
it "should return success if user record doesn't exist" do
|
89
|
-
request.env["warden"] = double("mock warden")
|
90
|
-
expect(request.env["warden"]).to receive(:authenticate!).at_least(:once).and_return(true)
|
91
|
-
expect(request.env["warden"]).to receive(:authenticated?).at_least(:once).and_return(true)
|
92
|
-
expect(request.env["warden"]).to receive(:user).at_least(:once).and_return(@signon_sso_push_user)
|
93
|
-
|
94
|
-
post :reauth, params: { uid: "nonexistent-user" }
|
95
|
-
|
96
|
-
expect(response.status).to eq(200)
|
97
|
-
expect(response.content_type).to eq("text/plain")
|
98
|
-
end
|
99
|
-
|
100
|
-
it "should set remotely_signed_out to true on the user" do
|
101
|
-
# Test that it authenticates
|
102
|
-
request.env["warden"] = double("mock warden")
|
103
|
-
expect(request.env["warden"]).to receive(:authenticate!).at_least(:once).and_return(true)
|
104
|
-
expect(request.env["warden"]).to receive(:authenticated?).at_least(:once).and_return(true)
|
105
|
-
expect(request.env["warden"]).to receive(:user).at_least(:once).and_return(@signon_sso_push_user)
|
106
|
-
|
107
|
-
post :reauth, params: { uid: @user_to_update.uid }
|
108
|
-
|
109
|
-
@user_to_update.reload
|
110
|
-
expect(@user_to_update).to be_remotely_signed_out
|
111
|
-
expect(response.content_type).to eq("text/plain")
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
data/spec/support/timecop.rb
DELETED
@@ -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
|