Skip to content

Commit 0c639b7

Browse files
author
Michael Bleigh
committed
Added OAuth 2.0 middleware (only for accessing protected resources at this point)
1 parent 0fe17a5 commit 0c639b7

File tree

9 files changed

+207
-6
lines changed

9 files changed

+207
-6
lines changed

.rspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
--color
22
--format=nested
3+
--backtrace

lib/grape.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@
44
require 'grape/middleware/base'
55
require 'grape/middleware/prefixer'
66
require 'grape/middleware/versioner'
7-
require 'grape/middleware/formatter'
7+
require 'grape/middleware/formatter'
8+
require 'grape/middleware/error'
9+
10+
require 'grape/middleware/auth/oauth2'
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module Grape::Middleware::Auth
2+
class OAuth2 < Grape::Middleware::Base
3+
def default_options
4+
{
5+
:token_class => 'AccessToken',
6+
:realm => 'OAuth API'
7+
}
8+
end
9+
10+
def before
11+
if request['oauth_token']
12+
verify_token(request['oauth_token'])
13+
elsif env['Authorization'] && t = parse_authorization_header
14+
verify_token(t)
15+
end
16+
end
17+
18+
def token_class
19+
@klass ||= eval(options[:token_class])
20+
end
21+
22+
def verify_token(token)
23+
if token = token_class.verify(token)
24+
if token.expired?
25+
error_out(401, 'expired_token')
26+
else
27+
if token.permission_for?(env)
28+
env['api.token'] = token
29+
else
30+
error_out(403, 'insufficient_scope')
31+
end
32+
end
33+
else
34+
error_out(401, 'invalid_token')
35+
end
36+
end
37+
38+
def parse_authorization_header
39+
if env['Authorization'] =~ /oauth (.*)/i
40+
$1
41+
end
42+
end
43+
44+
def error_out(status, error)
45+
throw :error, {
46+
:message => 'The token provided has expired.',
47+
:status => status,
48+
:headers => {
49+
'WWW-Authenticate' => "OAuth realm='#{options[:realm]}', error='#{error}'"
50+
}
51+
}
52+
end
53+
end
54+
end
55+

lib/grape/middleware/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def before; end
2525
def after; end
2626

2727
def request
28-
Rack::Request.new(env)
28+
Rack::Request.new(self.env)
2929
end
3030

3131
def response

lib/grape/middleware/error.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,18 @@
33
module Grape
44
module Middleware
55
class Error < Base
6+
def call!(env)
7+
@env = env
8+
err = catch :error do
9+
@app.call(@env)
10+
end
11+
12+
error_response(err)
13+
end
614

15+
def error_response(error = {})
16+
Rack::Response.new([(error[:message] || options[:default_message])], error[:status] || 403, error[:headers] || {}).finish
17+
end
718
end
819
end
920
end

lib/grape/middleware/formatter.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ def before
2828
if content_types.key?(fmt)
2929
env['api.format'] = fmt
3030
else
31-
env['api.error.status'] = 406
32-
env['api.error.message'] = 'The requested format is not supported.'
31+
throw :error, :status => 406, :message => 'The requested format is not supported.'
3332
end
3433
end
3534

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
require 'spec_helper'
2+
3+
describe Grape::Middleware::Auth::OAuth2 do
4+
class FakeToken
5+
def self.verify(token)
6+
FakeToken.new(token) if %w(g e).include?(token[0..0])
7+
end
8+
9+
def initialize(token)
10+
self.token = token
11+
end
12+
13+
def expired?
14+
self.token[0..0] == 'e'
15+
end
16+
17+
def permission_for?(env)
18+
env['PATH_INFO'] == '/forbidden' ? false : true
19+
end
20+
21+
attr_accessor :token
22+
end
23+
24+
def app
25+
Rack::Builder.app do
26+
use Grape::Middleware::Auth::OAuth2, :token_class => 'FakeToken'
27+
run lambda{|env| [200, {}, [ (env['api.token'].token rescue '') ]]}
28+
end
29+
end
30+
31+
context 'with the token in the query string' do
32+
context 'and a valid token' do
33+
before { get '/awesome?oauth_token=g123' }
34+
35+
it 'should set env["api.token"]' do
36+
last_response.body.should == 'g123'
37+
end
38+
end
39+
40+
context 'and an invalid token' do
41+
before do
42+
@err = catch :error do
43+
get '/awesome?oauth_token=b123'
44+
end
45+
end
46+
47+
it 'should throw an error' do
48+
@err[:status].should == 401
49+
end
50+
51+
it 'should set the WWW-Authenticate header in the response' do
52+
@err[:headers]['WWW-Authenticate'].should == "OAuth realm='OAuth API', error='invalid_token'"
53+
end
54+
end
55+
end
56+
57+
context 'with an expired token' do
58+
before do
59+
@err = catch :error do
60+
get '/awesome?oauth_token=e123'
61+
end
62+
end
63+
64+
it { @err[:status].should == 401 }
65+
it { @err[:headers]['WWW-Authenticate'].should == "OAuth realm='OAuth API', error='expired_token'" }
66+
end
67+
68+
context 'with the token in the header' do
69+
before { get '/awesome', {}, 'Authorization' => 'OAuth g123' }
70+
it { last_response.body.should == 'g123' }
71+
end
72+
73+
context 'with the token in the POST body' do
74+
before { post '/awesome', {'oauth_token' => 'g123'} }
75+
it { last_response.body.should == 'g123'}
76+
end
77+
78+
context 'when accessing something outside its scope' do
79+
before do
80+
@err = catch :error do
81+
get '/forbidden?oauth_token=g123'
82+
end
83+
end
84+
85+
it { @err[:headers]['WWW-Authenticate'].should == "OAuth realm='OAuth API', error='insufficient_scope'" }
86+
it { @err[:status].should == 403 }
87+
end
88+
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,49 @@
11
require 'spec_helper'
22

33
describe Grape::Middleware::Error do
4+
class ErrApp
5+
class << self
6+
attr_accessor :error
7+
attr_accessor :format
8+
9+
def call(env)
10+
throw :error, self.error
11+
end
12+
end
13+
end
414

15+
def app
16+
Rack::Builder.app do
17+
use Grape::Middleware::Error, :default_message => 'Aww, hamburgers.'
18+
run ErrApp
19+
end
20+
end
21+
22+
it 'should set the status code appropriately' do
23+
ErrApp.error = {:status => 410}
24+
get '/'
25+
last_response.status.should == 410
26+
end
27+
28+
it 'should set the error message appropriately' do
29+
ErrApp.error = {:message => 'Awesome stuff.'}
30+
get '/'
31+
last_response.body.should == 'Awesome stuff.'
32+
end
33+
34+
it 'should default to a 403 status' do
35+
ErrApp.error = {}
36+
get '/'
37+
last_response.status.should == 403
38+
end
39+
40+
it 'should have a default message' do
41+
ErrApp.error = {}
42+
get '/'
43+
last_response.body.should == 'Aww, hamburgers.'
44+
end
45+
46+
context 'with formatting' do
47+
48+
end
549
end

spec/grape/middleware/formatter_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
end
2929

3030
it 'should throw an error on an unrecognized format' do
31-
subject.call({'PATH_INFO' => '/info.barklar'})
32-
subject.env['api.error.status'].should == 406
31+
err = catch(:error){ subject.call({'PATH_INFO' => '/info.barklar'}) }
32+
err.should == {:status => 406, :message => "The requested format is not supported."}
3333
end
3434
end
3535
end

0 commit comments

Comments
 (0)