machina-auth 1.0.2 → 1.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43343ff99400bd16b180b6df3091ec04b98808ee6f3109292fbdcc357289543c
|
|
4
|
+
data.tar.gz: bea7a1a87b91e44fbd9e2fafd4235e2083aa11160e93a5fbae79bb96edd0aa18
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 34ee949454a8421c955c5ba1c52f00ae768629747c305d989760bd40012c0e445bc6b3ce3181326284ad0b8fd942dea6197af2a95bc57e01a0ed845cfdcea351
|
|
7
|
+
data.tar.gz: b3d44a849ca3cd050ea7b85a393c922536d48065e9f0158d6a47f39cd44985b213d8d902f049df14274e2f8808ba184dd6ffc428606b971c5cf5c96a998cd085
|
|
@@ -6,11 +6,17 @@ module Machina
|
|
|
6
6
|
# echoed back or the token is rejected (login-CSRF / session fixation).
|
|
7
7
|
# The engine has no ApplicationController; Base is required for cookies.
|
|
8
8
|
class CallbacksController < ActionController::Base # rubocop:disable Rails/ApplicationController
|
|
9
|
+
include Machina::ControllerHelpers
|
|
10
|
+
|
|
11
|
+
# Duplicate callbacks from the same browser (a double-submitted Console
|
|
12
|
+
# picker) arrive within seconds; the state stays valid this long after use.
|
|
13
|
+
STATE_GRACE_PERIOD = 1.minute
|
|
14
|
+
|
|
9
15
|
def show
|
|
10
16
|
return reject('missing token') if params[:token].blank?
|
|
11
|
-
return
|
|
17
|
+
return restart_handshake('state mismatch') unless valid_state?
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
extend_state_cookie
|
|
14
20
|
reset_session
|
|
15
21
|
store_session_cookie
|
|
16
22
|
redirect_to safe_return_to
|
|
@@ -30,6 +36,23 @@ module Machina
|
|
|
30
36
|
render plain: 'Invalid authentication callback', status: :unprocessable_content
|
|
31
37
|
end
|
|
32
38
|
|
|
39
|
+
# A stale, overwritten or missing state proves nothing either way, so
|
|
40
|
+
# re-run the handshake with a fresh nonce instead of dead-ending.
|
|
41
|
+
def restart_handshake(reason)
|
|
42
|
+
Rails.logger.warn("[machina] Auth callback rejected: #{reason}; restarting handshake")
|
|
43
|
+
redirect_to Machina.authorize_url(return_to: safe_return_to, state: issue_state), allow_other_host: true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def extend_state_cookie
|
|
47
|
+
cookies.signed[:machina_state] = {
|
|
48
|
+
value: cookies.signed[:machina_state],
|
|
49
|
+
httponly: true,
|
|
50
|
+
same_site: :lax,
|
|
51
|
+
secure: Machina.secure_cookies?,
|
|
52
|
+
expires: STATE_GRACE_PERIOD
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
33
56
|
def store_session_cookie
|
|
34
57
|
cookies[:machina_session] = {
|
|
35
58
|
value: params[:token],
|
|
@@ -50,7 +50,7 @@ module Machina
|
|
|
50
50
|
|
|
51
51
|
private
|
|
52
52
|
|
|
53
|
-
# Mints the
|
|
53
|
+
# Mints the CSRF state for the auth handshake. The value rides
|
|
54
54
|
# on the callback URL through Console; the callback rejects a redirect
|
|
55
55
|
# whose state does not match this cookie.
|
|
56
56
|
def issue_state
|
data/lib/machina/version.rb
CHANGED
|
@@ -29,10 +29,21 @@ RSpec.describe Machina::CallbacksController, type: :controller do
|
|
|
29
29
|
expect(set_cookie).to match(/machina_session=.*httponly/i)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
it '
|
|
32
|
+
it 'keeps the state cookie so a duplicate callback still succeeds' do
|
|
33
33
|
get :show, params: { token:, state: }
|
|
34
34
|
|
|
35
|
-
expect(response.cookies).to
|
|
35
|
+
expect(response.cookies['machina_state']).to be_present
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'accepts a repeated callback and stores the newest token' do
|
|
39
|
+
get :show, params: { token: 'ps_first', state: }
|
|
40
|
+
request.cookies['machina_state'] = response.cookies['machina_state']
|
|
41
|
+
request.cookies['machina_session'] = 'ps_first'
|
|
42
|
+
|
|
43
|
+
get :show, params: { token: 'ps_second', state:, return_to: '/dashboard' }
|
|
44
|
+
|
|
45
|
+
expect(response).to redirect_to('/dashboard')
|
|
46
|
+
expect(response.cookies['machina_session']).to eq('ps_second')
|
|
36
47
|
end
|
|
37
48
|
|
|
38
49
|
it 'resets the session on credential swap' do
|
|
@@ -77,38 +88,54 @@ RSpec.describe Machina::CallbacksController, type: :controller do
|
|
|
77
88
|
end
|
|
78
89
|
|
|
79
90
|
describe 'with an invalid state' do
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
before { Machina.config.identity_callback_uri = 'http://localhost:3000/machina/callback' }
|
|
92
|
+
|
|
93
|
+
shared_examples 'restarts the handshake' do
|
|
94
|
+
it 'redirects to Console with a fresh state and no session cookie' do
|
|
95
|
+
expect(response).to redirect_to(%r{\Ahttps://machina\.example\.test/authorize\?redirect_to=})
|
|
96
|
+
expect(CGI.unescape(response.location)).to include('return_to=%2Fdashboard', 'state=')
|
|
97
|
+
expect(response.cookies['machina_state']).to be_present
|
|
98
|
+
expect(response.cookies['machina_session']).to be_nil
|
|
99
|
+
end
|
|
100
|
+
end
|
|
82
101
|
|
|
83
|
-
|
|
102
|
+
context 'with a mismatched state' do
|
|
103
|
+
before do
|
|
104
|
+
sign_state_cookie(state)
|
|
105
|
+
get :show, params: { token:, state: 'forged', return_to: '/dashboard' }
|
|
106
|
+
end
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
expect(response.cookies['machina_session']).to be_nil
|
|
108
|
+
include_examples 'restarts the handshake'
|
|
87
109
|
end
|
|
88
110
|
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
context 'with a missing state param' do
|
|
112
|
+
before do
|
|
113
|
+
sign_state_cookie(state)
|
|
114
|
+
get :show, params: { token:, return_to: '/dashboard' }
|
|
115
|
+
end
|
|
91
116
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
expect(response).to have_http_status(:unprocessable_content)
|
|
95
|
-
expect(response.cookies['machina_session']).to be_nil
|
|
117
|
+
include_examples 'restarts the handshake'
|
|
96
118
|
end
|
|
97
119
|
|
|
98
|
-
|
|
99
|
-
get :show, params: { token:, state: }
|
|
120
|
+
context 'when no state cookie was issued' do
|
|
121
|
+
before { get :show, params: { token:, state:, return_to: '/dashboard' } }
|
|
100
122
|
|
|
101
|
-
|
|
102
|
-
expect(response.cookies['machina_session']).to be_nil
|
|
123
|
+
include_examples 'restarts the handshake'
|
|
103
124
|
end
|
|
104
125
|
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
context 'with a tampered (unsigned) state cookie' do
|
|
127
|
+
before do
|
|
128
|
+
request.cookies['machina_state'] = state
|
|
129
|
+
get :show, params: { token:, state:, return_to: '/dashboard' }
|
|
130
|
+
end
|
|
107
131
|
|
|
108
|
-
|
|
132
|
+
include_examples 'restarts the handshake'
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'does not reflect an unsafe return_to into the restarted handshake' do
|
|
136
|
+
get :show, params: { token:, state:, return_to: '//evil.example.com/' }
|
|
109
137
|
|
|
110
|
-
expect(response).to
|
|
111
|
-
expect(response.cookies['machina_session']).to be_nil
|
|
138
|
+
expect(CGI.unescape(response.location)).to include('return_to=%2F&').or include('return_to=%2F"')
|
|
112
139
|
end
|
|
113
140
|
end
|
|
114
141
|
|