rack-api-key 0.0.1 → 0.0.2
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.
- data/README.md +97 -16
- data/lib/rack-api-key/version.rb +1 -1
- data/lib/rack-api-key.rb +36 -10
- data/spec/rack_api_key_spec.rb +47 -0
- metadata +4 -4
data/README.md
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
# RackApiKey
|
2
2
|
|
3
|
+
[](https://travis-ci.org/techwhizbang/rack-api-key)
|
4
|
+
|
3
5
|
RackApiKey is a middleware that relies on the client submitting requests
|
4
|
-
with a header named "X-API-KEY" storing their private API key as the value.
|
6
|
+
with a header named "X-API-KEY" storing their private API key as the value.
|
5
7
|
The middleware will then intercept the request, read the value from the named
|
6
8
|
header and call the given "proc" used for API key lookup. The API key lookup
|
7
9
|
should only return a value if there is an exact match for the value stored in
|
8
10
|
the named API key header.
|
9
|
-
If such
|
11
|
+
If such an API key exists, the middleware will pass the request onward and also
|
10
12
|
set a new value in the request representing the authenticated API key. Otherwise
|
11
13
|
the middleware will return a HTTP status of 401, and a plain text message
|
12
14
|
notifying the calling requestor that they are not authorized.
|
@@ -28,18 +30,97 @@ Or install it yourself as:
|
|
28
30
|
## Usage
|
29
31
|
|
30
32
|
```ruby
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
33
|
+
use RackApiKey, :api_key_proc => Proc.new { |val| ApiKey.find(val) },
|
34
|
+
:rack_api_key => "account.api.key",
|
35
|
+
:header_key => "HTTP_X_CUSTOM_API_HEADER"
|
36
|
+
```
|
37
|
+
|
38
|
+
### :header_key
|
39
|
+
It's important to note that internally Rack actually mutates any given headers
|
40
|
+
and prefixes them with HTTP and subsequently underscores them. For example if an
|
41
|
+
API client passed "X-API-KEY" in the header, Rack would interpret that header
|
42
|
+
as "HTTP_X_API_KEY". "HTTP_X_API_KEY" is the default header. If you want to use
|
43
|
+
a different header you can specify it in the :header_key options Hash.
|
44
|
+
|
45
|
+
### :api_key_proc
|
46
|
+
This is required, there is no default behavior, and the middleware will not work
|
47
|
+
properly unless you specify a Proc that takes one argument.
|
48
|
+
The value the Proc receives will be the value set in the API header key.
|
49
|
+
Use anything you like to determine if the header value
|
50
|
+
is valid. If the value is invalid, the Proc should return nil, otherwise return
|
51
|
+
a value that will ultimately be set in the Rack env.
|
52
|
+
|
53
|
+
### :rack_api_key
|
54
|
+
This is the key that will be set in the Rack env with the return value of the
|
55
|
+
:api_key_proc.
|
56
|
+
|
57
|
+
### :url_restriction
|
58
|
+
This is an option that can restrict the rack-api-middleware to specific URLs.
|
59
|
+
This works well when you have a mixture of API endpoints that require
|
60
|
+
authentication and some that might not. Or a combination of API endpoints and
|
61
|
+
publicly facing webpages. Perhaps you've scoped all of your API endpoints to
|
62
|
+
"/api", and the rest of the URL mappings or routes are supposed to be wide open.
|
63
|
+
|
64
|
+
|
65
|
+
### unauthorized_api_key method
|
66
|
+
This is a method that can be overridden with however you'd like to respond
|
67
|
+
when a request with an invalid or unauthorized API key is encountered. The default
|
68
|
+
behavior responds with a 401 plain/text message. I find it especially useful to
|
69
|
+
override this method and switch the response to JSON format.
|
70
|
+
|
71
|
+
### valid_api_key? method
|
72
|
+
This is another method that can be overridden if there are additional checks
|
73
|
+
and validations beyond the ones already provided. For instance the API key
|
74
|
+
may exist, but for some reason it was temporarily disabled. You could add a check
|
75
|
+
for that here.
|
76
|
+
|
77
|
+
### rack_api_key_request_setter method
|
78
|
+
The default behavior of this method will take the return value of the API key
|
79
|
+
proc and set it to the Rack env with the ke specified by :rack_api_key. You
|
80
|
+
may override this method if you prefer setting something else in the Rack env
|
81
|
+
or perhaps nothing at all.
|
82
|
+
|
83
|
+
## Examples
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# Overridden to use the default behavior plus check if the api key is enabled.
|
87
|
+
def valid_api_key?(api_header_val, api_key_lookup_val)
|
88
|
+
super && api_key_lookup_val.enabled?
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# Overridden to respond in JSON format.
|
94
|
+
def unauthorized_api_key
|
95
|
+
body_text = {"error" => "blah blah blah"}.to_json
|
96
|
+
[401, {'Content-Type' => 'application/json; charset=utf-8',
|
97
|
+
'Content-Length' => body_text.size.to_s},
|
98
|
+
[body_text]]
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
# Overridden to set the Account attached to the API key instead.
|
104
|
+
def rack_api_key_request_setter(env, api_key_lookup_val)
|
105
|
+
env[@options[:rack_api_key]] = api_key_lookup_val.account
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
Rack::Builder.new do
|
111
|
+
map '/' do
|
112
|
+
use RackApiKey,
|
113
|
+
:api_key_proc => Proc.new { |val| ApiKey.find(val) },
|
114
|
+
:url_restriction => [/api/]
|
115
|
+
run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
|
116
|
+
end
|
117
|
+
|
118
|
+
map "/all-options" do
|
119
|
+
use RackApiKey,
|
120
|
+
:api_key_proc => Proc.new { |val| ApiKey.find(val) },
|
121
|
+
:rack_api_key => "account.api.key",
|
122
|
+
:header_key => "HTTP_X_CUSTOM_API_HEADER"
|
123
|
+
run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
|
124
|
+
end
|
125
|
+
end
|
45
126
|
```
|
data/lib/rack-api-key/version.rb
CHANGED
data/lib/rack-api-key.rb
CHANGED
@@ -23,7 +23,14 @@ class RackApiKey
|
|
23
23
|
# on successful authentication. The default value is
|
24
24
|
# "rack_api_key".
|
25
25
|
# * +:header_key+ - A way to override the header's name used to store the API key.
|
26
|
-
# The
|
26
|
+
# The value given here should reflect how Rack interprets the
|
27
|
+
# header. For example if the client passes "X-API-KEY" Rack
|
28
|
+
# transforms interprets it as "HTTP_X_API_KEY". The default
|
29
|
+
# value is "HTTP_X_API_KEY".
|
30
|
+
# * +:url_restriction+ - A way to restrict specific URLs that should pass through
|
31
|
+
# the rack-api-key middleware. In order to use pass an Array of Regex patterns.
|
32
|
+
# If left unspecified all requests will pass through the rack-api-key
|
33
|
+
# middleware.
|
27
34
|
#
|
28
35
|
# ==== Example
|
29
36
|
# use RackApiKey,
|
@@ -35,21 +42,26 @@ class RackApiKey
|
|
35
42
|
default_options = {
|
36
43
|
:header_key => "HTTP_X_API_KEY",
|
37
44
|
:rack_api_key => "rack_api_key",
|
38
|
-
:api_key_proc => Proc.new { raise NotImplementedError.new("Caller must implement a way to lookup an API key.") }
|
45
|
+
:api_key_proc => Proc.new { raise NotImplementedError.new("Caller must implement a way to lookup an API key.") },
|
46
|
+
:url_restriction => []
|
39
47
|
}
|
40
48
|
@options = default_options.merge(options)
|
41
49
|
end
|
42
50
|
|
43
51
|
def call(env)
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
@
|
50
|
-
|
51
|
-
|
52
|
+
|
53
|
+
if @options[:url_restriction].nil? || @options[:url_restriction].empty?
|
54
|
+
process_request(env)
|
55
|
+
else
|
56
|
+
request = Rack::Request.new(env)
|
57
|
+
url_matches = @options[:url_restriction].select { |url_regex| request.fullpath.match(url_regex) }
|
58
|
+
unless url_matches.empty?
|
59
|
+
process_request(env)
|
60
|
+
else
|
61
|
+
@app.call(env)
|
62
|
+
end
|
52
63
|
end
|
64
|
+
|
53
65
|
end
|
54
66
|
|
55
67
|
##
|
@@ -80,4 +92,18 @@ class RackApiKey
|
|
80
92
|
!api_key_lookup_val.nil? && api_key_lookup_val != ""
|
81
93
|
end
|
82
94
|
|
95
|
+
private
|
96
|
+
|
97
|
+
def process_request(env)
|
98
|
+
api_header_val = env[@options[:header_key]]
|
99
|
+
api_key_lookup_val = @options[:api_key_proc].call(api_header_val)
|
100
|
+
|
101
|
+
if valid_api_key?(api_header_val, api_key_lookup_val)
|
102
|
+
rack_api_key_request_setter(env, api_key_lookup_val)
|
103
|
+
@app.call(env)
|
104
|
+
else
|
105
|
+
unauthorized_api_key
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
83
109
|
end
|
data/spec/rack_api_key_spec.rb
CHANGED
@@ -25,6 +25,7 @@ describe RackApiKey do
|
|
25
25
|
|
26
26
|
# simple test app for the middleware test
|
27
27
|
def app
|
28
|
+
|
28
29
|
Rack::Builder.new do
|
29
30
|
map '/' do
|
30
31
|
use RackApiKey, :api_key_proc => Proc.new { |val| ApiKey.find(val) }
|
@@ -43,6 +44,13 @@ describe RackApiKey do
|
|
43
44
|
:header_key => "HTTP_X_CUSTOM_API_HEADER"
|
44
45
|
run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
|
45
46
|
end
|
47
|
+
|
48
|
+
map "/url-restricted" do
|
49
|
+
use RackApiKey,
|
50
|
+
:api_key_proc => Proc.new { |val| ApiKey.find(val) },
|
51
|
+
:url_restriction => [/url-restricted\/foo/]
|
52
|
+
run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
|
53
|
+
end
|
46
54
|
|
47
55
|
end
|
48
56
|
end
|
@@ -114,4 +122,43 @@ describe RackApiKey do
|
|
114
122
|
end
|
115
123
|
|
116
124
|
end
|
125
|
+
|
126
|
+
context "when URL restriction is used" do
|
127
|
+
|
128
|
+
context "and the requesting URL matches the restricted list" do
|
129
|
+
|
130
|
+
it 'attempts to find the ApiKey with the header value' do
|
131
|
+
ApiKey.should_receive(:find).with("SECRET API KEY")
|
132
|
+
get "/url-restricted/foo", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'responds with a 200 upon successful authorization' do
|
136
|
+
ApiKey.stub(:find).and_return(ApiKey.new("SECRET API KEY"))
|
137
|
+
get "/url-restricted/foo", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
|
138
|
+
last_response.ok?.should be_true
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'responds with a 401 when the header is not set' do
|
142
|
+
header("HTTP_X_API_KEY", nil)
|
143
|
+
get "/url-restricted/foo"
|
144
|
+
last_response.status.should == 401
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
context "and the requesting URL is not in the restricted list" do
|
150
|
+
|
151
|
+
it 'does not attempt to find the ApiKey with the header value' do
|
152
|
+
ApiKey.should_not_receive(:find).with("SECRET API KEY")
|
153
|
+
get "/url-restricted/bar"
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'responds with a 200 without the header set' do
|
157
|
+
get "/url-restricted/bar"
|
158
|
+
last_response.ok?.should be_true
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
117
164
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-api-key
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-04-
|
12
|
+
date: 2013-04-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -107,7 +107,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
107
|
version: '0'
|
108
108
|
segments:
|
109
109
|
- 0
|
110
|
-
hash:
|
110
|
+
hash: 2860984967925191594
|
111
111
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
112
|
none: false
|
113
113
|
requirements:
|
@@ -116,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
116
|
version: '0'
|
117
117
|
segments:
|
118
118
|
- 0
|
119
|
-
hash:
|
119
|
+
hash: 2860984967925191594
|
120
120
|
requirements: []
|
121
121
|
rubyforge_project:
|
122
122
|
rubygems_version: 1.8.25
|