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 +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
|