prx_auth 1.1.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 +7 -0
- data/.gitignore +16 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE +22 -0
- data/README.md +66 -0
- data/Rakefile +10 -0
- data/lib/prx_auth.rb +3 -0
- data/lib/prx_auth/resource_map.rb +124 -0
- data/lib/prx_auth/scope_list.rb +138 -0
- data/lib/prx_auth/version.rb +3 -0
- data/lib/rack/prx_auth.rb +63 -0
- data/lib/rack/prx_auth/certificate.rb +55 -0
- data/lib/rack/prx_auth/token_data.rb +53 -0
- data/lib/rack/prx_auth/version.rb +7 -0
- data/prx_auth.gemspec +32 -0
- data/test/prx_auth/resource_map_test.rb +158 -0
- data/test/prx_auth/scope_list_test.rb +102 -0
- data/test/rack/prx_auth/certificate_test.rb +130 -0
- data/test/rack/prx_auth/token_data_test.rb +101 -0
- data/test/rack/prx_auth_test.rb +97 -0
- data/test/test_helper.rb +10 -0
- metadata +187 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3e7866c4a21b61c3e43328c528301e6b53aa350efc5caec23f94b2c4acebea30
|
4
|
+
data.tar.gz: 4d39f067376023bfa72cdaa2059ea976d5011535e5b766fb5928478508f48e23
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 849fe18e035fe9406412cf48da28578d60ecdd5e5ea632b0577d17dcb0b2566e187452313f86d6186529b963a5771275a4968b5ed8bd342a7fea5bfafeb6d34e
|
7
|
+
data.tar.gz: 916cc3c3e08829b1bd07f638b853bc4a19b9bf68ce08ec5580df999fda8c712f50046a9e11b77ef4063df7e8bfd1ebf15457439ccc67e2c5ff5089ae9c2c2350
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Change Log
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
This project adheres to [Semantic Versioning](http://semver.org/).
|
4
|
+
|
5
|
+
## [Unreleased][unreleased]
|
6
|
+
|
7
|
+
## [0.0.7] - 2015-06-15
|
8
|
+
### Added
|
9
|
+
- This Changelog
|
10
|
+
- TokenData#authorized? method
|
11
|
+
|
12
|
+
## [0.0.6] - 2015-04-22
|
13
|
+
### Removed
|
14
|
+
- Railtie
|
15
|
+
### Fixed
|
16
|
+
- Endless loop while fetching afer certificate cache expires
|
17
|
+
|
18
|
+
[unreleased]: https://github.com/PRX/rack-prx_auth/compare/v0.0.7...HEAD
|
19
|
+
[0.0.7]: https://github.com/PRX/rack-prx_auth/compare/v0.0.6...v0.0.7
|
20
|
+
[0.0.6]: https://github.com/PRX/rack-prx_auth/compare/v0.0.5...v0.0.6
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
guard :minitest, all_after_pass: true do
|
2
|
+
watch(%r{^test/(.*)\/?test_(.*)\.rb})
|
3
|
+
watch(%r{^lib/(.*/)?([^/]+)\.rb}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
|
4
|
+
watch(%r{^lib/(.+)\.rb}) { |m| "test/#{m[1]}_test.rb" }
|
5
|
+
watch(%r{^lib/(.+)\.rb}) { |m| "test/#{m[1]}_test.rb" }
|
6
|
+
watch(%r{^test/.+_test\.rb})
|
7
|
+
watch(%r{^test/test_helper\.rb}) { 'test' }
|
8
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 PRX, Eve Asher
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Rack::PrxAuth
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/rack-prx_auth)
|
4
|
+
[](https://gemnasium.com/PRX/rack-prx_auth)
|
5
|
+
[](https://travis-ci.org/PRX/rack-prx_auth)
|
6
|
+
[](https://codeclimate.com/github/PRX/rack-prx_auth)
|
7
|
+
[](https://coveralls.io/r/PRX/rack-prx_auth)
|
8
|
+
|
9
|
+
This gem adds middleware to a Rack application that decodes and verified a JSON Web Token (JWT) issued by PRX.org. If the JWT is invalid, the middleware will respond with a 401 Unauthorized. If the JWT was not issued by PRX (or the specified issuer), the request will continue through the middleware stack.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'rack-prx_auth'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install rack-prx_auth
|
26
|
+
|
27
|
+
In a non-Rails app, add the following to the application's config.ru file:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
use Rack::PrxAuth, cert_location: [CERT LOCATION], issuer: [ISSUER]
|
31
|
+
```
|
32
|
+
The `cert_location` and `issuer` parameters are optional. See below.
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
### The Request
|
37
|
+
|
38
|
+
Rack-prx_auth looks for a token in the request's HTTP_AUTHORIZATION header. It expects that the header's content will take the form of 'Bearer <your token>'. If no HTTP_AUTHORIZATION header is present, rack-prx_auth passes the request to the next middleware.
|
39
|
+
|
40
|
+
We have another application that's in charge of making the token. It's called id.prx.org. Its job is to show a form for a user to enter credentials, validate those credentials, and then generate a JWT using PRX's private key. See http://openid.net/specs/openid-connect-implicit-1_0.html to find out what information is encoded in a JWT. Basically it's a hash containing, among other things, the user's ID, the issuer of the token, and when the token expires.
|
41
|
+
|
42
|
+
### Configuration
|
43
|
+
|
44
|
+
Rack-prx_auth takes two optional parameters, `issuer` and `cert_location`. See Installation for how to specify them.
|
45
|
+
|
46
|
+
By default, rack-prx_auth will assume that you want to make sure the JWT was issued by PRX. After decoding the JWT, rack-prx_auth checks the `issuer` field to make sure it's id.prx.org. If you want it to check for a different issuer, pass `issuer: <your issuer>` as a parameter.
|
47
|
+
|
48
|
+
Since the JWT was created using PRX's private key, rack-prx_auth needs to fetch PRX's public key to decode it. It does this by accessing the `cert_location` (default is https://id.prx.org/api/v1/certs), generating an OpenSSL::X509::Certificate based on its contents, and determining the public key from the certificate object. Should you wish to get your public key from a different certificate, you may specify a different endpoint by passing `cert_location: <your cert location>` as a parameter. Keep in mind that unless the certificate matches the private key used to make the JWT, rack-prx_auth will return 401.
|
49
|
+
|
50
|
+
### The Response
|
51
|
+
|
52
|
+
If the token isn't valid, meaning it's expired or it wasn't created using our private key, rack-prx_auth will return 401 Unauthorized.
|
53
|
+
|
54
|
+
If there's nothing in the HTTP_AUTHORIZATION heading, there's something but JSON::JWT can't decode it, or the issuer field doesn't specify the correct issuer, rack-prx_auth just punts to the next piece of middleware.
|
55
|
+
|
56
|
+
If all goes well, rack-prx_auth takes the decoded JWT and makes a TokenData object. Then it adds this object to the `env` with the key 'prx.auth'.
|
57
|
+
|
58
|
+
If you are using rack-prx_auth in a Rails app, you'll have a few handy controller methods available to you. Calling `prx_auth_token` within a controller returns the TokenData object, and `prx_authenticated?` tells you whether a TokenData object is available. Also, if you call `user_id` on the TokenData object, you get the user's ID so you can ask id.prx.org for information about them.
|
59
|
+
|
60
|
+
## Contributing
|
61
|
+
|
62
|
+
1. Fork it ( https://github.com/PRX/rack-prx_auth/fork )
|
63
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
64
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
65
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
66
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/lib/prx_auth.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
module PrxAuth
|
2
|
+
class ResourceMap
|
3
|
+
WILDCARD_KEY = '*'
|
4
|
+
|
5
|
+
def initialize(mapped_values)
|
6
|
+
input = mapped_values.clone
|
7
|
+
@wildcard = ScopeList.new(input.delete(WILDCARD_KEY)||'')
|
8
|
+
@map = Hash[input.map do |(key, values)|
|
9
|
+
[key, ScopeList.new(values)]
|
10
|
+
end]
|
11
|
+
end
|
12
|
+
|
13
|
+
def contains?(resource, namespace=nil, scope=nil)
|
14
|
+
resource = resource.to_s
|
15
|
+
|
16
|
+
if resource == WILDCARD_KEY
|
17
|
+
raise ArgumentError if namespace.nil?
|
18
|
+
|
19
|
+
@wildcard.contains?(namespace, scope)
|
20
|
+
else
|
21
|
+
mapped_resource = @map[resource]
|
22
|
+
|
23
|
+
if mapped_resource && !namespace.nil?
|
24
|
+
mapped_resource.contains?(namespace, scope) || @wildcard.contains?(namespace, scope)
|
25
|
+
elsif !namespace.nil?
|
26
|
+
@wildcard.contains?(namespace, scope)
|
27
|
+
else
|
28
|
+
!!mapped_resource
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def condense
|
34
|
+
condensed_wildcard = @wildcard.condense
|
35
|
+
condensed_map = Hash[@map.map do |resource, list|
|
36
|
+
[resource, (list - condensed_wildcard).condense]
|
37
|
+
end]
|
38
|
+
ResourceMap.new(condensed_map.merge(WILDCARD_KEY => condensed_wildcard))
|
39
|
+
end
|
40
|
+
|
41
|
+
def +(other_map)
|
42
|
+
result = {}
|
43
|
+
(resources + other_map.resources + [WILDCARD_KEY]).uniq.each do |resource|
|
44
|
+
list_a = list_for_resource(resource)
|
45
|
+
list_b = other_map.list_for_resource(resource)
|
46
|
+
result[resource] = if list_a.nil?
|
47
|
+
list_b
|
48
|
+
elsif list_b.nil?
|
49
|
+
list_a
|
50
|
+
else
|
51
|
+
list_a + list_b
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
ResourceMap.new(result).condense
|
56
|
+
end
|
57
|
+
|
58
|
+
def -(other_map)
|
59
|
+
result = {}
|
60
|
+
other_wildcard = other_map.list_for_resource(WILDCARD_KEY) || PrxAuth::ScopeList.new('')
|
61
|
+
|
62
|
+
resources.each do |resource|
|
63
|
+
result[resource] = list_for_resource(resource) - (other_wildcard + other_map.list_for_resource(resource))
|
64
|
+
end
|
65
|
+
|
66
|
+
if @wildcard
|
67
|
+
result[WILDCARD_KEY] = @wildcard - other_wildcard
|
68
|
+
end
|
69
|
+
|
70
|
+
ResourceMap.new(result)
|
71
|
+
end
|
72
|
+
|
73
|
+
def &(other_map)
|
74
|
+
result = {}
|
75
|
+
other_wildcard = other_map.list_for_resource(WILDCARD_KEY)
|
76
|
+
|
77
|
+
(resources + other_map.resources).uniq.each do |res|
|
78
|
+
left = list_for_resource(res)
|
79
|
+
right = other_map.list_for_resource(res)
|
80
|
+
|
81
|
+
result[res] = if left.nil?
|
82
|
+
right & @wildcard
|
83
|
+
elsif right.nil?
|
84
|
+
left & other_wildcard
|
85
|
+
else
|
86
|
+
(left + @wildcard) & (right + other_wildcard)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
if @wildcard
|
91
|
+
result[WILDCARD_KEY] = @wildcard - (@wildcard - other_wildcard)
|
92
|
+
end
|
93
|
+
|
94
|
+
ResourceMap.new(result).condense
|
95
|
+
end
|
96
|
+
|
97
|
+
def as_json(opts={})
|
98
|
+
@map.merge(WILDCARD_KEY => @wildcard).as_json(opts)
|
99
|
+
end
|
100
|
+
|
101
|
+
def freeze
|
102
|
+
@map.freeze
|
103
|
+
@wildcard.freeze
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
def resources(namespace=nil, scope=nil)
|
108
|
+
if namespace.nil?
|
109
|
+
@map.keys
|
110
|
+
else
|
111
|
+
@map.select do |name, list|
|
112
|
+
list.contains?(namespace, scope) || @wildcard.contains?(namespace, scope)
|
113
|
+
end.map(&:first)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
protected
|
118
|
+
|
119
|
+
def list_for_resource(resource)
|
120
|
+
return @wildcard if resource.to_s == WILDCARD_KEY
|
121
|
+
@map[resource.to_s]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module PrxAuth
|
2
|
+
class ScopeList
|
3
|
+
SCOPE_SEPARATOR = ' '
|
4
|
+
NAMESPACE_SEPARATOR = ':'
|
5
|
+
NO_NAMESPACE = :_
|
6
|
+
|
7
|
+
def self.new(list)
|
8
|
+
case list
|
9
|
+
when PrxAuth::ScopeList then list
|
10
|
+
else super(list)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(list)
|
15
|
+
@string = list
|
16
|
+
end
|
17
|
+
|
18
|
+
def contains?(namespace, scope=nil)
|
19
|
+
scope, namespace = namespace, NO_NAMESPACE if scope.nil?
|
20
|
+
|
21
|
+
if namespace == NO_NAMESPACE
|
22
|
+
map[namespace].include?(symbolize(scope))
|
23
|
+
else
|
24
|
+
symbolized_scope = symbolize(scope)
|
25
|
+
map[symbolize(namespace)].include?(symbolized_scope) || map[NO_NAMESPACE].include?(symbolized_scope)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def freeze
|
30
|
+
@string.freeze
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
@string
|
36
|
+
end
|
37
|
+
|
38
|
+
def condense
|
39
|
+
tripped = false
|
40
|
+
result = map[NO_NAMESPACE].clone
|
41
|
+
namespaces = map.keys - [NO_NAMESPACE]
|
42
|
+
|
43
|
+
namespaces.each do |ns|
|
44
|
+
map[ns].each do |scope|
|
45
|
+
if !contains?(NO_NAMESPACE, scope)
|
46
|
+
result << scope_string(ns, scope)
|
47
|
+
else
|
48
|
+
tripped = true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if tripped
|
54
|
+
ScopeList.new(result.join(SCOPE_SEPARATOR))
|
55
|
+
else
|
56
|
+
self
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def as_json(opts=())
|
61
|
+
to_s.as_json(opts)
|
62
|
+
end
|
63
|
+
|
64
|
+
def -(other_scope_list)
|
65
|
+
return self if other_scope_list.nil?
|
66
|
+
|
67
|
+
tripped = false
|
68
|
+
result = []
|
69
|
+
|
70
|
+
map.each do |namespace, scopes|
|
71
|
+
scopes.each do |scope|
|
72
|
+
if other_scope_list.contains?(namespace, scope)
|
73
|
+
tripped = true
|
74
|
+
else
|
75
|
+
result << scope_string(namespace, scope)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
if tripped
|
81
|
+
ScopeList.new(result.join(SCOPE_SEPARATOR))
|
82
|
+
else
|
83
|
+
self
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def +(other_list)
|
88
|
+
return self if other_list.nil?
|
89
|
+
|
90
|
+
ScopeList.new([to_s, other_list.to_s].join(SCOPE_SEPARATOR)).condense
|
91
|
+
end
|
92
|
+
|
93
|
+
def &(other_list)
|
94
|
+
return ScopeList.new('') if other_list.nil?
|
95
|
+
|
96
|
+
self - (self - other_list)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def map
|
102
|
+
@parsed_map ||= empty_map.tap do |map|
|
103
|
+
@string.split(SCOPE_SEPARATOR).each do |value|
|
104
|
+
next if value.length < 1
|
105
|
+
|
106
|
+
parts = value.split(NAMESPACE_SEPARATOR, 2)
|
107
|
+
if parts.length == 2
|
108
|
+
map[symbolize(parts[0])] << symbolize(parts[1])
|
109
|
+
else
|
110
|
+
map[NO_NAMESPACE] << symbolize(parts[0])
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def scope_string(ns, scope)
|
117
|
+
if ns == NO_NAMESPACE
|
118
|
+
scope.to_s
|
119
|
+
else
|
120
|
+
[ns, scope].join(NAMESPACE_SEPARATOR)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def empty_map
|
125
|
+
@empty_map ||= Hash.new do |hash, key|
|
126
|
+
hash[key] = []
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def symbolize(value)
|
131
|
+
case value
|
132
|
+
when Symbol then value
|
133
|
+
when String then value.downcase.gsub('-', '_').intern
|
134
|
+
else symbolize value.to_s
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|