coca 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +208 -0
- data/Rakefile +29 -0
- data/app/assets/javascripts/coca/application.js +15 -0
- data/app/assets/stylesheets/coca/application.css +13 -0
- data/app/controllers/coca/authentications_controller.rb +24 -0
- data/app/helpers/coca/application_helper.rb +4 -0
- data/app/views/layouts/coca/application.html.erb +14 -0
- data/config/routes.rb +7 -0
- data/lib/coca.rb +62 -0
- data/lib/coca/auth_cookie.rb +77 -0
- data/lib/coca/delegate.rb +62 -0
- data/lib/coca/engine.rb +16 -0
- data/lib/coca/version.rb +3 -0
- data/lib/devise/models/cocable.rb +11 -0
- data/lib/devise/strategies/cocable.rb +56 -0
- data/lib/tasks/coca_tasks.rake +4 -0
- metadata +256 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
# Coca
|
2
|
+
|
3
|
+
Coca stands for **Chain of Command Authentication**. It's a lightweight authentication-delegation scheme that makes SSO very easy to add to a devise-based rails app. Delegation is through an ordinary JSON API so you can pass any user data you like down the chain after authentication succeeds.
|
4
|
+
|
5
|
+
We thought about calling it 'delegated devise' because in the present implementation it's just a devise strategy that can delegate to another application and an API for receiving delegated auth calls. In future we'd like to extend the principle more widely so we gave it a more general name.
|
6
|
+
|
7
|
+
Coca is highly configurable but the defaults are simple and secure. It is designed to be part of a service-based architecture, where it will bind together a set of applications around a shared authentication service.
|
8
|
+
|
9
|
+
Coca is naturally extensible because you can send any information you like down the chain and do whatever you like when it arrives. RBAC or other authorization information is easily distributed this way.
|
10
|
+
|
11
|
+
Since we already have a nice tidy deference tree, Coca also supports attribute propagation. Your auth master is likely also to be a directory server and coca can help you to dry out contact (or any) attributes by passing notifications up and down the chain when they change.
|
12
|
+
|
13
|
+
|
14
|
+
## tl;dr
|
15
|
+
|
16
|
+
class User < ActiveRecord::Base
|
17
|
+
# must have a :uid column
|
18
|
+
devise :cocable
|
19
|
+
end
|
20
|
+
|
21
|
+
# and in an initialiser:
|
22
|
+
|
23
|
+
Coca.secret = "Shared secret key"
|
24
|
+
|
25
|
+
Coca.delegate_to do |master|
|
26
|
+
master.host = auth.example.com
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
## Use coca if
|
31
|
+
|
32
|
+
* You want to implement single sign-on within a cluster of trusted applications that all inhabit a single domain;
|
33
|
+
|
34
|
+
* You want an easy way to pass around permissions or other extended user information;
|
35
|
+
|
36
|
+
* You want your authentication service to be part of your usual rails cluster and benefit from its existing availability and backup measures;
|
37
|
+
|
38
|
+
* You're not planning to offer a remote login-with-me service to applications as yet unknown.
|
39
|
+
|
40
|
+
There's nothing to stop you running coca in parallel with other auth services including OAuth, SAML or even LDAP.
|
41
|
+
|
42
|
+
|
43
|
+
## Usage
|
44
|
+
|
45
|
+
Each link in the chain of command is defined as a server to which we defer and/or a set of servers from which we accept requests. Within that link you also have to declare that one or models should take part in the authentication scheme.
|
46
|
+
|
47
|
+
|
48
|
+
### Configuring the chain
|
49
|
+
|
50
|
+
Each application can be master, servant or both. Configuration is usually in config/initialisers/coca.rb:
|
51
|
+
|
52
|
+
# On every application in this chain
|
53
|
+
|
54
|
+
Coca.secret = "Shared secret key"
|
55
|
+
|
56
|
+
# To act as a servant:
|
57
|
+
|
58
|
+
Coca.delegate_to do |master|
|
59
|
+
master.host = fq.domain.com or ip address or localhost
|
60
|
+
master.port = optional
|
61
|
+
master.path = "/coca/user"
|
62
|
+
end
|
63
|
+
|
64
|
+
# To act as a master:
|
65
|
+
|
66
|
+
Coca.delegate_from do |servant|
|
67
|
+
servant.host = fq.domain.com or ip address or localhost
|
68
|
+
servant.ttl = 3600
|
69
|
+
end
|
70
|
+
|
71
|
+
You can have any number of links up and down. More than one `delegate_to` is unlikely but possible.
|
72
|
+
|
73
|
+
# TTL can be set globally
|
74
|
+
Coca.token_ttl = 86400
|
75
|
+
|
76
|
+
# disable to not broadcast user changes or ignore incoming updates
|
77
|
+
Coca.propagate_updates = true
|
78
|
+
|
79
|
+
# disable to allow insecure requests in and out
|
80
|
+
Coca.require_https = true
|
81
|
+
|
82
|
+
# disable to omit reverse IP check on incoming requests and updates
|
83
|
+
Coca.check_source = true
|
84
|
+
|
85
|
+
# you may want to set this per environment
|
86
|
+
Coca.cookie_name = 'must_be_the_same_across_sso_group'
|
87
|
+
Coca.cookie_domain = :all
|
88
|
+
|
89
|
+
|
90
|
+
### Delegating in a model
|
91
|
+
|
92
|
+
To start with coca is implemented as a devise strategy. Minimally:
|
93
|
+
|
94
|
+
class User < ActiveRecord::Base
|
95
|
+
devise :cocable
|
96
|
+
|
97
|
+
You can try other strategies first: they will work in the usual way and check against local resource properties. The `:cocable` strategy would normally be last.
|
98
|
+
|
99
|
+
class User < ActiveRecord::Base
|
100
|
+
devise :database_authenticatable,
|
101
|
+
:token_authenticatable,
|
102
|
+
:cocable
|
103
|
+
|
104
|
+
If masters are defined, the cocable strategy will pass credentials up the chain before finally admitting failure.
|
105
|
+
|
106
|
+
|
107
|
+
### Authenticating in the master
|
108
|
+
|
109
|
+
The delegated auth call is just an ordinary API request. The supplied auth parameters are passed on. If they are accepted by the master server then the API request will work and a JSON packet will be returned. If not, we return 401. All of this is done just by trying to authenticate the resource in the usual way, and usually requires no change to your resource class.
|
110
|
+
|
111
|
+
To continue the chain upward, just add the cocable strategy to the User class in your master application (and define a master).
|
112
|
+
|
113
|
+
|
114
|
+
### Routing in the master
|
115
|
+
|
116
|
+
For most purposese you only need to mount coca:
|
117
|
+
|
118
|
+
mount Coca::Engine => "/coca", :as => :coca
|
119
|
+
|
120
|
+
The main api controller response to /coca/authenticate. It's a very ordinary controller that inherits from your ApplicationController and tries to authenticate against your local resources.
|
121
|
+
|
122
|
+
|
123
|
+
### Data
|
124
|
+
|
125
|
+
Coca requires all your authenticable resources to have a `uid` column, and it expects your authentication master to set that value and pass it down on auth.
|
126
|
+
|
127
|
+
In the servant coca requires no devise columns. If you're not trying any other strategies locally, your user model can be very minimal.
|
128
|
+
|
129
|
+
In the master coca has no other requirements but you will want a fuller set of devise strategies, probably include `:database_authenticatable` and `:token_authenticatable` and all the data columns they require.
|
130
|
+
|
131
|
+
|
132
|
+
## Extending Coca
|
133
|
+
|
134
|
+
At heart coca is a simple remote authentication service: credentials are checked remotely and if they are found valid, user information is returned.
|
135
|
+
|
136
|
+
The data package that we return on successful auth is built by calling `serializable_hash` on the user model in the coca master. As a minimum it should return an auth token, but you can add any other data you like. If you have local resources that are owned by remote users, some kind of uid will be needed. Also permissions, friends lists, recent messages, password replacement instructions: anything it makes sense to hold centrally can be worked out in the master and passed down to all servant applications.
|
137
|
+
|
138
|
+
Your coca response package will be treated as model attributes. It's up to you to make sure that your local user object does the right thing with whatever is passed down.
|
139
|
+
|
140
|
+
|
141
|
+
## Reminders and confirmations
|
142
|
+
|
143
|
+
We're going to need some routing here. Only the auth master is in a position to handle password-reminders and to send confirmation and other messages to new users.
|
144
|
+
|
145
|
+
|
146
|
+
## Propagating resource changes
|
147
|
+
|
148
|
+
It's a short step from an auth server to a directory server. Once authentication is centralised, you start to avoid duplicating other user attributes. But what if you want to display the user's email address in one of the servant apps, or offer the user a change of address form while they're working in the calendar? You don't always want to send them somewhere else, or call another server just to get a single attribute.
|
149
|
+
|
150
|
+
|
151
|
+
## How coca works
|
152
|
+
|
153
|
+
All the actual authentication done by devise as usual. Coca just adds a way to try out credentials on another server.
|
154
|
+
|
155
|
+
1. A coca servant app has its own devise strategies. You can set that up any way you like, and you may choose to accept credentials locally for some users.
|
156
|
+
|
157
|
+
2. If the app is presented with an auth token or login pair that it doesn't recognise, those credentials are passed up to the configured master application in a JSON request that includes a predefined API key.
|
158
|
+
|
159
|
+
3. The master will check that the requesting host and the secret key match before it gives any response.
|
160
|
+
|
161
|
+
4. If the supplied credentials succeed in authenticating the user at the master application, it will return 200 and a package of user information. If the app doesn't recognise the credentials, it may pass them on to its own coca master. If there isn't one, or that request fails too, it will return 401 and no data.
|
162
|
+
|
163
|
+
5. As the confirmation package travels down the chain each servant app stores the auth token and does what it likes with any other data that is returned.
|
164
|
+
|
165
|
+
6. Subsequent requests that present a valid auth token will be successful at the bottom of the chain.
|
166
|
+
|
167
|
+
7. The master will include in the confirmation package a configurable TTL for the auth token. You can use this to set a balance between revokability and responsiveness. Set it to zero for maximum control or to a sensible session length like 30 minutes to minimise the coca overhead.
|
168
|
+
|
169
|
+
8. The original servant app receives the confirmation package, saves the authentication token and does whatever it does with the other data. It also puts the auth token in a domain cookie (not the usual session cookie) that will be picked up by any other application in the same domain.
|
170
|
+
|
171
|
+
9. When the same browser visits another application in the same domain, it presents the master's auth token cookie. The second servant app passes that token up the chain to the same master. Having issued the token, the master accepts it and returns the same confirmation package as before. The second servant stores the auth token locally so that it too can accept subsequent requests without delegation, token expiry permitting.
|
172
|
+
|
173
|
+
|
174
|
+
## Compared to OAuth
|
175
|
+
|
176
|
+
* **This is not an authorization service.** There is no mechanism for issuing API tokens or granting access to individual resources. The chain of command is defined on initialisation and all requests and responses have to come from sources that are already trusted. OAuth makes it possible to have ad-hoc relationships between strangers. Coca is a conversation among friends.
|
177
|
+
|
178
|
+
* **OAuth is a user-pull scheme** that bounces off the browser a few times and requires it to pass callbacks between servers. The user sees confirmation dialogs from different providers or is forced to visit services in a certain order. Coca works by server-pull, invisible to the user and no more difficult than just logging in. It doesn't matter where you arrive: once you've logged in, you're in.
|
179
|
+
|
180
|
+
* **Oauth works across domains.** Coca mostly doesn't: you can authenticate against a master at any address, but SSO is limited to domains that can share a cookie. In practice that means coca only provides SSO across related subdomains.
|
181
|
+
|
182
|
+
* **OAuth is horrible to work with,** absurdly complicated and a daft way to implement SSO unless you are also piggybacking on a remote service. Coca is quite nice.
|
183
|
+
|
184
|
+
|
185
|
+
## Compared to XAuth
|
186
|
+
|
187
|
+
* XAuth is just OAuth made stupider. It is superficially similar to coca in that credentials are passed to a remote server, but after that you're bouncing around callbacks again.
|
188
|
+
|
189
|
+
|
190
|
+
## Compared to LDAP
|
191
|
+
|
192
|
+
There's nothing wrong with LDAP. If you're working in a situation where an LDAP server is available, and you don't need to layer RBAC or anything else on top, we advise you to use it.
|
193
|
+
|
194
|
+
If you don't already have an LDAP server with its own availability regime and a dedicated team of lab-coats, you get to choose between several lardy Java services. None of them will sit nicely on your load-balanced cloud node, and none of them are easy to couple with RBAC or other services. In that case you're probably better off using coca and providing a CardDAV service from the top of the chain.
|
195
|
+
|
196
|
+
|
197
|
+
## Author
|
198
|
+
|
199
|
+
William Ross for spanner. Will at spanner dot org.
|
200
|
+
|
201
|
+
## Copyright
|
202
|
+
|
203
|
+
Copyright Spanner 2013. All rights reserved.
|
204
|
+
|
205
|
+
## License
|
206
|
+
|
207
|
+
Released under the MIT license.
|
208
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'Coca'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
24
|
+
load 'rails/tasks/engine.rake'
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
Bundler::GemHelper.install_tasks
|
29
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// the compiled file.
|
9
|
+
//
|
10
|
+
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
|
11
|
+
// GO AFTER THE REQUIRES BELOW.
|
12
|
+
//
|
13
|
+
//= require jquery
|
14
|
+
//= require jquery_ujs
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
9
|
+
* compiled file, but it's generally better to create a new file per style scope.
|
10
|
+
*
|
11
|
+
*= require_self
|
12
|
+
*= require_tree .
|
13
|
+
*/
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Coca
|
2
|
+
class AuthenticationsController < RocketPants::Base
|
3
|
+
include Devise::Controllers::Helpers
|
4
|
+
|
5
|
+
before_filter :require_valid_servant!
|
6
|
+
before_filter :allow_params_authentication!
|
7
|
+
|
8
|
+
def show
|
9
|
+
scope = params[:scope].to_sym
|
10
|
+
if user = warden.authenticate(:scope => scope)
|
11
|
+
expose user
|
12
|
+
else
|
13
|
+
head :unauthorized
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def require_valid_servant!
|
20
|
+
Coca.valid_servant?(request.remote_ip, params[:key])
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Coca</title>
|
5
|
+
<%= stylesheet_link_tag "coca/application", :media => "all" %>
|
6
|
+
<%= javascript_include_tag "coca/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
data/lib/coca.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require "coca/engine"
|
2
|
+
require 'coca/delegate'
|
3
|
+
require 'coca/auth_cookie'
|
4
|
+
require 'devise/models/cocable'
|
5
|
+
require 'devise/strategies/cocable'
|
6
|
+
|
7
|
+
Warden::Manager.after_set_user do |user, warden, options|
|
8
|
+
Coca::AuthCookie.new(warden.cookies, options[:scope]).set(user)
|
9
|
+
end
|
10
|
+
|
11
|
+
Warden::Manager.before_logout do |user, warden, options|
|
12
|
+
Coca::AuthCookie.new(warden.cookies, options[:scope]).unset
|
13
|
+
end
|
14
|
+
|
15
|
+
module Coca
|
16
|
+
mattr_accessor :masters,
|
17
|
+
:servants,
|
18
|
+
:cookie_domain,
|
19
|
+
:check_source,
|
20
|
+
:require_https,
|
21
|
+
:propagate_updates,
|
22
|
+
:token_ttl,
|
23
|
+
:secret,
|
24
|
+
:debug
|
25
|
+
|
26
|
+
@@masters = []
|
27
|
+
@@servants = []
|
28
|
+
@@cookie_domain = :all
|
29
|
+
@@check_source = true
|
30
|
+
@@require_https = true
|
31
|
+
@@propagate_updates = false
|
32
|
+
@@token_ttl = 1800
|
33
|
+
@@secret = "Unset"
|
34
|
+
@@debug = true
|
35
|
+
|
36
|
+
class << self
|
37
|
+
def add_master(name, &block)
|
38
|
+
@@masters.push Coca::Delegate.new(name, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_servant(name, &block)
|
42
|
+
@@servants.push Coca::Delegate.new(name, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def valid_servant?(referer, key)
|
46
|
+
servants.find { |servant| servant.valid_referer?(referer) && valid_secret?(key) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid_master?(referer, key)
|
50
|
+
masters.find { |master| master.valid_referer?(referer) && valid_secret?(key) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid_secret?(key)
|
54
|
+
!!key && !key.blank? && key == Coca.secret
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
Devise.add_module :cocable,
|
60
|
+
:route => :session,
|
61
|
+
:strategy => true,
|
62
|
+
:controller => :sessions
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'signed_json'
|
2
|
+
|
3
|
+
# Based on the same class in devise-login-cookie by pda
|
4
|
+
# https://github.com/pda/devise-login-cookie
|
5
|
+
|
6
|
+
module Coca
|
7
|
+
|
8
|
+
class AuthCookie
|
9
|
+
|
10
|
+
def initialize(cookies, scope)
|
11
|
+
@cookies = cookies
|
12
|
+
@scope = scope
|
13
|
+
end
|
14
|
+
|
15
|
+
# Sets the cookie, referencing the given resource.id (e.g. User)
|
16
|
+
def set(resource)
|
17
|
+
@cookies[cookie_name] = cookie_options.merge(:value => encoded_value(resource))
|
18
|
+
end
|
19
|
+
|
20
|
+
# Unsets the cookie via the HTTP response.
|
21
|
+
def unset
|
22
|
+
@cookies.delete cookie_name, cookie_options
|
23
|
+
end
|
24
|
+
|
25
|
+
def token
|
26
|
+
value[0]
|
27
|
+
end
|
28
|
+
|
29
|
+
# The Time at which the cookie was created.
|
30
|
+
def created_at
|
31
|
+
valid? ? Time.at(value[1]) : nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# Whether the cookie appears valid.
|
35
|
+
def valid?
|
36
|
+
present? && value.all?
|
37
|
+
end
|
38
|
+
|
39
|
+
def present?
|
40
|
+
@cookies[cookie_name].present?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Whether the cookie was set since the given Time
|
44
|
+
def set_since?(time)
|
45
|
+
created_at && created_at >= time
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def value
|
51
|
+
begin
|
52
|
+
@value = signer.decode @cookies[cookie_name]
|
53
|
+
rescue SignedJson::Error
|
54
|
+
[nil, nil]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def cookie_name
|
59
|
+
:"coca_#{@scope}_token"
|
60
|
+
end
|
61
|
+
|
62
|
+
def encoded_value(resource)
|
63
|
+
signer.encode [ resource.authentication_token, Time.now.to_i ]
|
64
|
+
end
|
65
|
+
|
66
|
+
def cookie_options
|
67
|
+
@session_options ||= Rails.configuration.session_options
|
68
|
+
@session_options.slice(:path, :secure, :httponly).merge(:domain => Coca.cookie_domain)
|
69
|
+
end
|
70
|
+
|
71
|
+
def signer
|
72
|
+
@signer ||= SignedJson::Signer.new(Coca.secret)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
require 'httparty'
|
3
|
+
|
4
|
+
module Coca
|
5
|
+
class Delegate
|
6
|
+
include HTTParty
|
7
|
+
attr_writer :name, :host, :port, :ttl, :protocol
|
8
|
+
|
9
|
+
def initialize(name='')
|
10
|
+
@name = name
|
11
|
+
yield self if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
def ttl
|
15
|
+
@ttl ||= Coca.token_ttl
|
16
|
+
end
|
17
|
+
|
18
|
+
def host
|
19
|
+
@host ||= "localhost"
|
20
|
+
end
|
21
|
+
|
22
|
+
def port
|
23
|
+
@port
|
24
|
+
end
|
25
|
+
|
26
|
+
def protocol
|
27
|
+
@protocol ||= 'https'
|
28
|
+
end
|
29
|
+
|
30
|
+
def base_uri
|
31
|
+
[host, port].compact.join(':')
|
32
|
+
end
|
33
|
+
|
34
|
+
def path
|
35
|
+
@path ||= "/coca/1/check"
|
36
|
+
end
|
37
|
+
|
38
|
+
def ip_address
|
39
|
+
@ip ||= Resolv.new.getaddress(host)
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid_referer?(referer)
|
43
|
+
!!referer && !referer.blank? && referer == ip_address
|
44
|
+
end
|
45
|
+
|
46
|
+
def host_url_with_port
|
47
|
+
hup = "#{protocol}://#{host}"
|
48
|
+
hup << ":#{port}" if port
|
49
|
+
hup
|
50
|
+
end
|
51
|
+
|
52
|
+
def url
|
53
|
+
@url ||= URI.join(host_url_with_port, path).to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def authenticate(scope, credentials)
|
57
|
+
response = HTTParty.get url, :scope => scope, :"#{scope}" => credentials
|
58
|
+
response.body if response.code != 401
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
data/lib/coca/engine.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'devise'
|
2
|
+
require 'rocket_pants'
|
3
|
+
|
4
|
+
module Coca
|
5
|
+
class Engine < ::Rails::Engine
|
6
|
+
isolate_namespace Coca
|
7
|
+
|
8
|
+
config.generators do |g|
|
9
|
+
g.test_framework :rspec, :fixture => false
|
10
|
+
g.fixture_replacement :factory_girl, :dir => 'spec/factories'
|
11
|
+
g.assets false
|
12
|
+
g.helper false
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
data/lib/coca/version.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'devise/strategies/authenticatable'
|
2
|
+
|
3
|
+
module Devise
|
4
|
+
module Strategies
|
5
|
+
|
6
|
+
# The cocable strategy will look for a user in two places:
|
7
|
+
# in a domain cookie set by one of our peers and verified by an upstream app
|
8
|
+
# or in credentials supplied to us and verified by an upstream app
|
9
|
+
#
|
10
|
+
# (If the credentials matched locally, we wouldn't usually get to this strategy)
|
11
|
+
|
12
|
+
class Cocable < Authenticatable
|
13
|
+
def valid?
|
14
|
+
valid_for_params_auth? || cookie.valid?
|
15
|
+
end
|
16
|
+
|
17
|
+
def authenticate!
|
18
|
+
resource = nil
|
19
|
+
response = nil
|
20
|
+
|
21
|
+
if authentication_hash
|
22
|
+
response = delegate(authentication_hash)
|
23
|
+
else
|
24
|
+
response = delegate({:auth_token => cookie.token})
|
25
|
+
end
|
26
|
+
|
27
|
+
if response
|
28
|
+
resource = mapping.to.where(:uid => response[:uid]).first_or_create
|
29
|
+
resource.update_attributes(response.except(:uid))
|
30
|
+
end
|
31
|
+
|
32
|
+
success!(resource) if resource# && validate(resource)
|
33
|
+
end
|
34
|
+
|
35
|
+
def delegate(credentials)
|
36
|
+
response = nil
|
37
|
+
Coca.masters.each do |master|
|
38
|
+
response = master.authenticate(scope, credentials)
|
39
|
+
break if response
|
40
|
+
end
|
41
|
+
response
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
# AuthCookie takes care of the naming and signing of cookies for each warden scope.
|
46
|
+
# All we need is the token contained in the cookie for this scope, if there is one.
|
47
|
+
#
|
48
|
+
def cookie
|
49
|
+
@cookie ||= Coca::AuthCookie.new(cookies, scope)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
Warden::Strategies.add(:cocable, Devise::Strategies::Cocable)
|
metadata
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: coca
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- William Ross
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.13
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.2.13
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: jquery-rails
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: devise
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rocket_pants
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: signed_json
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: mysql2
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rspec-rails
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: factory_girl_rails
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: simplecov
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: database_cleaner
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ! '>='
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
- !ruby/object:Gem::Dependency
|
175
|
+
name: factory_girl_rails
|
176
|
+
requirement: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ! '>='
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
type: :development
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
none: false
|
186
|
+
requirements:
|
187
|
+
- - ! '>='
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
- !ruby/object:Gem::Dependency
|
191
|
+
name: shoulda-matchers
|
192
|
+
requirement: !ruby/object:Gem::Requirement
|
193
|
+
none: false
|
194
|
+
requirements:
|
195
|
+
- - ! '>='
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
type: :development
|
199
|
+
prerelease: false
|
200
|
+
version_requirements: !ruby/object:Gem::Requirement
|
201
|
+
none: false
|
202
|
+
requirements:
|
203
|
+
- - ! '>='
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
description: Coca is a chainable, devise-based scheme for delegation of authentication.
|
207
|
+
It works through a standard JSON API so the packet that you pass down on auth is
|
208
|
+
completely configurable. See coca-rbac for a similarly transparent RBAC implementation.
|
209
|
+
email:
|
210
|
+
- will@spanner.org
|
211
|
+
executables: []
|
212
|
+
extensions: []
|
213
|
+
extra_rdoc_files: []
|
214
|
+
files:
|
215
|
+
- app/assets/javascripts/coca/application.js
|
216
|
+
- app/assets/stylesheets/coca/application.css
|
217
|
+
- app/controllers/coca/authentications_controller.rb
|
218
|
+
- app/helpers/coca/application_helper.rb
|
219
|
+
- app/views/layouts/coca/application.html.erb
|
220
|
+
- config/routes.rb
|
221
|
+
- lib/coca/auth_cookie.rb
|
222
|
+
- lib/coca/delegate.rb
|
223
|
+
- lib/coca/engine.rb
|
224
|
+
- lib/coca/version.rb
|
225
|
+
- lib/coca.rb
|
226
|
+
- lib/devise/models/cocable.rb
|
227
|
+
- lib/devise/strategies/cocable.rb
|
228
|
+
- lib/tasks/coca_tasks.rake
|
229
|
+
- MIT-LICENSE
|
230
|
+
- Rakefile
|
231
|
+
- README.md
|
232
|
+
homepage: https://github.com/spanner/coca
|
233
|
+
licenses: []
|
234
|
+
post_install_message:
|
235
|
+
rdoc_options: []
|
236
|
+
require_paths:
|
237
|
+
- lib
|
238
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
239
|
+
none: false
|
240
|
+
requirements:
|
241
|
+
- - ! '>='
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: '0'
|
244
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
245
|
+
none: false
|
246
|
+
requirements:
|
247
|
+
- - ! '>='
|
248
|
+
- !ruby/object:Gem::Version
|
249
|
+
version: '0'
|
250
|
+
requirements: []
|
251
|
+
rubyforge_project:
|
252
|
+
rubygems_version: 1.8.23
|
253
|
+
signing_key:
|
254
|
+
specification_version: 3
|
255
|
+
summary: Lightweight, simple SSO for rails.
|
256
|
+
test_files: []
|