rack-api-key 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/techwhizbang/rack-api-key.png)](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
|