Skip to content

Commit 195e229

Browse files
committed
first attempt
0 parents  commit 195e229

12 files changed

+686
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.bundle
2+
vendor

Gemfile

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
source 'https://rubygems.org'
2+
3+
gemspec

Gemfile.lock

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
PATH
2+
remote: .
3+
specs:
4+
roda-auth (0.0.1)
5+
roda (~> 1.2)
6+
warden (~> 1.2)
7+
8+
GEM
9+
remote: https://rubygems.org/
10+
specs:
11+
minitest (5.5.0)
12+
rack (1.6.0)
13+
rack-test (0.6.2)
14+
rack (>= 1.0)
15+
rake (10.4.2)
16+
roda (1.2.0)
17+
rack
18+
warden (1.2.3)
19+
rack (>= 1.0)
20+
21+
PLATFORMS
22+
ruby
23+
24+
DEPENDENCIES
25+
minitest (~> 5.5)
26+
rack-test (~> 0.6)
27+
rake (~> 10)
28+
roda-auth!

README.md

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
Roda plugin for Authentication
2+
=============
3+
4+
### Quick start
5+
6+
Install gem with
7+
8+
gem 'roda-auth' #Gemfile
9+
10+
11+
Create rack app
12+
13+
```ruby
14+
#api.ru
15+
16+
require 'roda/auth'
17+
18+
class App < Roda
19+
20+
# :user_class defaults to ::User
21+
22+
plugin :auth, :form, user_class: MyUser, redirect: '/login'
23+
24+
route do |r|
25+
r.post 'login' do
26+
sign_in
27+
end
28+
r.get 'login' do
29+
#render login form
30+
end
31+
r.on 'public' do
32+
#public content
33+
end
34+
authenticate!
35+
r.on 'private' do
36+
#private content
37+
end
38+
end
39+
40+
end
41+
42+
class MyUser
43+
44+
#required - should return either a valid user or nil
45+
46+
def self.authentic?(credentials)
47+
#credentials is either {'username' => 'foo', 'password' => 'bar'} or {'token' => '123'}
48+
if token = credentials['token']
49+
self.find_by_token(token) #make sure to use a safe (constant time) method of looking up tokens
50+
else
51+
self.check_password(credentials['username'], credentials['password'])
52+
end
53+
end
54+
55+
#optional - used for generating/updating auth tokens or tracking logins
56+
57+
def authentic!
58+
#call for each successful authentication request
59+
end
60+
61+
end
62+

Rakefile

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
require 'rake/testtask'
2+
3+
Rake::TestTask.new do |t|
4+
t.libs << "test"
5+
t.pattern = "test/*_test.rb"
6+
end

lib/roda/auth.rb

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require 'roda/plugins/auth'

lib/roda/plugins/auth.rb

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
require 'base64'
2+
require 'warden'
3+
require 'roda'
4+
5+
class Roda
6+
7+
module RodaPlugins
8+
9+
module Auth
10+
11+
def self.load_dependencies(app, *args, &block)
12+
Warden::Strategies.add(:token, Strategies::Token)
13+
Warden::Strategies.add(:password, Strategies::Password)
14+
Warden::Strategies.add(:basic, Strategies::Basic)
15+
end
16+
17+
def self.configure(app, *args)
18+
options = args.last.is_a?(Hash) ? args.pop : {}
19+
user_class = options.delete(:user_class) || ::User
20+
type = args[0] || :basic
21+
redirect = options.delete(:redirect) || '/unautenticated'
22+
case type
23+
when :basic
24+
strategies = [:basic]
25+
when :form
26+
strategies = [:password]
27+
app.use Rack::Session::Cookie, secret:'foo'
28+
when :token
29+
strategies = [:token, :password]
30+
end
31+
app.use Warden::Manager do |config|
32+
config.default_scope = :user
33+
config.failure_app = self.fail(type)
34+
config[:user_class] = user_class
35+
config.scope_defaults(
36+
:user,
37+
:strategies => strategies,
38+
:action => redirect
39+
)
40+
end
41+
end
42+
43+
def self.fail(type)
44+
auth_fail = case type
45+
when :basic
46+
->(env) {[401, {"WWW-AUTHENTICATE" => "Basic: Realm=\"#{env['warden.options'][:attempted_path]}\""}, []] }
47+
when :form
48+
->(env) {[302, {"HTTP-LOCATION" => env['warden.options'][:action]} , []] }
49+
when :token
50+
->(env) {[401, {"WWW-AUTHENTICATE" => "\"Token\""}, []] }
51+
end
52+
->(env) { auth_fail.call(env) }
53+
end
54+
55+
module InstanceMethods
56+
57+
def authenticate!
58+
user = warden.authenticate!
59+
warden.set_user(user)
60+
end
61+
62+
def current_user
63+
warden.user
64+
end
65+
66+
def sign_in &block
67+
raise RodaError 'sign_in with POST only' unless request.env['REQUEST_METHOD'] == "POST"
68+
user = warden.authenticate!
69+
warden.set_user(user)
70+
request.is(&block) if block
71+
request.response.status = 201
72+
user
73+
end
74+
75+
def sign_out &block
76+
warden.set_user(nil)
77+
request.is(&block) if block
78+
request.response.status = 204
79+
end
80+
81+
private
82+
83+
def warden
84+
request.env['warden']
85+
end
86+
87+
def session_path
88+
roda_class.opts[:session_path].to_s
89+
end
90+
91+
end
92+
93+
end
94+
95+
module Strategies
96+
97+
class Base < Warden::Strategies::Base
98+
99+
def success!(u)
100+
u.authentic! if u.respond_to?(:authentic!)
101+
super
102+
end
103+
104+
def authenticate!
105+
u = warden.config[:user_class].authentic?(credentials)
106+
u.nil? ? fail!("Could not log in") : success!(u)
107+
end
108+
109+
private
110+
111+
def warden
112+
@env['warden']
113+
end
114+
115+
def credentials_from_basic
116+
header = authorization_header
117+
return unless header && header =~ /\ABasic (.*)/m
118+
username, password = Base64.decode64($1).split(/:/, 2)
119+
return unless username and password
120+
{ 'username' => username, 'password' => password }
121+
end
122+
123+
def credentials_from_form
124+
request.media_type == "application/x-www-form-urlencoded" && params
125+
end
126+
127+
def credentials_from_body
128+
request.body && JSON.parse(request.body.string)
129+
end
130+
131+
def token_from_auth_header
132+
return unless header = authorization_header
133+
match = header =~ /\AAuth (.*)/m
134+
match && { 'token' => $1 }
135+
end
136+
137+
def authorization_header
138+
@env['HTTP_AUTHORIZATION'] || @env['X-HTTP_AUTHORIZATION'] || @env['X_HTTP_AUTHORIZATION'] || @env['REDIRECT_X_HTTP_AUTHORIZATION']
139+
end
140+
141+
end
142+
143+
144+
class Password < Base
145+
146+
def valid?
147+
credentials['username'] && credentials['password']
148+
end
149+
150+
private
151+
152+
def credentials
153+
@credentials if @credentials
154+
@credentials = credentials_from_form || credentials_from_body || {}
155+
end
156+
157+
end
158+
159+
class Basic < Password
160+
161+
def valid?
162+
credentials['username'] && credentials['password']
163+
end
164+
165+
private
166+
167+
def credentials
168+
@credentials if @credentials
169+
@credentials = credentials_from_basic || {}
170+
end
171+
172+
end
173+
174+
175+
class Token < Base
176+
177+
def valid?
178+
credentials['token']
179+
end
180+
181+
private
182+
183+
def credentials
184+
@credentials if @credentials
185+
@credentials = token_from_auth_header || {}
186+
end
187+
188+
end
189+
190+
end
191+
192+
register_plugin(:auth, Auth)
193+
194+
end
195+
196+
197+
end

roda-auth.gemspec

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Gem::Specification.new do |s|
2+
s.name = 'roda-auth'
3+
s.version = '0.0.1'
4+
s.date = '2014-12-21'
5+
s.summary = "Roda authentication"
6+
s.description = "A Roda plugin for authentication with Warden"
7+
s.authors = ["Michel Benevento"]
8+
s.email = '[email protected]'
9+
s.files = ["lib/roda/auth.rb", "lib/roda/plugins/auth.rb"]
10+
s.homepage = 'http://github.com/beno/roda-auth'
11+
s.license = 'MIT'
12+
13+
s.add_runtime_dependency 'roda', '~> 1.2'
14+
s.add_runtime_dependency 'warden', '~> 1.2'
15+
16+
s.add_development_dependency 'rake', '~> 10'
17+
s.add_development_dependency 'minitest', '~> 5.5'
18+
s.add_development_dependency 'rack-test', '~> 0.6'
19+
20+
end

test/auth_basic_test.rb

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require "test_helpers"
2+
require 'json'
3+
require 'roda/auth'
4+
5+
class AuthBasicTest < Minitest::Test
6+
include Rack::Test::Methods
7+
include TestHelpers
8+
9+
10+
def setup
11+
u = User.new(valid_credentials)
12+
User.db[:users][u.username] = u
13+
14+
app :bare do |app|
15+
16+
app.plugin :auth
17+
18+
app.route do |r|
19+
r.on 'public' do
20+
'public'
21+
end
22+
authenticate!
23+
r.on 'private' do
24+
'private'
25+
end
26+
end
27+
end
28+
end
29+
30+
def test_public
31+
assert_equal 200, status('/public')
32+
end
33+
34+
def test_private_refused
35+
assert_equal 401, status('/private')
36+
assert_equal "Basic: Realm=\"/private\"", header('WWW-AUTHENTICATE', '/private')
37+
end
38+
39+
def test_private_accepted
40+
assert_equal 200, status('/private', {"HTTP_AUTHORIZATION" => "Basic #{http_auth(valid_credentials)}"})
41+
end
42+
43+
end
44+
45+
46+
class User < TestHelpers::User ; end
47+

0 commit comments

Comments
 (0)