machina-auth 0.4.0 → 1.0.1
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/README.md +2 -1
- data/app/controllers/machina/callbacks_controller.rb +52 -0
- data/config/routes.rb +1 -0
- data/lib/machina/controller_helpers.rb +17 -1
- data/lib/machina/middleware/authentication.rb +12 -9
- data/lib/machina/version.rb +1 -1
- data/lib/machina.rb +21 -4
- data/spec/machina/authorize_url_spec.rb +24 -0
- data/spec/machina/controller_helpers_spec.rb +20 -0
- data/spec/machina/middleware/authentication_spec.rb +33 -15
- data/spec/requests/machina/callbacks_spec.rb +123 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b6572b3ef8cb554a701292ebeca7442e4267c7fea9f3c8fc8635067f02ce6fc
|
|
4
|
+
data.tar.gz: 0e25ba27df749426535443c1c8ff5b165e1ed02658d15ced035f9256b3ed731f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19fb58e16fd547814694f94b976ad0baebd1fb2740ffabf709c28a6ed6ab614bb75ad58ac40743d791c51cbf1840700baad43c63ba4bc5518551d6d3a6eb60db
|
|
7
|
+
data.tar.gz: 9e710c4519b897849a7d29dd0b93663a94627b5141d89893493decb2f16d3de4d9621b25db6080cae340916cf4b92c860dba6d9e5fe8a21350e7820f30229d57
|
data/README.md
CHANGED
|
@@ -4,7 +4,8 @@ Rails engine that integrates product apps with the Machina Console identity serv
|
|
|
4
4
|
|
|
5
5
|
## What It Provides
|
|
6
6
|
|
|
7
|
-
- **Authentication middleware** — extracts session tokens from cookies
|
|
7
|
+
- **Authentication middleware** — extracts session tokens from cookies or headers and resolves them against the Console
|
|
8
|
+
- **Auth callback** — the engine serves `GET <mount>/callback`, completing the Console login handshake with CSRF-state verification (see `docs/PRODUCT_CALLBACK_FLOW.md`)
|
|
8
9
|
- **`Machina::Authorized`** — frozen value object with `can?`, `cannot?`, `authorize!`, and permission query methods
|
|
9
10
|
- **`Machina::Current`** — thread-safe current attributes (user, org, workspace, session)
|
|
10
11
|
- **`Machina::ControllerHelpers`** — `require_authorized!` and `authorize!` for controllers
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Machina
|
|
4
|
+
# Completes the Console auth handshake. Console redirects here with the
|
|
5
|
+
# minted session token; the state nonce issued by +authenticate!+ must be
|
|
6
|
+
# echoed back or the token is rejected (login-CSRF / session fixation).
|
|
7
|
+
# The engine has no ApplicationController; Base is required for cookies.
|
|
8
|
+
class CallbacksController < ActionController::Base # rubocop:disable Rails/ApplicationController
|
|
9
|
+
def show
|
|
10
|
+
return reject('missing token') if params[:token].blank?
|
|
11
|
+
return reject('state mismatch') unless valid_state?
|
|
12
|
+
|
|
13
|
+
cookies.delete(:machina_state)
|
|
14
|
+
reset_session
|
|
15
|
+
store_session_cookie
|
|
16
|
+
redirect_to safe_return_to
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def valid_state?
|
|
22
|
+
expected = cookies.signed[:machina_state]
|
|
23
|
+
given = params[:state].to_s
|
|
24
|
+
expected.present? && given.present? && ActiveSupport::SecurityUtils.secure_compare(given, expected)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reject(reason)
|
|
28
|
+
cookies.delete(:machina_state)
|
|
29
|
+
Rails.logger.warn("[machina] Auth callback rejected: #{reason}")
|
|
30
|
+
render plain: 'Invalid authentication callback', status: :unprocessable_content
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def store_session_cookie
|
|
34
|
+
cookies[:machina_session] = {
|
|
35
|
+
value: params[:token],
|
|
36
|
+
httponly: true,
|
|
37
|
+
same_site: :lax,
|
|
38
|
+
secure: Machina.secure_cookies?
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Relative paths only. Reject a leading // or /\ — browsers treat both as
|
|
43
|
+
# protocol-relative, so either is an open redirect on a credential-bearing
|
|
44
|
+
# request (a backslash also trips Rails' UnsafeRedirectError as a 500).
|
|
45
|
+
def safe_return_to
|
|
46
|
+
value = params[:return_to].to_s
|
|
47
|
+
return value if value.start_with?('/') && !value.match?(%r{\A/[/\\]})
|
|
48
|
+
|
|
49
|
+
'/'
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -25,7 +25,8 @@ module Machina
|
|
|
25
25
|
if request.format.json?
|
|
26
26
|
render json: { error: 'unauthorized' }, status: :unauthorized
|
|
27
27
|
else
|
|
28
|
-
redirect_to Machina.authorize_url(return_to: request.
|
|
28
|
+
redirect_to Machina.authorize_url(return_to: request.fullpath, state: issue_state),
|
|
29
|
+
allow_other_host: true
|
|
29
30
|
end
|
|
30
31
|
end
|
|
31
32
|
|
|
@@ -49,6 +50,21 @@ module Machina
|
|
|
49
50
|
|
|
50
51
|
private
|
|
51
52
|
|
|
53
|
+
# Mints the single-use CSRF state for the auth handshake. The value rides
|
|
54
|
+
# on the callback URL through Console; the callback rejects a redirect
|
|
55
|
+
# whose state does not match this cookie.
|
|
56
|
+
def issue_state
|
|
57
|
+
state = SecureRandom.urlsafe_base64(32)
|
|
58
|
+
cookies.signed[:machina_state] = {
|
|
59
|
+
value: state,
|
|
60
|
+
httponly: true,
|
|
61
|
+
same_site: :lax,
|
|
62
|
+
secure: Machina.secure_cookies?,
|
|
63
|
+
expires: 10.minutes
|
|
64
|
+
}
|
|
65
|
+
state
|
|
66
|
+
end
|
|
67
|
+
|
|
52
68
|
def respond_unauthorized(error)
|
|
53
69
|
if request.format.json?
|
|
54
70
|
render json: { error: 'forbidden', permission: error.message }, status: :forbidden
|
|
@@ -20,7 +20,7 @@ module Machina
|
|
|
20
20
|
|
|
21
21
|
def call(env)
|
|
22
22
|
request = ActionDispatch::Request.new(env)
|
|
23
|
-
return @app.call(env) if skip_path?(request)
|
|
23
|
+
return @app.call(env) if callback_path?(request) || skip_path?(request)
|
|
24
24
|
|
|
25
25
|
token = extract_token(request)
|
|
26
26
|
return @app.call(env) if token.blank?
|
|
@@ -51,6 +51,16 @@ module Machina
|
|
|
51
51
|
result == TRANSIENT_FAILURE
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# The callback sets the session cookie itself; resolving a stale cookie
|
|
55
|
+
# here would answer it with an expired-token deletion that clobbers the
|
|
56
|
+
# fresh cookie.
|
|
57
|
+
def callback_path?(request)
|
|
58
|
+
callback = Machina.config.identity_callback_uri
|
|
59
|
+
callback.present? && request.path == URI.parse(callback).path
|
|
60
|
+
rescue URI::InvalidURIError
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
54
64
|
# Strings match as path prefixes; regexes match path or full URL.
|
|
55
65
|
def skip_path?(request)
|
|
56
66
|
return false if Machina.config.skip_paths.blank?
|
|
@@ -75,18 +85,11 @@ module Machina
|
|
|
75
85
|
end
|
|
76
86
|
|
|
77
87
|
def extract_token(request)
|
|
78
|
-
|
|
79
|
-
|| request.cookies['machina_session']
|
|
88
|
+
request.cookies['machina_session']
|
|
80
89
|
|| extract_bearer(request)
|
|
81
90
|
|| request.headers['X-Api-Key']
|
|
82
91
|
end
|
|
83
92
|
|
|
84
|
-
def extract_param_token(request)
|
|
85
|
-
request.params['token']
|
|
86
|
-
rescue ActionDispatch::Http::Parameters::ParseError
|
|
87
|
-
nil
|
|
88
|
-
end
|
|
89
|
-
|
|
90
93
|
def extract_bearer(request)
|
|
91
94
|
auth_header = request.headers['Authorization'].to_s
|
|
92
95
|
match = auth_header.match(/\ABearer\s+(.+)\z/)
|
data/lib/machina/version.rb
CHANGED
data/lib/machina.rb
CHANGED
|
@@ -71,11 +71,14 @@ module Machina
|
|
|
71
71
|
# present the callback URI config is ignored
|
|
72
72
|
# @param return_to [String, nil] user's intended destination, appended to
|
|
73
73
|
# +identity_callback_uri+ so the product app can restore it after auth
|
|
74
|
+
# @param state [String, nil] CSRF nonce appended to the redirect target;
|
|
75
|
+
# Console echoes it back and the callback rejects a mismatch
|
|
74
76
|
# @return [String] the full Console authorize URL
|
|
75
77
|
# @raise [ConfigurationError] when +redirect_to+ is omitted and
|
|
76
78
|
# +identity_callback_uri+ is not configured
|
|
77
|
-
def authorize_url(redirect_to: nil, return_to: nil)
|
|
79
|
+
def authorize_url(redirect_to: nil, return_to: nil, state: nil)
|
|
78
80
|
redirect_target = redirect_to.presence || callback_redirect_target(return_to)
|
|
81
|
+
redirect_target = append_query_param(redirect_target, 'state', state) if state.present?
|
|
79
82
|
|
|
80
83
|
base = config.identity_service_url.to_s.sub(%r{/\z}, '')
|
|
81
84
|
"#{base}/authorize?redirect_to=#{CGI.escape(redirect_target)}"
|
|
@@ -84,9 +87,18 @@ module Machina
|
|
|
84
87
|
# Convenience wrapper that delegates to {authorize_url} with +return_to+.
|
|
85
88
|
#
|
|
86
89
|
# @param return_to [String] the user's intended destination
|
|
90
|
+
# @param state [String, nil] CSRF nonce (see {authorize_url})
|
|
87
91
|
# @return [String] the full authorize URL
|
|
88
|
-
def login_url(return_to:)
|
|
89
|
-
authorize_url(return_to:)
|
|
92
|
+
def login_url(return_to:, state: nil)
|
|
93
|
+
authorize_url(return_to:, state:)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# True when session/state cookies must carry the Secure flag. Forced on
|
|
97
|
+
# outside development/test: +request.ssl?+ is false behind TLS-terminating
|
|
98
|
+
# load balancers unless the app sets assume_ssl, silently downgrading the
|
|
99
|
+
# cookie.
|
|
100
|
+
def secure_cookies?
|
|
101
|
+
!Rails.env.local?
|
|
90
102
|
end
|
|
91
103
|
|
|
92
104
|
private
|
|
@@ -105,7 +117,12 @@ module Machina
|
|
|
105
117
|
'identity_callback_uri must be configured to use authorize_url without an explicit redirect_to'
|
|
106
118
|
end
|
|
107
119
|
|
|
108
|
-
return_to.present? ?
|
|
120
|
+
return_to.present? ? append_query_param(callback, 'return_to', return_to) : callback
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def append_query_param(url, key, value)
|
|
124
|
+
separator = url.include?('?') ? '&' : '?'
|
|
125
|
+
"#{url}#{separator}#{key}=#{CGI.escape(value)}"
|
|
109
126
|
end
|
|
110
127
|
end
|
|
111
128
|
end
|
|
@@ -29,6 +29,30 @@ RSpec.describe 'Machina.authorize_url' do
|
|
|
29
29
|
callback_with_return = 'http://localhost:3000/auth/machina/callback?return_to=http%3A%2F%2Flocalhost%3A3000%2Finquiries%3Fpage%3D2'
|
|
30
30
|
expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape(callback_with_return)}")
|
|
31
31
|
end
|
|
32
|
+
|
|
33
|
+
it 'appends state to the callback URI' do
|
|
34
|
+
url = Machina.authorize_url(state: 'abc123')
|
|
35
|
+
|
|
36
|
+
callback_with_state = 'http://localhost:3000/auth/machina/callback?state=abc123'
|
|
37
|
+
expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape(callback_with_state)}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'appends state after return_to with an ampersand' do
|
|
41
|
+
url = Machina.authorize_url(return_to: '/dashboard', state: 'abc123')
|
|
42
|
+
|
|
43
|
+
callback = 'http://localhost:3000/auth/machina/callback?return_to=%2Fdashboard&state=abc123'
|
|
44
|
+
expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape(callback)}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'escapes the state value' do
|
|
48
|
+
url = Machina.authorize_url(state: 'a+b/c=')
|
|
49
|
+
|
|
50
|
+
expect(CGI.unescape(url)).to include("state=#{CGI.escape('a+b/c=')}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'omits state when not given' do
|
|
54
|
+
expect(Machina.authorize_url).not_to include('state')
|
|
55
|
+
end
|
|
32
56
|
end
|
|
33
57
|
|
|
34
58
|
context 'when identity_callback_uri is not configured' do
|
|
@@ -40,6 +40,26 @@ RSpec.describe Machina::ControllerHelpers, type: :controller do
|
|
|
40
40
|
expect(CGI.unescape(response.location)).to include('http://localhost:3000/auth/machina/callback?return_to=')
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
it 'mints a state cookie and threads the same value through the redirect' do
|
|
44
|
+
Machina.config.identity_callback_uri = 'http://localhost:3000/auth/machina/callback'
|
|
45
|
+
|
|
46
|
+
get :index
|
|
47
|
+
|
|
48
|
+
state = controller.send(:cookies).signed[:machina_state]
|
|
49
|
+
expect(state).to be_present
|
|
50
|
+
expect(CGI.unescape(response.location)).to include("state=#{CGI.escape(state)}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'passes return_to as a relative path so the callback accepts it' do
|
|
54
|
+
Machina.config.identity_callback_uri = 'http://localhost:3000/auth/machina/callback'
|
|
55
|
+
|
|
56
|
+
get :index
|
|
57
|
+
|
|
58
|
+
# request.fullpath, not the absolute original_url — safe_return_to in the
|
|
59
|
+
# callback rejects absolute URLs and would otherwise drop the destination.
|
|
60
|
+
expect(CGI.unescape(response.location)).to include("return_to=#{CGI.escape('/index')}")
|
|
61
|
+
end
|
|
62
|
+
|
|
43
63
|
it 'returns json unauthorized for api-style requests' do
|
|
44
64
|
request.accept = 'application/json'
|
|
45
65
|
get :index, format: :json
|
|
@@ -218,32 +218,50 @@ RSpec.describe Machina::Middleware::Authentication do
|
|
|
218
218
|
expect(identity_client).to have_received(:resolve_session).with(token).once
|
|
219
219
|
end
|
|
220
220
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
describe 'token query param' do
|
|
222
|
+
it 'never authenticates a request' do
|
|
223
|
+
allow(identity_client).to receive(:resolve_session)
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
Machina::IdentityClient::Response.new(status: 404, body: '{}'),
|
|
228
|
-
)
|
|
225
|
+
env = Rack::MockRequest.env_for('/resource?token=ps_leaked_in_url')
|
|
226
|
+
status, _headers, body = middleware.call(env)
|
|
229
227
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
)
|
|
228
|
+
expect(status).to eq(200)
|
|
229
|
+
expect(JSON.parse(body.first)['user_id']).to be_nil
|
|
230
|
+
expect(identity_client).not_to have_received(:resolve_session)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
describe 'callback path' do
|
|
235
|
+
before do
|
|
236
|
+
Machina.config.identity_callback_uri = 'http://localhost:3000/machina/callback'
|
|
233
237
|
end
|
|
234
238
|
|
|
235
|
-
it '
|
|
239
|
+
it 'is skipped entirely, even with a stale session cookie' do
|
|
240
|
+
allow(identity_client).to receive(:resolve_session)
|
|
241
|
+
|
|
236
242
|
env = Rack::MockRequest.env_for(
|
|
237
|
-
'/
|
|
243
|
+
'/machina/callback?token=ps_fresh&state=abc',
|
|
238
244
|
'HTTP_COOKIE' => 'machina_session=ps_expired_cookie',
|
|
239
245
|
)
|
|
240
246
|
|
|
247
|
+
_status, headers, = middleware.call(env)
|
|
248
|
+
|
|
249
|
+
# The stale cookie must not be resolved or deleted — the callback
|
|
250
|
+
# controller overwrites it with the fresh token.
|
|
251
|
+
expect(identity_client).not_to have_received(:resolve_session)
|
|
252
|
+
expect(headers['set-cookie']).to be_nil
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it 'does not skip other paths' do
|
|
256
|
+
allow(identity_client).to receive(:resolve_session).with('ps_123').and_return(
|
|
257
|
+
Machina::IdentityClient::Response.new(status: 200, body: MockResponses.session_resolution),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
env = Rack::MockRequest.env_for('/resource', 'HTTP_COOKIE' => 'machina_session=ps_123')
|
|
241
261
|
status, _headers, body = middleware.call(env)
|
|
242
262
|
|
|
243
263
|
expect(status).to eq(200)
|
|
244
|
-
|
|
245
|
-
expect(parsed['user_id']).to eq(MockResponses.session_resolution['data']['user']['id'])
|
|
246
|
-
expect(identity_client).not_to have_received(:resolve_session).with(stale_token)
|
|
264
|
+
expect(JSON.parse(body.first)['user_id']).to be_present
|
|
247
265
|
end
|
|
248
266
|
end
|
|
249
267
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../rails_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Machina::CallbacksController, type: :controller do
|
|
6
|
+
routes { Machina::Engine.routes }
|
|
7
|
+
|
|
8
|
+
let(:state) { 'a-random-state-value' }
|
|
9
|
+
let(:token) { 'ps_fresh_token' }
|
|
10
|
+
|
|
11
|
+
def sign_state_cookie(value)
|
|
12
|
+
cookies.signed[:machina_state] = value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe 'with a valid state' do
|
|
16
|
+
before { sign_state_cookie(state) }
|
|
17
|
+
|
|
18
|
+
it 'stores the session cookie and redirects to return_to' do
|
|
19
|
+
get :show, params: { token:, state:, return_to: '/dashboard' }
|
|
20
|
+
|
|
21
|
+
expect(response).to redirect_to('/dashboard')
|
|
22
|
+
expect(response.cookies['machina_session']).to eq(token)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'sets the session cookie httponly' do
|
|
26
|
+
get :show, params: { token:, state: }
|
|
27
|
+
|
|
28
|
+
set_cookie = response.headers['Set-Cookie'].to_s
|
|
29
|
+
expect(set_cookie).to match(/machina_session=.*httponly/i)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'clears the state cookie' do
|
|
33
|
+
get :show, params: { token:, state: }
|
|
34
|
+
|
|
35
|
+
expect(response.cookies).to include('machina_state' => nil)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'resets the session on credential swap' do
|
|
39
|
+
session[:stale] = 'pre-login'
|
|
40
|
+
|
|
41
|
+
get :show, params: { token:, state: }
|
|
42
|
+
|
|
43
|
+
expect(session[:stale]).to be_nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'overwrites a stale session cookie' do
|
|
47
|
+
request.cookies['machina_session'] = 'ps_stale'
|
|
48
|
+
|
|
49
|
+
get :show, params: { token:, state: }
|
|
50
|
+
|
|
51
|
+
expect(response.cookies['machina_session']).to eq(token)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'defaults to root when return_to is blank' do
|
|
55
|
+
get :show, params: { token:, state: }
|
|
56
|
+
|
|
57
|
+
expect(response).to redirect_to('/')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'rejects an absolute return_to' do
|
|
61
|
+
get :show, params: { token:, state:, return_to: 'https://evil.example.com/' }
|
|
62
|
+
|
|
63
|
+
expect(response).to redirect_to('/')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'rejects a protocol-relative return_to' do
|
|
67
|
+
get :show, params: { token:, state:, return_to: '//evil.example.com/' }
|
|
68
|
+
|
|
69
|
+
expect(response).to redirect_to('/')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'rejects a backslash protocol-relative return_to' do
|
|
73
|
+
get :show, params: { token:, state:, return_to: '/\\evil.example.com' }
|
|
74
|
+
|
|
75
|
+
expect(response).to redirect_to('/')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe 'with an invalid state' do
|
|
80
|
+
it 'rejects a mismatched state' do
|
|
81
|
+
sign_state_cookie(state)
|
|
82
|
+
|
|
83
|
+
get :show, params: { token:, state: 'forged' }
|
|
84
|
+
|
|
85
|
+
expect(response).to have_http_status(:unprocessable_content)
|
|
86
|
+
expect(response.cookies['machina_session']).to be_nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'rejects a missing state param' do
|
|
90
|
+
sign_state_cookie(state)
|
|
91
|
+
|
|
92
|
+
get :show, params: { token: }
|
|
93
|
+
|
|
94
|
+
expect(response).to have_http_status(:unprocessable_content)
|
|
95
|
+
expect(response.cookies['machina_session']).to be_nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'rejects when no state cookie was issued' do
|
|
99
|
+
get :show, params: { token:, state: }
|
|
100
|
+
|
|
101
|
+
expect(response).to have_http_status(:unprocessable_content)
|
|
102
|
+
expect(response.cookies['machina_session']).to be_nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'rejects a tampered (unsigned) state cookie' do
|
|
106
|
+
request.cookies['machina_state'] = state
|
|
107
|
+
|
|
108
|
+
get :show, params: { token:, state: }
|
|
109
|
+
|
|
110
|
+
expect(response).to have_http_status(:unprocessable_content)
|
|
111
|
+
expect(response.cookies['machina_session']).to be_nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'rejects a blank token' do
|
|
116
|
+
sign_state_cookie(state)
|
|
117
|
+
|
|
118
|
+
get :show, params: { state: }
|
|
119
|
+
|
|
120
|
+
expect(response).to have_http_status(:unprocessable_content)
|
|
121
|
+
expect(response.cookies['machina_session']).to be_nil
|
|
122
|
+
end
|
|
123
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: machina-auth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ZAR
|
|
@@ -137,6 +137,7 @@ extra_rdoc_files: []
|
|
|
137
137
|
files:
|
|
138
138
|
- Gemfile
|
|
139
139
|
- README.md
|
|
140
|
+
- app/controllers/machina/callbacks_controller.rb
|
|
140
141
|
- app/controllers/machina/webhooks_controller.rb
|
|
141
142
|
- config/routes.rb
|
|
142
143
|
- lib/generators/machina/install_generator.rb
|
|
@@ -188,6 +189,7 @@ files:
|
|
|
188
189
|
- spec/machina/webhook_receiver_spec.rb
|
|
189
190
|
- spec/machina/workspace_scoped_spec.rb
|
|
190
191
|
- spec/rails_helper.rb
|
|
192
|
+
- spec/requests/machina/callbacks_spec.rb
|
|
191
193
|
- spec/requests/machina/webhooks_spec.rb
|
|
192
194
|
- spec/spec_helper.rb
|
|
193
195
|
- spec/support/console_schema.rb
|