rapid-rack 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: af245c337b3548052eb181b90d9e817a0c7f993d
4
- data.tar.gz: 4d8dabe27aeadf4221dedcfa547c8e3cfc16a11d
3
+ metadata.gz: 8a7a45733ec5f445a2b7cca550518cfcf6758508
4
+ data.tar.gz: 71743e8d43c0f32a356f502dbea161231aae1f73
5
5
  SHA512:
6
- metadata.gz: fca522118573223624ea3ba0d22d7ea8634ea87a4e293134cef252a8fda9d928da05b97fe03111b0047d3dd63fbbec9ff1e9fb97bda040e3041bfb1e00df53ea
7
- data.tar.gz: ff9bebca1bf46e73e8ffbfb45b3b58bc244b1c5e984506f52356183741b5351e97152ed6c5fea008b054dde63176e00d866ab342bbdb58e3c2f8739f68ffefbf
6
+ metadata.gz: 44f99dc995ba155323f9f8afb067d2df829c845ddaf4a7dcb5180065b4400e63bb24ff359f7d72bb977b5fc5684290675fb12f36838bcb143f7e6b4a929dee0a
7
+ data.tar.gz: 7578c349a4e9a6f49a782d2e3003f56bedf5c4c4496000386714ec17a00339f31c44531dce1e9667eaf9494d67083854e3a349c6c0619ecdb5e6ad875536fd02
data/README.md CHANGED
@@ -95,7 +95,9 @@ end
95
95
 
96
96
  ### Integrating with a Rack application
97
97
 
98
- Map the `RapidRack::Authenticator` app to a path in your application:
98
+ Map the `RapidRack::Authenticator` app to a path in your application. The
99
+ strongly suggested default of `/auth` will result in a callback URL ending in
100
+ `/auth/jwt`, which is given to Rapid Connect during registration:
99
101
 
100
102
  ```ruby
101
103
  Rack::Builder.new do
@@ -152,7 +154,9 @@ module MyApplication
152
154
  end
153
155
  ```
154
156
 
155
- Mount the `RapidRack::Engine` engine in your Rails app. In `config/routes.rb`:
157
+ Mount the `RapidRack::Engine` engine in your Rails app. The strongly suggested
158
+ default of `/auth` will result in a callback URL ending in `/auth/jwt`, which is
159
+ given to Rapid Connect during registration. In `config/routes.rb`:
156
160
 
157
161
  ```ruby
158
162
  Rails.application.routes.draw do
@@ -177,6 +181,60 @@ class WelcomeController < ApplicationController
177
181
  end
178
182
  ```
179
183
 
184
+ ### Using with Capybara-style tests
185
+
186
+ Configure `rapid_rack` to run in test mode. In a Rails application this can be
187
+ set in `config/environments/test.rb`:
188
+
189
+ ```ruby
190
+ Rails.application.configure do
191
+ # ...
192
+
193
+ config.rapid_rack.test_mode = true
194
+ end
195
+ ```
196
+
197
+ Set the JWT in your test code. In this example factory\_girl has a `jwt` factory
198
+ registered which creates a valid JWT.
199
+
200
+ ```ruby
201
+ RSpec.feature 'First visit', type: :feature do
202
+ given(:user_attrs) { attributes_for(:user) }
203
+
204
+ background do
205
+ attrs = create(:aaf_attributes, displayname: user_attrs[:name],
206
+ mail: user_attrs[:email])
207
+ RapidRack::TestAuthenticator.jwt = create(:jwt, aaf_attributes: attrs)
208
+ end
209
+
210
+ # ...
211
+ end
212
+ ```
213
+
214
+ Once this is in place, your example will be presented with a plain page with a
215
+ 'Login' button when it is required to log in. The button will submit the form
216
+ to the `/auth/jwt` endpoint, which works as normal and will invoke your
217
+ receiver.
218
+
219
+ ```ruby
220
+ RSpec.feature 'First visit', type: :feature do
221
+ # ... code from above ...
222
+
223
+ scenario 'initial login' do
224
+ visit '/'
225
+ click_button 'Sign in via AAF'
226
+
227
+ # At this point, your Capybara test is sitting in the TestAuthenticator page
228
+ # which has a 'Login' button and no other content.
229
+ expect(current_path).to eq('/auth/login')
230
+ click_button 'Login'
231
+
232
+ expect(current_path).to match(%r{/users/\d+})
233
+ expect(page).to have_content("Logged in as: #{user_attrs[:name]}")
234
+ end
235
+ end
236
+ ```
237
+
180
238
  ## Contributing
