rack-cerberus 0.3.1 → 1.0.0
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.
- checksums.yaml +7 -0
- data/.gitignore +6 -1
- data/.rspec +4 -0
- data/Gemfile +5 -0
- data/{MIT_LICENCE → MIT_LICENSE} +2 -2
- data/README.md +69 -55
- data/example/config.ru +35 -0
- data/lib/rack/cerberus.rb +133 -0
- data/{cerberus.gemspec → rack_cerberus.gemspec} +15 -6
- data/spec/rack_cerberus_spec.rb +133 -0
- data/spec/spec_helper.rb +24 -0
- metadata +51 -55
- data/cerberus.rb +0 -121
- data/example.css +0 -1
- data/example.ru +0 -35
- data/spec.rb +0 -84
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: afcdada63ffd1a684458910d59b89d15fa216886
|
4
|
+
data.tar.gz: d412afa1080dfcc3fe423ed035c359c5caaff232
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 76d1daf65b65cfc21b741b33e9874d69a4f259c464d92906897fb80cddf9d491358288e5820317843ef64b9567335f5dc7ee11a7ccaf584cb09524e90f8368c7
|
7
|
+
data.tar.gz: ede819c5610004dca634ec0e4e2a5fe035feef2b9e55ae667b16fd5039b4cb4d792f33734b749122f3a2714fa435deb5c65e10173c588496c347ff0dc0b2b905
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/Gemfile
ADDED
data/{MIT_LICENCE → MIT_LICENSE}
RENAMED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c) 2010-
|
1
|
+
Copyright (c) 2010-2015 Mickael Riga
|
2
2
|
|
3
3
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
16
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
17
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
18
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
-
THE SOFTWARE.
|
19
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,93 +1,107 @@
|
|
1
|
-
|
1
|
+
Rack::Cerberus
|
2
|
+
==============
|
2
3
|
|
3
|
-
Cerberus
|
4
|
-
|
5
|
-
|
6
|
-
Cerberus is a Rack middleware for form-based authentication. Its purpose is only
|
7
|
-
to offer a nicer (or more actual) replacement for Basic HTTP authentication.
|
4
|
+
`Rack::Cerberus` is a Rack middleware for form-based authentication.
|
5
|
+
It works roughly like Basic HTTP authentication except that you can use
|
6
|
+
options in order to style the authentication page.
|
8
7
|
|
9
8
|
Install with:
|
10
9
|
|
11
|
-
|
10
|
+
```
|
11
|
+
# sudo gem install rack-cerberus
|
12
|
+
```
|
13
|
+
|
14
|
+
Or in your `Gemfile`:
|
15
|
+
|
16
|
+
```
|
17
|
+
gem 'rack-cerberus'
|
18
|
+
```
|
12
19
|
|
13
20
|
You can use it almost the same way you use `Rack::Auth::Basic`:
|
14
21
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
22
|
+
```
|
23
|
+
require 'rack/cerberus'
|
24
|
+
use Rack::Session::Cookie, secret: 'change_me'
|
25
|
+
use Rack::Cerberus do |login, pass|
|
26
|
+
pass=='secret'
|
27
|
+
end
|
28
|
+
```
|
20
29
|
|
21
|
-
Like in that example, make sure you have a session, because
|
22
|
-
persistent login.
|
30
|
+
Like in that example, make sure you have a session, because
|
31
|
+
`Rack::Cerberus` uses it for persistent login, and make sure it is encrypted.
|
23
32
|
|
33
|
+
Options
|
34
|
+
-------
|
35
|
+
|
24
36
|
There is an optional hash you can add for customisation it. Options are:
|
25
37
|
|
26
38
|
- `:company_name`
|
27
|
-
- `:
|
28
|
-
- `:
|
39
|
+
- `:bg_color` (Background color)
|
40
|
+
- `:fg_color` (Actually the color of the box color)
|
29
41
|
- `:text_color`
|
30
|
-
- `:icon_url` (
|
31
|
-
- `:css_location`
|
42
|
+
- `:icon_url` (For a company logo or any icon)
|
43
|
+
- `:css_location` (Path to a CSS file for a complete reskin)
|
44
|
+
- `:session_key` (Where login name is kept. Default is `cerberus_user`)
|
32
45
|
|
33
46
|
Which is used that way:
|
34
47
|
|
35
|
-
|
36
|
-
|
37
|
-
|
48
|
+
```
|
49
|
+
use Rack::Cerberus, {company_name: 'Nintendo'} do |login, pass|
|
50
|
+
pass=='secret'
|
51
|
+
end
|
52
|
+
```
|
38
53
|
|
39
|
-
The purpose of Cerberus is to be basic, which is why there are
|
40
|
-
a page fairly customized with colors and
|
41
|
-
|
54
|
+
The purpose of `Rack::Cerberus` is to be basic, which is why there are
|
55
|
+
enough options to have a page fairly customized with colors and
|
56
|
+
logo (`:icon_url`). The logo can even replace the company name if
|
57
|
+
you leave `:company_name` blank. But should you be fussy, this is possible
|
42
58
|
to have more control using an external CSS file with the option `:css_location`.
|
43
59
|
|
44
|
-
|
45
|
-
|
60
|
+
Authentication
|
61
|
+
--------------
|
46
62
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
It's gonna start the example at http://localhost:9292
|
63
|
+
Just like `Rack::Auth::Basic`, `Rack::Cerberus` yields login and pass,
|
64
|
+
and delegate authentication to the block you send it which should
|
65
|
+
return `true` or `false`.
|
52
66
|
|
53
67
|
You can also use the 3rd argument which is the request object:
|
54
68
|
|
55
|
-
|
69
|
+
```
|
70
|
+
use Rack::Cerberus, {company_name: 'Nintendo'} do |login, pass, req|
|
56
71
|
pass=='secret' && req.xhr?
|
57
72
|
end
|
73
|
+
```
|
58
74
|
|
59
|
-
This is
|
60
|
-
Like the referer or another parameter.
|
61
|
-
|
75
|
+
This is useful if you want to check other details of the request.
|
76
|
+
Like the referer or another parameter. But bear in mind that `cerberus_login`
|
77
|
+
and `cerberus_pass` are still mandatory.
|
78
|
+
|
79
|
+
Example
|
80
|
+
-------
|
81
|
+
|
82
|
+
If you want to see a concrete example, go into the `example/` directory and run:
|
83
|
+
|
84
|
+
```
|
85
|
+
# rackup
|
86
|
+
```
|
87
|
+
|
88
|
+
It's gonna start the example at `http://localhost:9292`
|
62
89
|
|
63
90
|
Logout
|
64
91
|
------
|
65
92
|
|
66
|
-
Any request to `/logout` on the path where the middleware is mounted
|
67
|
-
In other words, if you put the middleware at `/admin`,
|
68
|
-
logged out. Pretty simple.
|
93
|
+
Any request to `/logout` on the path where the middleware is mounted
|
94
|
+
will log you out. In other words, if you put the middleware at `/admin`,
|
95
|
+
query `/admin/logout` to be logged out. Pretty simple.
|
69
96
|
|
70
97
|
Help
|
71
98
|
----
|
72
99
|
|
73
|
-
If you want to help me, don't hesitate to fork that project on Github
|
74
|
-
|
75
|
-
Changelog
|
76
|
-
---------
|
77
|
-
|
78
|
-
0.0.1 Changed Everything somehow
|
79
|
-
0.1.0 Make it possible to authenticate through GET request (for restful APIs)
|
80
|
-
0.1.1 Documentation improvement
|
81
|
-
0.1.2 Raise message when using without session
|
82
|
-
0.1.3 Don't go to page /logout when signing in after a logout (redirect to / instead)
|
83
|
-
0.1.4 Fix /logout redirect so that it works with mapping
|
84
|
-
0.1.5 Fix CSS and Javascript for IE (Yes I'm too kind)
|
85
|
-
0.1.6 Send an Array instead of a string to Rack so that it works on Ruby 1.9
|
86
|
-
0.2.0 External CSS file + `:text_color` option + keep details after login failure
|
87
|
-
0.3.0 Now sends request as a 3rd argument to the block
|
88
|
-
0.3.1 Escape HTML in fields now that they are kept
|
100
|
+
If you want to help me, don't hesitate to fork that project on Github
|
101
|
+
or send patches.
|
89
102
|
|
90
103
|
Copyright
|
91
104
|
---------
|
92
105
|
|
93
|
-
(c) 2010-
|
106
|
+
(c) 2010-2015 Mickael Riga - see MIT_LICENSE for details
|
107
|
+
|
data/example/config.ru
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative '../lib/rack/cerberus'
|
2
|
+
|
3
|
+
use Rack::Session::Cookie, secret: 'change_me'
|
4
|
+
|
5
|
+
map '/' do
|
6
|
+
run lambda {|env|
|
7
|
+
body = <<-EOB.strip
|
8
|
+
<html>
|
9
|
+
<head>
|
10
|
+
<title>Rack::Cerberus</title>
|
11
|
+
</head>
|
12
|
+
<body>This page is public, so you can see it. But what happens if you want to see a <a href='/secret'>Secret Page</a>? Nevertheless, I can give you access:<br /><br />
|
13
|
+
Login: <b>mario</b><br />Pass: <b>bros</b>
|
14
|
+
</body>
|
15
|
+
</html>
|
16
|
+
EOB
|
17
|
+
[200, {'Content-Type' => 'text/html'}, [body]]
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
map '/secret' do
|
22
|
+
use Rack::Cerberus, {
|
23
|
+
company_name: 'Nintendo',
|
24
|
+
fg_color: 'red',
|
25
|
+
} do |login,pass|
|
26
|
+
[login,pass]==['mario','bros']
|
27
|
+
end
|
28
|
+
run lambda {|env|
|
29
|
+
[
|
30
|
+
200, {'Content-Type' => 'text/plain'},
|
31
|
+
['Welcome back Mario. Your Credit Card number is: 9292']
|
32
|
+
]
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
|
5
|
+
class Cerberus
|
6
|
+
|
7
|
+
VERSION = '1.0.0'
|
8
|
+
|
9
|
+
class NoSessionError < RuntimeError; end
|
10
|
+
|
11
|
+
def initialize(app, options={}, &block)
|
12
|
+
@app = app
|
13
|
+
defaults = {
|
14
|
+
company_name: 'Cerberus',
|
15
|
+
bg_color: '#999',
|
16
|
+
fg_color: '#CCC',
|
17
|
+
text_color: '#FFF',
|
18
|
+
icon_url: nil,
|
19
|
+
session_key: 'cerberus_user'
|
20
|
+
}
|
21
|
+
@options = defaults.merge(options)
|
22
|
+
@options[:icon] = @options[:icon_url].nil? ? '' : "<img src='#{@options[:icon_url]}' /><br />"
|
23
|
+
@options[:css] = @options[:css_location].nil? ? '' : "<link href='#{@options[:css_location]}' rel='stylesheet' type='text/css'>"
|
24
|
+
@block = block
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(env)
|
28
|
+
dup._call(env)
|
29
|
+
end
|
30
|
+
|
31
|
+
def _call(env)
|
32
|
+
raise(NoSessionError, 'Cerberus cannot work without Session') if env['rack.session'].nil?
|
33
|
+
req = Rack::Request.new(env)
|
34
|
+
login = req['cerberus_login']
|
35
|
+
pass = req['cerberus_pass']
|
36
|
+
err = req.post? ? "<p class='err'>Wrong login or password</p>" : ''
|
37
|
+
if ((env['rack.session'][@options[:session_key]]!=nil && env['PATH_INFO']!='/logout') || (login && pass && @block.call(login, pass, req)))
|
38
|
+
env['rack.session'][@options[:session_key]] ||= login
|
39
|
+
if env['PATH_INFO']=='/logout'
|
40
|
+
res = Rack::Response.new(env)
|
41
|
+
res.redirect(env['SCRIPT_NAME']=='' ? '/' : env['SCRIPT_NAME'])
|
42
|
+
res.finish
|
43
|
+
else
|
44
|
+
@app.call(env)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
env['rack.session'].delete(@options[:session_key])
|
48
|
+
[
|
49
|
+
401, {'Content-Type' => 'text/html'},
|
50
|
+
[AUTH_PAGE % @options.merge({
|
51
|
+
error: err, submit_path: env['REQUEST_URI'],
|
52
|
+
login: Rack::Utils.escape_html(login),
|
53
|
+
pass: Rack::Utils.escape_html(pass)
|
54
|
+
})]
|
55
|
+
]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
AUTH_PAGE = <<-PAGE
|
60
|
+
<!DOCTYPE html>
|
61
|
+
<html><head>
|
62
|
+
<title>%{company_name} Authentication</title>
|
63
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
64
|
+
<style type='text/css'>
|
65
|
+
* {
|
66
|
+
-moz-box-sizing: border-box;
|
67
|
+
-ms-box-sizing: border-box;
|
68
|
+
box-sizing: border-box;
|
69
|
+
}
|
70
|
+
body { background-color: %{bg_color}; font-family: sans-serif; text-align: center; margin: 0px; }
|
71
|
+
h1, p { color: %{text_color}; }
|
72
|
+
.err {
|
73
|
+
padding: 1em;
|
74
|
+
border-radius: 3px;
|
75
|
+
-moz-border-radius: 3px;
|
76
|
+
-webkit-border-radius: 3px;
|
77
|
+
color: white;
|
78
|
+
background-color: red;
|
79
|
+
}
|
80
|
+
div {
|
81
|
+
text-align: left;
|
82
|
+
width: 500px;
|
83
|
+
margin: 0px auto;
|
84
|
+
padding: 2em;
|
85
|
+
-webkit-border-bottom-left-radius: 3px;
|
86
|
+
-moz-border-radius-bottomleft: 3px;
|
87
|
+
border-bottom-left-radius: 3px;
|
88
|
+
-webkit-border-bottom-right-radius: 3px;
|
89
|
+
-moz-border-radius-bottomright: 3px;
|
90
|
+
border-bottom-right-radius: 3px;
|
91
|
+
-moz-box-shadow: 0px 0px 5px #333;
|
92
|
+
-webkit-box-shadow: 0px 0px 5px #555;
|
93
|
+
box-shadow: 0px 0px 5px #555;
|
94
|
+
background-color: %{fg_color}; }
|
95
|
+
input[type=text], input[type=password] {
|
96
|
+
display: block; width: 100%%; padding: 0.5em;
|
97
|
+
border: 0px; font-size: 1.25em;
|
98
|
+
}
|
99
|
+
</style>
|
100
|
+
%{css}
|
101
|
+
</head><body>
|
102
|
+
<div>
|
103
|
+
<h1>%{company_name}</h1>
|
104
|
+
%{icon}
|
105
|
+
%{error}
|
106
|
+
<p>Please Sign In</p>
|
107
|
+
<form action="%{submit_path}" method="post" accept-charset="utf-8">
|
108
|
+
<input type="text" name="cerberus_login" value="%{login}" id='login' title='Login' placeholder='Login'><br />
|
109
|
+
<input type="password" name="cerberus_pass" value="%{pass}" id='pass' title='Password' placeholder='Password'>
|
110
|
+
<p><input type="submit" value="SIGN IN →"></p>
|
111
|
+
</form>
|
112
|
+
<script type="text/javascript" charset="utf-8">
|
113
|
+
var login = document.getElementById('login');
|
114
|
+
var pass = document.getElementById('pass');
|
115
|
+
var focus = function() {
|
116
|
+
if (this.value==this.id) this.value = '';
|
117
|
+
}
|
118
|
+
var blur = function() {
|
119
|
+
if (this.value=='') this.value = this.id;
|
120
|
+
}
|
121
|
+
login.onfocus = focus;
|
122
|
+
pass.onfocus = focus;
|
123
|
+
login.onblur = blur;
|
124
|
+
pass.onblur = blur;
|
125
|
+
</script>
|
126
|
+
</div>
|
127
|
+
</body></html>
|
128
|
+
PAGE
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
@@ -1,13 +1,22 @@
|
|
1
|
+
require_relative './lib/rack/cerberus'
|
2
|
+
|
1
3
|
Gem::Specification.new do |s|
|
4
|
+
|
2
5
|
s.name = 'rack-cerberus'
|
3
|
-
s.version =
|
4
|
-
s.platform = Gem::Platform::RUBY
|
6
|
+
s.version = Rack::Cerberus::VERSION
|
5
7
|
s.summary = "A Rack middleware for form-based authentication"
|
6
|
-
s.description = "A Rack middleware for form-based authentication.
|
8
|
+
s.description = "A Rack middleware for form-based authentication. It works roughly like Basic HTTP Authentication except that the authentication page can be styled with the middleware options."
|
9
|
+
s.licenses = ['MIT']
|
10
|
+
|
7
11
|
s.files = `git ls-files`.split("\n").sort
|
8
|
-
s.
|
9
|
-
s.
|
12
|
+
s.require_path = './lib'
|
13
|
+
s.add_dependency('rack')
|
14
|
+
s.test_files = s.files.select { |p| p =~ /^spec\/.*_spec.rb/ }
|
15
|
+
s.platform = Gem::Platform::RUBY
|
16
|
+
|
10
17
|
s.author = "Mickael Riga"
|
11
18
|
s.email = "mig@mypeplum.com"
|
12
19
|
s.homepage = "http://github.com/mig-hub/cerberus"
|
13
|
-
|
20
|
+
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'rack/cerberus'
|
2
|
+
|
3
|
+
RSpec.describe Rack::Cerberus do
|
4
|
+
|
5
|
+
let(:secret_app) {
|
6
|
+
lambda {|env|
|
7
|
+
[200, {'Content-Type'=>'text/plain'}, env['rack.session'].inspect]
|
8
|
+
}
|
9
|
+
}
|
10
|
+
|
11
|
+
let(:cerberus_app) {
|
12
|
+
Rack::Cerberus.new(secret_app, cerberus_options) do |login,pass|
|
13
|
+
[login,pass]==['mario@nintendo.com','bros']
|
14
|
+
end
|
15
|
+
}
|
16
|
+
|
17
|
+
let(:app) {
|
18
|
+
Rack::URLMap.new({
|
19
|
+
mount_path => Rack::Session::Cookie.new(cerberus_app, {secret: '42'})
|
20
|
+
})
|
21
|
+
}
|
22
|
+
|
23
|
+
let(:cerberus_options) { {} }
|
24
|
+
let(:mount_path) { '/' }
|
25
|
+
|
26
|
+
before :each do
|
27
|
+
clear_cookies
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'No session is set' do
|
31
|
+
let(:app) { cerberus_app }
|
32
|
+
it 'Raises' do
|
33
|
+
expect{ get('/') }.to raise_error(Rack::Cerberus::NoSessionError)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'Not logged in' do
|
38
|
+
it 'Stops requests' do
|
39
|
+
get '/'
|
40
|
+
expect(last_response.status).to eq 401
|
41
|
+
body = last_response.body
|
42
|
+
expect(body.class).to eq String
|
43
|
+
expect(body).to match(/name="cerberus_login" value=""/)
|
44
|
+
expect(body).to match(/name="cerberus_pass" value=""/)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'Logging in' do
|
49
|
+
|
50
|
+
context 'Login details are incorrect' do
|
51
|
+
it 'Stops requests' do
|
52
|
+
post('/', {'cerberus_login' => 'fake_login', 'cerberus_pass' => 'fake_pass'})
|
53
|
+
expect(last_response.status).to eq 401
|
54
|
+
expect(last_response.body).to match(/Wrong login or password/)
|
55
|
+
end
|
56
|
+
it 'Keeps what was entered in the fields' do
|
57
|
+
post('/', {'cerberus_login' => 'fake_login', 'cerberus_pass' => 'fake_pass'})
|
58
|
+
expect(last_response.body).to match(/name="cerberus_login" value="fake_login"/)
|
59
|
+
expect(last_response.body).to match(/name="cerberus_pass" value="fake_pass"/)
|
60
|
+
end
|
61
|
+
it 'Escapes HTML on submitted info' do
|
62
|
+
expect(Rack::Utils).to receive(:escape_html).with('<script>bad</script>').twice
|
63
|
+
post('/', {'cerberus_login' => '<script>bad</script>', 'cerberus_pass' => '<script>bad</script>'})
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'Login details are correct' do
|
68
|
+
it 'Gives access' do
|
69
|
+
get('/', {'cerberus_login' => 'mario@nintendo.com', 'cerberus_pass' => 'bros'})
|
70
|
+
expect(last_response.status).to eq 200
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'Already logged in' do
|
77
|
+
|
78
|
+
it 'Uses session for persistent login' do
|
79
|
+
get('/', {'cerberus_login' => 'mario@nintendo.com', 'cerberus_pass' => 'bros'})
|
80
|
+
get('/')
|
81
|
+
expect(last_response.status).to eq 200
|
82
|
+
expect(last_response.body).to include('"cerberus_user"=>"mario@nintendo.com"}')
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
describe 'Logout' do
|
88
|
+
|
89
|
+
let(:mount_path) { '/admin' }
|
90
|
+
|
91
|
+
it 'Happens via /logout path' do
|
92
|
+
get('/admin/', {'cerberus_login' => 'mario@nintendo.com', 'cerberus_pass' => 'bros'})
|
93
|
+
expect(last_response.status).to eq 200
|
94
|
+
get('/admin/logout')
|
95
|
+
expect(last_response.status).to eq 401
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'Never redirects to the logout path' do
|
99
|
+
get('/admin/logout', {'cerberus_login' => 'mario@nintendo.com', 'cerberus_pass' => 'bros'})
|
100
|
+
expect(last_response.status).to eq 302
|
101
|
+
expect(last_response['Location']).to eq '/admin'
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
describe 'Options' do
|
107
|
+
|
108
|
+
it 'Does not link CSS by default' do
|
109
|
+
get('/')
|
110
|
+
expect(last_response.body).not_to match(/<link/)
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'CSS option is used' do
|
114
|
+
let(:cerberus_options) { {:css_location=>'/main.css'} }
|
115
|
+
it 'Links the CSS file' do
|
116
|
+
get('/')
|
117
|
+
expect(last_response.body).to match(/<link/)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'Session key is different' do
|
122
|
+
let(:cerberus_options) { {session_key: 'different_user'} }
|
123
|
+
it 'Uses the session key of the options' do
|
124
|
+
get('/', {'cerberus_login' => 'mario@nintendo.com', 'cerberus_pass' => 'bros'})
|
125
|
+
get('/')
|
126
|
+
expect(last_response.status).to eq 200
|
127
|
+
expect(last_response.body).to include('"different_user"=>"mario@nintendo.com"}')
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
|
3
|
+
RSpec.configure do |config|
|
4
|
+
|
5
|
+
config.include Rack::Test::Methods
|
6
|
+
config.expect_with :rspec do |expectations|
|
7
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
8
|
+
end
|
9
|
+
config.mock_with :rspec do |mocks|
|
10
|
+
mocks.verify_partial_doubles = true
|
11
|
+
end
|
12
|
+
config.filter_run :focus
|
13
|
+
config.run_all_when_everything_filtered = true
|
14
|
+
config.disable_monkey_patching!
|
15
|
+
config.warnings = true
|
16
|
+
if config.files_to_run.one?
|
17
|
+
config.default_formatter = 'doc'
|
18
|
+
end
|
19
|
+
config.profile_examples = 10
|
20
|
+
config.order = :random
|
21
|
+
Kernel.srand config.seed
|
22
|
+
|
23
|
+
end
|
24
|
+
|
metadata
CHANGED
@@ -1,74 +1,70 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-cerberus
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 3
|
9
|
-
- 1
|
10
|
-
version: 0.3.1
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
11
5
|
platform: ruby
|
12
|
-
authors:
|
6
|
+
authors:
|
13
7
|
- Mickael Riga
|
14
8
|
autorequire:
|
15
9
|
bindir: bin
|
16
10
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
11
|
+
date: 2015-06-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: A Rack middleware for form-based authentication. It works roughly like
|
28
|
+
Basic HTTP Authentication except that the authentication page can be styled with
|
29
|
+
the middleware options.
|
23
30
|
email: mig@mypeplum.com
|
24
31
|
executables: []
|
25
|
-
|
26
32
|
extensions: []
|
27
|
-
|
28
33
|
extra_rdoc_files: []
|
29
|
-
|
30
|
-
files:
|
34
|
+
files:
|
31
35
|
- .gitignore
|
32
|
-
-
|
36
|
+
- .rspec
|
37
|
+
- Gemfile
|
38
|
+
- MIT_LICENSE
|
33
39
|
- README.md
|
34
|
-
-
|
35
|
-
- cerberus.rb
|
36
|
-
-
|
37
|
-
-
|
38
|
-
- spec.rb
|
39
|
-
has_rdoc: true
|
40
|
+
- example/config.ru
|
41
|
+
- lib/rack/cerberus.rb
|
42
|
+
- rack_cerberus.gemspec
|
43
|
+
- spec/rack_cerberus_spec.rb
|
44
|
+
- spec/spec_helper.rb
|
40
45
|
homepage: http://github.com/mig-hub/cerberus
|
41
|
-
licenses:
|
42
|
-
|
46
|
+
licenses:
|
47
|
+
- MIT
|
48
|
+
metadata: {}
|
43
49
|
post_install_message:
|
44
50
|
rdoc_options: []
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
version:
|
57
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
-
none: false
|
59
|
-
requirements:
|
60
|
-
- - ">="
|
61
|
-
- !ruby/object:Gem::Version
|
62
|
-
hash: 3
|
63
|
-
segments:
|
64
|
-
- 0
|
65
|
-
version: "0"
|
51
|
+
require_paths:
|
52
|
+
- ./lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
66
63
|
requirements: []
|
67
|
-
|
68
64
|
rubyforge_project:
|
69
|
-
rubygems_version:
|
65
|
+
rubygems_version: 2.0.14
|
70
66
|
signing_key:
|
71
|
-
specification_version:
|
67
|
+
specification_version: 4
|
72
68
|
summary: A Rack middleware for form-based authentication
|
73
|
-
test_files:
|
74
|
-
- spec.rb
|
69
|
+
test_files:
|
70
|
+
- spec/rack_cerberus_spec.rb
|
data/cerberus.rb
DELETED
@@ -1,121 +0,0 @@
|
|
1
|
-
class Cerberus
|
2
|
-
|
3
|
-
class NoSessionError < RuntimeError; end
|
4
|
-
|
5
|
-
AUTH_PAGE = <<-PAGE
|
6
|
-
<!DOCTYPE html>
|
7
|
-
<html><head>
|
8
|
-
<title>%s Authentication</title>
|
9
|
-
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
10
|
-
<style type='text/css'>
|
11
|
-
body { background-color: %s; font-family: sans-serif; text-align: center; margin: 0px; }
|
12
|
-
h1, p { color: %s; }
|
13
|
-
.err {
|
14
|
-
padding: 5px;
|
15
|
-
border-radius: 5px;
|
16
|
-
-moz-border-radius: 5px;
|
17
|
-
-webkit-border-radius: 5px;
|
18
|
-
color: white;
|
19
|
-
background-color: red;
|
20
|
-
}
|
21
|
-
div {
|
22
|
-
text-align: left;
|
23
|
-
width: 400px;
|
24
|
-
margin: 0px auto;
|
25
|
-
padding: 10px;
|
26
|
-
-webkit-border-bottom-left-radius: 10px;
|
27
|
-
-moz-border-radius-bottomleft: 10px;
|
28
|
-
border-bottom-left-radius: 10px;
|
29
|
-
-webkit-border-bottom-right-radius: 10px;
|
30
|
-
-moz-border-radius-bottomright: 10px;
|
31
|
-
border-bottom-right-radius: 10px;
|
32
|
-
-moz-box-shadow: 0px 0px 5px #333;
|
33
|
-
-webkit-box-shadow: 0px 0px 5px #333;
|
34
|
-
box-shadow: 0px 0px 5px #333;
|
35
|
-
background-color: %s; }
|
36
|
-
input[type=text], input[type=password] { width: 392px; padding: 4px; border: 0px; font-size: 20px; }
|
37
|
-
</style>
|
38
|
-
%s
|
39
|
-
</head><body>
|
40
|
-
<div>
|
41
|
-
<h1>%s</h1>
|
42
|
-
%s
|
43
|
-
%s
|
44
|
-
<p>Please Sign In</p>
|
45
|
-
<form action="%s" method="post" accept-charset="utf-8">
|
46
|
-
<input type="text" name="cerberus_login" value="%s" id='login'><br />
|
47
|
-
<input type="password" name="cerberus_pass" value="%s" id='pass'>
|
48
|
-
<p><input type="submit" value="SIGN IN →"></p>
|
49
|
-
</form>
|
50
|
-
<script type="text/javascript" charset="utf-8">
|
51
|
-
var login = document.getElementById('login');
|
52
|
-
var pass = document.getElementById('pass');
|
53
|
-
var focus = function() {
|
54
|
-
if (this.value==this.id) this.value = '';
|
55
|
-
}
|
56
|
-
var blur = function() {
|
57
|
-
if (this.value=='') this.value = this.id;
|
58
|
-
}
|
59
|
-
login.onfocus = focus;
|
60
|
-
pass.onfocus = focus;
|
61
|
-
login.onblur = blur;
|
62
|
-
pass.onblur = blur;
|
63
|
-
</script>
|
64
|
-
</div>
|
65
|
-
</body></html>
|
66
|
-
PAGE
|
67
|
-
|
68
|
-
def initialize(app, options={}, &block)
|
69
|
-
@app = app
|
70
|
-
defaults = {
|
71
|
-
:company_name => 'Cerberus',
|
72
|
-
:bg_color => '#999',
|
73
|
-
:fg_color => '#CCC',
|
74
|
-
:text_color => '#FFF',
|
75
|
-
:icon_url => nil
|
76
|
-
}
|
77
|
-
@options = defaults.merge(options)
|
78
|
-
@block = block
|
79
|
-
end
|
80
|
-
|
81
|
-
def call(env)
|
82
|
-
dup._call(env)
|
83
|
-
end
|
84
|
-
|
85
|
-
def _call(env)
|
86
|
-
raise(NoSessionError, 'Cerberus cannot work without Session') if env['rack.session'].nil?
|
87
|
-
req = Rack::Request.new(env)
|
88
|
-
login = req['cerberus_login']
|
89
|
-
pass = req['cerberus_pass']
|
90
|
-
err = req.post? ? "<p class='err'>Wrong login or password</p>" : ''
|
91
|
-
if ((env['rack.session']['cerberus_user']!=nil && env['PATH_INFO']!='/logout') || (login && pass && @block.call(login, pass, req)))
|
92
|
-
env['rack.session']['cerberus_user'] ||= login
|
93
|
-
if env['PATH_INFO']=='/logout'
|
94
|
-
res = Rack::Response.new(env)
|
95
|
-
res.redirect(env['SCRIPT_NAME']=='' ? '/' : env['SCRIPT_NAME'])
|
96
|
-
res.finish
|
97
|
-
else
|
98
|
-
@app.call(env)
|
99
|
-
end
|
100
|
-
else
|
101
|
-
env['rack.session'].delete('cerberus_user')
|
102
|
-
icon = @options[:icon_url].nil? ? '' : "<img src='#{@options[:icon_url]}' /><br />"
|
103
|
-
css = @options[:css_location].nil? ? '' : "<link href='#{@options[:css_location]}' rel='stylesheet' type='text/css'>"
|
104
|
-
[
|
105
|
-
401, {'Content-Type' => 'text/html'},
|
106
|
-
[AUTH_PAGE % [
|
107
|
-
@options[:company_name], @options[:bg_color], @options[:text_color], @options[:fg_color], css, @options[:company_name],
|
108
|
-
icon, err, env['REQUEST_URI'], html_escape(req['cerberus_login']||'login'), html_escape(req['cerberus_pass']||'pass')
|
109
|
-
]]
|
110
|
-
]
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
private
|
115
|
-
|
116
|
-
# Stolen from ERB
|
117
|
-
def html_escape(s)
|
118
|
-
s.to_s.gsub(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<")
|
119
|
-
end
|
120
|
-
|
121
|
-
end
|
data/example.css
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
body { background-color: #CCC; }
|
data/example.ru
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
require ::File.dirname(__FILE__) + '/cerberus'
|
2
|
-
use Rack::Session::Cookie, :secret => 'change_me'
|
3
|
-
F = ::File
|
4
|
-
|
5
|
-
map '/' do
|
6
|
-
run lambda {|env|
|
7
|
-
body = <<-EOB.strip
|
8
|
-
<html>
|
9
|
-
<head>
|
10
|
-
<title>Cerberus</title>
|
11
|
-
</head>
|
12
|
-
<body>This page is public, so you can see it. But what happens if you want to see a <a href='/secret'>Secret Page</a>? Nevertheless, I can give you access:<br /><br />
|
13
|
-
Login: <b>mario</b><br />Pass: <b>bros</b>
|
14
|
-
</body>
|
15
|
-
</html>
|
16
|
-
EOB
|
17
|
-
[200, {'Content-Type' => 'text/html'}, [body]]
|
18
|
-
}
|
19
|
-
end
|
20
|
-
|
21
|
-
map '/secret' do
|
22
|
-
use Cerberus, {:company_name => 'Nintendo', :fg_color => 'red', :css_location => '/css'} do |login,pass|
|
23
|
-
[login,pass]==['mario','bros']
|
24
|
-
end
|
25
|
-
run lambda {|env|
|
26
|
-
[200, {'Content-Type' => 'text/plain'}, ['Welcome back Mario. Your Credit Card number is: 9292']]
|
27
|
-
}
|
28
|
-
end
|
29
|
-
|
30
|
-
map '/css' do
|
31
|
-
run lambda {|env|
|
32
|
-
path = F.expand_path('./example.css')
|
33
|
-
[200, {'Content-Type' => 'text/css', "Last-Modified" => F.mtime(path).httpdate, "Content-Length" => F.size?(path).to_s}, [F.read(path)]]
|
34
|
-
}
|
35
|
-
end
|
data/spec.rb
DELETED
@@ -1,84 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'bacon'
|
3
|
-
require 'rack'
|
4
|
-
|
5
|
-
require ::File.dirname(__FILE__) + '/cerberus'
|
6
|
-
|
7
|
-
Bacon.summary_on_exit
|
8
|
-
|
9
|
-
describe 'cerberus' do
|
10
|
-
|
11
|
-
secret_app = lambda {|env| [200, {'Content-Type'=>'text/plain'}, env['rack.session'].inspect] }
|
12
|
-
app = Rack::Session::Cookie.new(Cerberus.new(secret_app, {}) {|login,pass| [login,pass]==['mario@nintendo.com','bros']})
|
13
|
-
req = Rack::MockRequest.new(app)
|
14
|
-
app_with_css = Rack::Session::Cookie.new(Cerberus.new(secret_app, {:css_location=>'/main.css'}) {|login,pass| [login,pass]==['mario','bros']})
|
15
|
-
req_with_css = Rack::MockRequest.new(app_with_css)
|
16
|
-
cookie = ''
|
17
|
-
|
18
|
-
should 'Raise if there is no session' do
|
19
|
-
no_session_app = Cerberus.new(secret_app, {}) {|login,pass| [login,pass]==['mario','bros']}
|
20
|
-
no_session_req = Rack::MockRequest.new(no_session_app)
|
21
|
-
lambda { no_session_req.get('/') }.should.raise(Cerberus::NoSessionError).message.should=='Cerberus cannot work without Session'
|
22
|
-
end
|
23
|
-
|
24
|
-
should 'Stop request if you are not already logged in' do
|
25
|
-
res = req.get('/')
|
26
|
-
res.status.should==401
|
27
|
-
res.body.class==String
|
28
|
-
res.body.should.match(/name="cerberus_login" value="login"/)
|
29
|
-
res.body.should.match(/name="cerberus_pass" value="pass"/)
|
30
|
-
end
|
31
|
-
|
32
|
-
should 'Stop request if you send wrong details and keep query values' do
|
33
|
-
res = req.post('/', :params => {'cerberus_login' => 'fake_login', 'cerberus_pass' => 'fake_pass'})
|
34
|
-
res.status.should==401
|
35
|
-
res.body.should.match(/name="cerberus_login" value="fake_login"/)
|
36
|
-
res.body.should.match(/name="cerberus_pass" value="fake_pass"/)
|
37
|
-
end
|
38
|
-
|
39
|
-
should 'Escape HTML on submitted info' do
|
40
|
-
res = req.post('/', :params => {'cerberus_login' => '<script>bad</script>', 'cerberus_pass' => '<script>bad</script>'})
|
41
|
-
res.status.should==401
|
42
|
-
res.body.should.match(/name="cerberus_login" value="<script>bad<\/script>"/)
|
43
|
-
res.body.should.match(/name="cerberus_pass" value="<script>bad<\/script>"/)
|
44
|
-
end
|
45
|
-
|
46
|
-
should 'Give access with the appropriate login and pass' do
|
47
|
-
res = req.get('/', :params => {'cerberus_login' => 'mario@nintendo.com', 'cerberus_pass' => 'bros'})
|
48
|
-
cookie = res["Set-Cookie"]
|
49
|
-
res.status.should==200
|
50
|
-
end
|
51
|
-
|
52
|
-
should 'Use session for persistent login' do
|
53
|
-
res = req.get('/', "HTTP_COOKIE" => cookie)
|
54
|
-
res.status.should==200
|
55
|
-
res.body.should=='{"cerberus_user"=>"mario@nintendo.com"}'
|
56
|
-
cookie = res["Set-Cookie"]
|
57
|
-
req.get('/', "HTTP_COOKIE" => cookie).status.should==200
|
58
|
-
end
|
59
|
-
|
60
|
-
should 'Logout via /logout path' do
|
61
|
-
res = req.get('/logout', "HTTP_COOKIE" => cookie)
|
62
|
-
res.status.should==401
|
63
|
-
cookie = res["Set-Cookie"]
|
64
|
-
res = req.get('/', "HTTP_COOKIE" => cookie)
|
65
|
-
res.status.should==401
|
66
|
-
end
|
67
|
-
|
68
|
-
should 'Not send not_found when logging after a logout (because the path is /logout)' do
|
69
|
-
res = req.get('/logout', :params => {'cerberus_login' => 'mario@nintendo.com', 'cerberus_pass' => 'bros'})
|
70
|
-
res.status.should==302
|
71
|
-
res['Location'].should=='/'
|
72
|
-
|
73
|
-
req = Rack::MockRequest.new(Rack::URLMap.new({'/backend' => app}))
|
74
|
-
res = req.get('/backend/logout', :params => {'cerberus_login' => 'mario@nintendo.com', 'cerberus_pass' => 'bros'})
|
75
|
-
res.status.should==302
|
76
|
-
res['Location'].should=='/backend'
|
77
|
-
end
|
78
|
-
|
79
|
-
should 'Use an external css file only if requested' do
|
80
|
-
req.get('/').body.should.not.match(/<link/)
|
81
|
-
req_with_css.get('/').body.should.match(/<link/)
|
82
|
-
end
|
83
|
-
|
84
|
-
end
|