prx_auth 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/rack-prx_auth.svg)](http://badge.fury.io/rb/rack-prx_auth)
|
4
|
+
[![Dependency Status](https://gemnasium.com/PRX/rack-prx_auth.svg)](https://gemnasium.com/PRX/rack-prx_auth)
|
5
|
+
[![Build Status](https://travis-ci.org/PRX/rack-prx_auth.svg?branch=master)](https://travis-ci.org/PRX/rack-prx_auth)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/PRX/rack-prx_auth/badges/gpa.svg)](https://codeclimate.com/github/PRX/rack-prx_auth)
|
7
|
+
[![Coverage Status](https://coveralls.io/repos/PRX/rack-prx_auth/badge.svg)](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
|