181
239
 
182
240
  Refer to [GitHub Flow](https://guides.github.com/introduction/flow/) for
@@ -2,7 +2,9 @@ module RapidRack
2
2
  end
3
3
 
4
4
  require 'rapid_rack/version'
5
+ require 'rapid_rack/with_claims'
5
6
  require 'rapid_rack/authenticator'
7
+ require 'rapid_rack/test_authenticator'
6
8
  require 'rapid_rack/default_receiver'
7
9
  require 'rapid_rack/redis_registry'
8
10
  require 'rapid_rack/engine' if defined?(Rails)
@@ -3,6 +3,11 @@ require 'rack/utils'
3
3
 
4
4
  module RapidRack
5
5
  class Authenticator
6
+ attr_reader :issuer, :audience, :secret, :error_handler
7
+ private :issuer, :audience, :secret, :error_handler
8
+
9
+ include WithClaims
10
+
6
11
  def initialize(opts)
7
12
  @url = opts[:url]
8
13
  @receiver = opts[:receiver].try(:constantize)
@@ -32,9 +37,6 @@ module RapidRack
32
37
 
33
38
  private
34
39
 
35
- InvalidClaim = Class.new(StandardError)
36
- private_constant :InvalidClaim
37
-
38
40
  DISPATCH = {
39
41
  '/login' => :initiate,
40
42
  '/jwt' => :callback,
@@ -63,49 +65,6 @@ module RapidRack
63
65
  receiver.logout(env)
64
66
  end
65
67
 
66
- def with_claims(env, assertion)
67
- claims = JSON::JWT.decode(assertion, @secret)
68
- validate_claims(claims)
69
- yield claims
70
- rescue JSON::JWT::Exception => e
71
- @error_handler.handle(env, e)
72
- rescue InvalidClaim => e
73
- @error_handler.handle(env, e)
74
- end
75
-
76
- def validate_claims(claims)
77
- reject_claim_if(claims, 'aud') { |v| v != @audience }
78
- reject_claim_if(claims, 'iss') { |v| v != @issuer }
79
- reject_claim_if(claims, 'typ') { |v| v != 'authnresponse' }
80
- reject_claim_if(claims, 'jti', &method(:replayed?))
81
- reject_claim_if(claims, 'nbf', &:zero?)
82
- reject_claim_if(claims, 'nbf', &method(:future?))
83
- reject_claim_if(claims, 'exp', &method(:expired?))
84
- reject_claim_if(claims, 'iat', &method(:skewed?))
85
- end
86
-
87
- def replayed?(jti)
88
- !receiver.register_jti(jti)
89
- end
90
-
91
- def skewed?(iat)
92
- (iat - Time.now.to_i).abs > 60
93
- end
94
-
95
- def expired?(exp)
96
- Time.at(exp) < Time.now
97
- end
98
-
99
- def future?(nbf)
100
- Time.at(nbf) > Time.now
101
- end
102
-
103
- def reject_claim_if(claims, key)
104
- val = claims[key]
105
- fail(InvalidClaim, "nil #{key}") unless val
106
- fail(InvalidClaim, "bad #{key}: #{val}") if yield(val)
107
- end
108
-
109
68
  def method?(env, method)
110
69
  env['REQUEST_METHOD'] == method
111
70
  end
@@ -27,6 +27,7 @@ module RapidRack
27
27
 
28
28
  def authenticator
29
29
  return 'RapidRack::MockAuthenticator' if configuration[:development_mode]
30
+ return 'RapidRack::TestAuthenticator' if configuration[:test_mode]
30
31
  'RapidRack::Authenticator'
31
32
  end
32
33
  end
@@ -0,0 +1,27 @@
1
+ module RapidRack
2
+ class TestAuthenticator < Authenticator
3
+ class <<self
4
+ attr_accessor :jwt
5
+ end
6
+
7
+ def call(env)
8
+ return login if env['PATH_INFO'] == '/login'
9
+ super
10
+ end
11
+
12
+ private
13
+
14
+ def login
15
+ jwt = TestAuthenticator.jwt || fail('No login JWT was set')
16
+ out = [] << <<-EOF
17
+ <html><body>
18
+ <form action="/auth/jwt" method="post">
19
+ <input type="hidden" name="assertion" value="#{jwt}"/>
20
+ <button type="submit">Login</button>
21
+ </form>
22
+ </body></html>
23
+ EOF
24
+ [200, {}, out]
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
1
  module RapidRack
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -0,0 +1,63 @@
1
+ module RapidRack
2
+ module WithClaims
3
+ def with_claims(env, assertion)
4
+ claims = JSON::JWT.decode(assertion, secret)
5
+ validate_claims(claims)
6
+ yield claims
7
+ rescue JSON::JWT::Exception => e
8
+ error_handler.handle(env, e)
9
+ rescue InvalidClaim => e
10
+ error_handler.handle(env, e)
11
+ end
12
+
13
+ private
14
+
15
+ InvalidClaim = Class.new(StandardError)
16
+ private_constant :InvalidClaim
17
+
18
+ def validate_claims(claims)
19
+ validate_aud(claims)
20
+ validate_iss(claims)
21
+ validate_typ(claims)
22
+ validate_jti(claims)
23
+ validate_nbf(claims)
24
+ validate_exp(claims)
25
+ validate_iat(claims)
26
+ end
27
+
28
+ def validate_jti(claims)
29
+ reject_claim_if(claims, 'jti') { |jti| !receiver.register_jti(jti) }
30
+ end
31
+
32
+ def validate_iat(claims)
33
+ reject_claim_if(claims, 'iat') { |iat| (iat - Time.now.to_i).abs > 60 }
34
+ end
35
+
36
+ def validate_exp(claims)
37
+ reject_claim_if(claims, 'exp') { |exp| Time.at(exp) < Time.now }
38
+ end
39
+
40
+ def validate_nbf(claims)
41
+ reject_claim_if(claims, 'nbf', &:zero?)
42
+ reject_claim_if(claims, 'nbf') { |nbf| Time.at(nbf) > Time.now }
43
+ end
44
+
45
+ def validate_typ(claims)
46
+ reject_claim_if(claims, 'typ') { |v| v != 'authnresponse' }
47
+ end
48
+
49
+ def validate_iss(claims)
50
+ reject_claim_if(claims, 'iss') { |v| v != issuer }
51
+ end
52
+
53
+ def validate_aud(claims)
54
+ reject_claim_if(claims, 'aud') { |v| v != audience }
55
+ end
56
+
57
+ def reject_claim_if(claims, key)
58
+ val = claims[key]
59
+ fail(InvalidClaim, "nil #{key}") unless val
60
+ fail(InvalidClaim, "bad #{key}: #{val}") if yield(val)
61
+ end
62
+ end
63
+ end
@@ -88,5 +88,29 @@ module RapidRack
88
88
  expect(last_request.session[:subject_id]).to eq(TestSubject.last.id)
89
89
  end
90
90
  end
91
+
92
+ context '#authenticator' do
93
+ before do
94
+ expect_any_instance_of(RapidRack::Engine)
95
+ .to receive(:configuration).at_least(:once).and_return(configuration)
96
+ end
97
+
98
+ subject { RapidRack::Engine.authenticator }
99
+
100
+ context 'in development mode' do
101
+ let(:configuration) { { development_mode: true } }
102
+ it { is_expected.to eq('RapidRack::MockAuthenticator') }
103
+ end
104
+
105
+ context 'in test mode' do
106
+ let(:configuration) { { test_mode: true } }
107
+ it { is_expected.to eq('RapidRack::TestAuthenticator') }
108
+ end
109
+
110
+ context 'with no mode' do
111
+ let(:configuration) { {} }
112
+ it { is_expected.to eq('RapidRack::Authenticator') }
113
+ end
114
+ end
91
115
  end
92
116
  end
@@ -0,0 +1,68 @@
1
+ require 'rack/lobster'
2
+
3
+ module RapidRack
4
+ RSpec.describe TestAuthenticator, type: :feature do
5
+ def build_app(prefix)
6
+ opts = { receiver: receiver, secret: secret,
7
+ issuer: issuer, audience: audience }
8
+ Rack::Builder.new do
9
+ use Rack::Lint
10
+ map(prefix) { run TestAuthenticator.new(opts) }
11
+ run Rack::Lobster.new
12
+ end
13
+ end
14
+
15
+ let(:prefix) { '/auth' }
16
+ let(:issuer) { 'https://rapid.example.com' }
17
+ let(:audience) { 'https://service.example.com' }
18
+ let(:secret) { '1234abcd' }
19
+ let(:app) { build_app(prefix) }
20
+ let(:receiver) do
21
+ build_class {}
22
+ end
23
+
24
+ subject { last_response }
25
+
26
+ context 'get /login' do
27
+ def run
28
+ get '/auth/login'
29
+ end
30
+
31
+ context 'with a JWT' do
32
+ around do |example|
33
+ begin
34
+ TestAuthenticator.jwt = 'the jwt'
35
+ example.run
36
+ ensure
37
+ TestAuthenticator.jwt = nil
38
+ end
39
+ end
40
+
41
+ before { run }
42
+ it { is_expected.to be_successful }
43
+
44
+ context 'login form' do
45
+ subject { Capybara.string(last_response.body) }
46
+
47
+ it { is_expected.to have_xpath("//form[@action='/auth/jwt']") }
48
+ it { is_expected.to have_xpath("//form/input[@value='the jwt']") }
49
+ end
50
+ end
51
+
52
+ context 'with no JWT' do
53
+ before { TestAuthenticator.jwt = nil }
54
+
55
+ it 'raises an error' do
56
+ expect { run }.to raise_error('No login JWT was set')
57
+ end
58
+ end
59
+ end
60
+
61
+ context 'post /jwt' do
62
+ it 'passes through to the parent' do
63
+ post '/auth/jwt', assertion: 'x.y.z'
64
+ expect(subject).to be_bad_request
65
+ end
66
+ end
67
+ end
68
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rapid-rack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shaun Mangelsdorf
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-04 00:00:00.000000000 Z
11
+ date: 2014-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json-jwt
@@ -244,7 +244,9 @@ files:
244
244
  - lib/rapid_rack/default_receiver.rb
245
245
  - lib/rapid_rack/engine.rb
246
246
  - lib/rapid_rack/redis_registry.rb
247
+ - lib/rapid_rack/test_authenticator.rb
247
248
  - lib/rapid_rack/version.rb
249
+ - lib/rapid_rack/with_claims.rb
248
250
  - rapid-rack.gemspec
249
251
  - spec/dummy/app/models/test_subject.rb
250
252
  - spec/dummy/config.ru
@@ -262,6 +264,7 @@ files:
262
264
  - spec/lib/rapid_rack/default_receiver_spec.rb
263
265
  - spec/lib/rapid_rack/engine_spec.rb
264
266
  - spec/lib/rapid_rack/redis_registry_spec.rb
267
+ - spec/lib/rapid_rack/test_authenticator_spec.rb
265
268
  - spec/spec_helper.rb
266
269
  - spec/support/authenticator_examples.rb
267
270
  - spec/support/temporary_test_class.rb
@@ -306,6 +309,7 @@ test_files:
306
309
  - spec/lib/rapid_rack/default_receiver_spec.rb
307
310
  - spec/lib/rapid_rack/engine_spec.rb
308
311
  - spec/lib/rapid_rack/redis_registry_spec.rb
312
+ - spec/lib/rapid_rack/test_authenticator_spec.rb
309
313
  - spec/spec_helper.rb
310
314
  - spec/support/authenticator_examples.rb
311
315
  - spec/support/temporary_test_class.rb