prx_auth 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module PrxAuth
2
+ VERSION = "1.1.0"
3
+ end
@@ -0,0 +1,63 @@
1
+ require 'json/jwt'
2
+ require 'rack/prx_auth/certificate'
3
+ require 'rack/prx_auth/token_data'
4
+ require 'prx_auth'
5
+
6
+ module Rack
7
+ class PrxAuth
8
+ INVALID_TOKEN = [
9
+ 401, {'Content-Type' => 'application/json'},
10
+ [{status: 401, error: 'Invalid JSON Web Token'}.to_json]
11
+ ]
12
+
13
+ DEFAULT_ISS = 'id.prx.org'
14
+
15
+ attr_reader :issuer
16
+
17
+ def initialize(app, options = {})
18
+ @app = app
19
+ @certificate = Certificate.new(options[:cert_location])
20
+ @issuer = options[:issuer] || DEFAULT_ISS
21
+ end
22
+
23
+ def call(env)
24
+ return @app.call(env) unless env['HTTP_AUTHORIZATION']
25
+
26
+ token = env['HTTP_AUTHORIZATION'].split[1]
27
+ claims = decode_token(token)
28
+
29
+ return @app.call(env) unless should_validate_token?(claims)
30
+
31
+ if valid?(claims, token)
32
+ env['prx.auth'] = TokenData.new(claims)
33
+ @app.call(env)
34
+ else
35
+ INVALID_TOKEN
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def valid?(claims, token)
42
+ !expired?(claims) && @certificate.valid?(token)
43
+ end
44
+
45
+ def decode_token(token)
46
+ return {} if token.nil?
47
+
48
+ begin
49
+ JSON::JWT.decode(token, :skip_verification)
50
+ rescue JSON::JWT::InvalidFormat
51
+ {}
52
+ end
53
+ end
54
+
55
+ def expired?(claims)
56
+ Time.now.to_i > (claims['iat'] + claims['exp'])
57
+ end
58
+
59
+ def should_validate_token?(claims)
60
+ claims['iss'] == @issuer
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,55 @@
1
+ require 'json/jwt'
2
+ require 'net/http'
3
+
4
+ module Rack
5
+ class PrxAuth
6
+ class Certificate
7
+ EXPIRES_IN = 43200
8
+ DEFAULT_CERT_LOC = URI('https://id.prx.org/api/v1/certs')
9
+
10
+ attr_reader :cert_location
11
+
12
+ def initialize(cert_uri = nil)
13
+ @cert_location = cert_uri.nil? ? DEFAULT_CERT_LOC : URI(cert_uri)
14
+ end
15
+
16
+ def valid?(token)
17
+ begin
18
+ JSON::JWT.decode(token, public_key)
19
+ rescue JSON::JWT::VerificationFailed
20
+ false
21
+ else
22
+ true
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def public_key
29
+ certificate.public_key
30
+ end
31
+
32
+ def certificate
33
+ if @certificate.nil? || needs_refresh?
34
+ @certificate = fetch
35
+ end
36
+ @certificate
37
+ end
38
+
39
+ def fetch
40
+ certs = JSON.parse(Net::HTTP.get(cert_location))
41
+ cert_string = certs['certificates'].values[0]
42
+ @refresh_at = Time.now.to_i + EXPIRES_IN
43
+ OpenSSL::X509::Certificate.new(cert_string)
44
+ end
45
+
46
+ def needs_refresh?
47
+ expired? || @refresh_at <= Time.now.to_i
48
+ end
49
+
50
+ def expired?
51
+ @certificate.not_after < Time.now
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,53 @@
1
+ require 'prx_auth/resource_map'
2
+
3
+ module Rack
4
+ class PrxAuth
5
+ class TokenData
6
+ attr_reader :scopes
7
+
8
+ def initialize(attrs = {})
9
+ @attributes = attrs
10
+
11
+ @authorized_resources = ::PrxAuth::ResourceMap.new(unpack_aur(attrs['aur'])).freeze
12
+
13
+ if attrs['scope']
14
+ @scopes = attrs['scope'].split(' ').freeze
15
+ else
16
+ @scopes = [].freeze
17
+ end
18
+ end
19
+
20
+ def resources(namespace=nil, scope=nil)
21
+ @authorized_resources.resources(namespace, scope)
22
+ end
23
+
24
+ def user_id
25
+ @attributes['sub']
26
+ end
27
+
28
+ def authorized?(resource, namespace=nil, scope=nil)
29
+ @authorized_resources.contains?(resource, namespace, scope)
30
+ end
31
+
32
+ def globally_authorized?(namespace, scope=nil)
33
+ authorized?(::PrxAuth::ResourceMap::WILDCARD_KEY, namespace, scope)
34
+ end
35
+
36
+ private
37
+
38
+ def unpack_aur(aur)
39
+ return {} if aur.nil?
40
+
41
+ aur.clone.tap do |result|
42
+ unless result['$'].nil?
43
+ result.delete('$').each do |role, resources|
44
+ resources.each do |res|
45
+ result[res.to_s] = role
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,7 @@
1
+ require 'prx_auth/version'
2
+
3
+ module Rack
4
+ class PrxAuth
5
+ VERSION = PrxAuth::VERSION
6
+ end
7
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'prx_auth/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "prx_auth"
8
+ spec.version = PrxAuth::VERSION
9
+ spec.authors = ["Eve Asher", "Chris Rhoden"]
10
+ spec.email = ["eve@prx.org", "carhoden@gmail.com"]
11
+ spec.summary = %q{Utilites for parsing PRX JWTs and Rack middleware that verifies and attaches the token's claims to env.}
12
+ spec.description = %q{Specific to PRX. Will ignore tokens that were not issued by PRX.}
13
+ spec.homepage = "https://github.com/PRX/prx_auth"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^test/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = '>= 2.3'
22
+
23
+ spec.add_development_dependency 'bundler', '~> 2.0'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'coveralls', '~> 0'
26
+ spec.add_development_dependency 'guard'
27
+ spec.add_development_dependency 'guard-minitest'
28
+
29
+ spec.add_dependency 'rack', '>= 1.5.2'
30
+ spec.add_dependency 'json', '>= 1.8.1'
31
+ spec.add_dependency 'json-jwt', '~> 1.9.4'
32
+ end
@@ -0,0 +1,158 @@
1
+ require 'test_helper'
2
+
3
+ describe PrxAuth::ResourceMap do
4
+
5
+ def new_map(val)
6
+ PrxAuth::ResourceMap.new(val)
7
+ end
8
+
9
+ let(:map) { PrxAuth::ResourceMap.new(input) }
10
+ let(:input) { {'123' => 'admin one two three ns1:namespaced', '456' => 'member four five six' } }
11
+
12
+ describe '#authorized?' do
13
+ it 'contains scopes in list' do
14
+ assert map.contains?(123, :admin)
15
+ end
16
+
17
+ it 'does not include across aur limits' do
18
+ assert !map.contains?(123, :member)
19
+ end
20
+
21
+ it 'does not require a scope' do
22
+ assert map.contains?(123)
23
+ end
24
+
25
+ it 'does not match if it hasnt seen the resource' do
26
+ assert !map.contains?(789)
27
+ end
28
+
29
+ it 'works with namespaced scopes' do
30
+ assert map.contains?(123, :ns1, :namespaced)
31
+ end
32
+
33
+ describe 'with wildcard resource' do
34
+ let(:input) do
35
+ {
36
+ '*' => 'peek',
37
+ '123' => 'admin one two three',
38
+ '456' => 'member four five six'
39
+ }
40
+ end
41
+
42
+ it 'applies wildcard lists to queries with no matching value' do
43
+ assert map.contains?(789, :peek)
44
+ end
45
+
46
+ it 'does not scan unscoped for wildcard resources' do
47
+ assert !map.contains?(789)
48
+ end
49
+
50
+ it 'allows querying by wildcard resource directly' do
51
+ assert map.contains?('*', :peek)
52
+ assert !map.contains?('*', :admin)
53
+ end
54
+
55
+ it 'treats wildcard lists as additive to other explicit ones' do
56
+ assert map.contains?(123, :peek)
57
+ end
58
+
59
+ it 'refuses to run against wildcard with no scope' do
60
+ assert_raises ArgumentError do
61
+ map.contains?('*')
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ describe '#resources' do
68
+ let (:input) do
69
+ {
70
+ '*' => 'read wildcard',
71
+ '123' => 'read write buy',
72
+ '456' => 'read ns1:buy'
73
+ }
74
+ end
75
+
76
+ let (:resources) { map.resources }
77
+
78
+ it 'returns resource ids' do
79
+ assert resources.include?('123')
80
+ assert resources.include?('456')
81
+ end
82
+
83
+ it 'excludes wildcard values' do
84
+ assert !resources.include?('*')
85
+ end
86
+
87
+ it 'filters for scope' do
88
+ resources = map.resources(:write)
89
+ assert resources.include?('123')
90
+ assert !resources.include?('456')
91
+ assert !resources.include?('*')
92
+ end
93
+
94
+ it 'works with namespaces' do
95
+ resources = map.resources(:ns1, :buy)
96
+ assert resources.include?('123')
97
+ assert resources.include?('456')
98
+
99
+ resources = map.resources(:buy)
100
+ assert !resources.include?('456')
101
+ end
102
+ end
103
+
104
+ describe '#condense' do
105
+ let (:input) {{ "one" => "one two three ns1:one", "two" => "two three", "three" => "two", "*" => "two" }}
106
+ let (:json) { map.condense.as_json }
107
+
108
+ it "removes redundant values which are in the wildcard" do
109
+ assert !json["one"].include?("two")
110
+ end
111
+
112
+ it "keeps resources in the hash even if all scopes are redundant" do
113
+ assert json["three"] == ""
114
+ end
115
+ end
116
+
117
+ describe '#+' do
118
+ it 'adds values' do
119
+ map = new_map("one" => "two", "two" => "four") + new_map("one" => "three", "three" => "six")
120
+ assert map.contains?('one', :two) && map.contains?('one', :three)
121
+ assert map.contains?('two', :four) && map.contains?('three', :six)
122
+ end
123
+ end
124
+
125
+ describe '#-' do
126
+ it 'subtracts values' do
127
+ map = new_map("one" => "two three", "two" => "four") - new_map("one" => "three four")
128
+ assert map.contains?('one', :two)
129
+ assert map.contains?('two', :four)
130
+ assert !map.contains?('one', :three) && !map.contains?('one', :four)
131
+ end
132
+
133
+ it 'works on wildcards on right side of operator' do
134
+ map = new_map("one" => "two three") - new_map("*" => "two")
135
+ assert !map.contains?("one", :two)
136
+ end
137
+ end
138
+
139
+ describe '#&' do
140
+ it 'computes the intersection' do
141
+ map = (
142
+ new_map("one" => "two three", "four" => "five six", "five" => "five") &
143
+ new_map("one" => "three four", "four" => "six seven", "six" => "six")
144
+ )
145
+ assert map.contains?("one", :three) && map.contains?("four", :six)
146
+ assert !map.contains?("one", :two) && !map.contains?("four", :five)
147
+ assert !map.contains?("one", :four) && !map.contains?("four", :seven)
148
+ assert !map.contains?("five", :five) && !map.contains?("six", :six)
149
+ end
150
+
151
+ it 'works with wildcards' do
152
+ map = new_map("*" => "three wild", "one" => "four two" ) & new_map("*" => "two wild", "two" => "three four")
153
+ assert map.contains?("two", :three) && map.contains?("one", :two)
154
+ assert !map.contains?("one", :four) && !map.contains?("two", :four)
155
+ assert map.contains?("*", :wild)
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,102 @@
1
+ require 'test_helper'
2
+
3
+ describe PrxAuth::ScopeList do
4
+
5
+ def new_list(val)
6
+ PrxAuth::ScopeList.new(val)
7
+ end
8
+
9
+ let (:scopes) { 'read write sell top-up' }
10
+ let (:list) { PrxAuth::ScopeList.new(scopes) }
11
+
12
+ it 'looks up successfully for a given scope' do
13
+ assert list.contains?('write')
14
+ end
15
+
16
+ it 'scans for symbols' do
17
+ assert list.contains?(:read)
18
+ end
19
+
20
+ it 'handles hyphen to underscore conversions' do
21
+ assert list.contains?(:top_up)
22
+ end
23
+
24
+ it 'fails for contents not in the list' do
25
+ assert !list.contains?(:buy)
26
+ end
27
+
28
+ describe 'with namespace' do
29
+ let (:scopes) { 'ns1:hello ns2:goodbye aloha 1:23' }
30
+
31
+ it 'works for namespaced lookups' do
32
+ assert list.contains?(:ns1, :hello)
33
+ end
34
+
35
+ it 'fails when the wrong namespace is passed' do
36
+ assert !list.contains?(:ns1, :goodbye)
37
+ end
38
+
39
+ it 'looks up global scopes when namespaced fails' do
40
+ assert list.contains?(:ns1, :aloha)
41
+ assert list.contains?(:ns3, :aloha)
42
+ end
43
+
44
+ it 'works with non-symbol namespaces' do
45
+ assert list.contains?(1, 23)
46
+ end
47
+ end
48
+
49
+ describe '#condense' do
50
+ let (:scopes) { "ns1:foo foo ns1:bar" }
51
+ it 'removes redundant scopes based on namespace wildcards' do
52
+ assert list.condense.to_s == "foo ns1:bar"
53
+ end
54
+ end
55
+
56
+ describe '#-' do
57
+ it 'subtracts scopes' do
58
+ sl = new_list('one two') - new_list('two')
59
+ assert sl.kind_of? PrxAuth::ScopeList
60
+ assert !sl.contains?(:two)
61
+ assert sl.contains?(:one)
62
+ end
63
+
64
+ it 'works with scope wildcards' do
65
+ sl = new_list('ns1:one ns2:two') - new_list('one')
66
+ assert !sl.contains?(:ns1, :one)
67
+ end
68
+
69
+ it 'accepts nil' do
70
+ sl = new_list('one two') - nil
71
+ assert sl.contains?(:one) && sl.contains?(:two)
72
+ end
73
+ end
74
+
75
+ describe '#+' do
76
+ it 'adds scopes' do
77
+ sl = new_list('one') + new_list('two')
78
+ assert sl.kind_of? PrxAuth::ScopeList
79
+ assert sl.contains?(:one)
80
+ assert sl.contains?(:two)
81
+ end
82
+
83
+ it 'accepts nil' do
84
+ sl = new_list('one two') + nil
85
+ assert sl.contains?(:one) && sl.contains?(:two)
86
+ end
87
+ end
88
+
89
+ describe '#&' do
90
+ it 'gets the intersect of scopes' do
91
+ sl = (new_list('one two three four') & new_list('two four six'))
92
+ assert sl.kind_of? PrxAuth::ScopeList
93
+ assert sl.contains?(:two) && sl.contains?(:four)
94
+ assert !sl.contains?(:one) && !sl.contains?(:three) && !sl.contains?(:six)
95
+ end
96
+
97
+ it 'accepts nil' do
98
+ sl = new_list('one') & nil
99
+ assert !sl.contains?(:one)
100
+ end
101
+ end
102
+ end