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 +4 -4
- data/README.md +60 -2
- data/lib/rapid_rack.rb +2 -0
- data/lib/rapid_rack/authenticator.rb +5 -46
- data/lib/rapid_rack/engine.rb +1 -0
- data/lib/rapid_rack/test_authenticator.rb +27 -0
- data/lib/rapid_rack/version.rb +1 -1
- data/lib/rapid_rack/with_claims.rb +63 -0
- data/spec/lib/rapid_rack/engine_spec.rb +24 -0
- data/spec/lib/rapid_rack/test_authenticator_spec.rb +68 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a7a45733ec5f445a2b7cca550518cfcf6758508
|
4
|
+
data.tar.gz: 71743e8d43c0f32a356f502dbea161231aae1f73
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
data/lib/rapid_rack.rb
CHANGED
@@ -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
|
data/lib/rapid_rack/engine.rb
CHANGED
@@ -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
|
data/lib/rapid_rack/version.rb
CHANGED
@@ -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.
|
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-
|
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
|