rack-cerberus 0.3.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|