rapid-rack 0.1.0 → 0.2.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
  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