leash_provider 0.0.1
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 +4 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +165 -0
- data/README.md +56 -0
- data/Rakefile +7 -0
- data/app/controllers/leash/provider/authorize_controller.rb +92 -0
- data/app/controllers/leash/provider/token_controller.rb +48 -0
- data/app/controllers/leash/provider_controller.rb +63 -0
- data/app/controllers/leash_controller.rb +2 -0
- data/app/models/leash/provider/access_token.rb +68 -0
- data/app/models/leash/provider/auth_code.rb +44 -0
- data/config.ru +7 -0
- data/leash_provider.gemspec +27 -0
- data/lib/generators/leash/install_generator.rb +20 -0
- data/lib/generators/leash/provider/install_generator.rb +22 -0
- data/lib/generators/templates/leash_provider.rb +28 -0
- data/lib/leash/provider/engine.rb +8 -0
- data/lib/leash/provider/routing.rb +11 -0
- data/lib/leash/provider/version.rb +5 -0
- data/lib/leash_provider.rb +30 -0
- data/spec/controllers/leash/provider/authorize_controller_spec.rb +267 -0
- data/spec/internal/app/controllers/application_controller.rb +5 -0
- data/spec/internal/app/models/admin.rb +6 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/initializers/devise.rb +228 -0
- data/spec/internal/config/routes.rb +5 -0
- data/spec/internal/db/schema.rb +28 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/spec_helper.rb +30 -0
- metadata +203 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
class Leash::Provider::AccessToken < Ohm::Model
|
2
|
+
MAX_ASSIGN_TRIES = 20
|
3
|
+
|
4
|
+
attribute :app_name
|
5
|
+
attribute :access_token
|
6
|
+
attribute :owner
|
7
|
+
attribute :created_at
|
8
|
+
attribute :accessed_at
|
9
|
+
|
10
|
+
index :app_name
|
11
|
+
index :owner
|
12
|
+
index :access_token
|
13
|
+
index :accessed_at
|
14
|
+
unique :access_token
|
15
|
+
|
16
|
+
|
17
|
+
def self.assign!(app_name, owner)
|
18
|
+
tries = 0
|
19
|
+
access_token = nil
|
20
|
+
|
21
|
+
loop do
|
22
|
+
begin
|
23
|
+
access_token = SecureRandom.hex(24)
|
24
|
+
timestamp = Time.now.to_i
|
25
|
+
self.create app_name: app_name, owner: owner_key(owner), access_token: access_token, created_at: timestamp, accessed_at: timestamp
|
26
|
+
break
|
27
|
+
|
28
|
+
rescue Ohm::UniqueIndexViolation => e
|
29
|
+
tries += 1
|
30
|
+
|
31
|
+
fail if tries > MAX_ASSIGN_TRIES
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
access_token
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def self.assign_from_auth_code!(auth_code)
|
40
|
+
access_token = assign! auth_code.app_name, auth_code.owner
|
41
|
+
auth_code.delete
|
42
|
+
access_token
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def self.valid?(access_token)
|
47
|
+
self.find(access_token: access_token).size != 0
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def self.find_by_access_token(access_token)
|
52
|
+
self.find(access_token: access_token).sort_by(:created_at, order: "DESC").first
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def self.find_by_app_name_and_owner(app_name, owner)
|
57
|
+
self.find(app_name: app_name, owner: owner_key(owner)).sort_by(:created_at, order: "DESC").first
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def self.owner_key(owner)
|
62
|
+
if owner.is_a? ActiveRecord::Base
|
63
|
+
"#{owner.class.name}##{owner.id}"
|
64
|
+
else
|
65
|
+
owner
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class Leash::Provider::AuthCode < Ohm::Model
|
2
|
+
MAX_ASSIGN_TRIES = 20
|
3
|
+
|
4
|
+
attribute :app_name
|
5
|
+
attribute :auth_code
|
6
|
+
attribute :owner
|
7
|
+
attribute :created_at
|
8
|
+
|
9
|
+
index :owner
|
10
|
+
index :auth_code
|
11
|
+
unique :auth_code
|
12
|
+
|
13
|
+
|
14
|
+
def self.assign!(app_name, owner)
|
15
|
+
tries = 0
|
16
|
+
auth_code = nil
|
17
|
+
|
18
|
+
loop do
|
19
|
+
begin
|
20
|
+
auth_code = SecureRandom.hex(24)
|
21
|
+
timestamp = Time.now.to_i
|
22
|
+
self.create app_name: app_name, owner: owner, auth_code: auth_code, created_at: timestamp
|
23
|
+
break
|
24
|
+
|
25
|
+
rescue Ohm::UniqueIndexViolation => e
|
26
|
+
tries += 1
|
27
|
+
|
28
|
+
fail if tries > MAX_ASSIGN_TRIES
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
auth_code
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def self.valid?(auth_code)
|
37
|
+
self.find(auth_code: auth_code).size != 0
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def self.find_by_auth_code(auth_code)
|
42
|
+
self.find(auth_code: auth_code).first
|
43
|
+
end
|
44
|
+
end
|
data/config.ru
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require "leash/provider/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "leash_provider"
|
7
|
+
s.version = Leash::Provider::VERSION
|
8
|
+
s.authors = ["Marcin Lewandowski"]
|
9
|
+
s.email = ["marcin@saepia.net"]
|
10
|
+
s.homepage = "http://github.com/mspanc/leash-provider"
|
11
|
+
s.summary = "High-performance OAuth2 provider for a closed set of trusted apps with multiple roles support"
|
12
|
+
s.description = "Leash allows you to build an OAuth2 provider for closed set of trusted apps. I can support multiple user types and is designed with high load in mind."
|
13
|
+
|
14
|
+
s.add_dependency "rails", "~> 4.2"
|
15
|
+
s.add_dependency "ohm"
|
16
|
+
s.add_dependency "devise"
|
17
|
+
s.add_development_dependency "rspec-rails"
|
18
|
+
s.add_development_dependency "rake"
|
19
|
+
s.add_development_dependency "factory_girl_rails", "~> 4.0"
|
20
|
+
s.add_development_dependency "bundler"
|
21
|
+
s.add_development_dependency "sqlite3"
|
22
|
+
s.add_development_dependency "combustion", "~> 0.5.3"
|
23
|
+
|
24
|
+
s.files = `git ls-files`.split("\n")
|
25
|
+
s.require_paths = ['lib']
|
26
|
+
s.required_ruby_version = '>= 1.9.3'
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
|
3
|
+
module Leash
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path("../../templates", __FILE__)
|
7
|
+
|
8
|
+
desc "Creates a Leash initializer and copy locale files to your application."
|
9
|
+
|
10
|
+
def copy_initializer
|
11
|
+
template "leash.rb", "config/initializers/leash.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def add_route
|
16
|
+
route "leash"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
|
3
|
+
module Leash
|
4
|
+
module Provider
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
source_root File.expand_path("../../templates", __FILE__)
|
8
|
+
|
9
|
+
desc "Creates a Leash initializer and route."
|
10
|
+
|
11
|
+
def copy_initializer
|
12
|
+
template "leash_provider.rb", "config/initializers/leash_provider.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def add_route
|
17
|
+
route "leash_provicer"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
Leash::Provider.configure do |config|
|
2
|
+
|
3
|
+
### ==> User classes
|
4
|
+
## Allowed user classes. Please create devise models with such names first.
|
5
|
+
## None by default so you have to set it to something meaningful in order
|
6
|
+
## to use Leash.
|
7
|
+
config.user_roles = [ "User", "Admin" ]
|
8
|
+
|
9
|
+
|
10
|
+
### ==> Redis
|
11
|
+
## Redis database address. Will use redis://localhost:6379/0 by default.
|
12
|
+
## Leash internally uses Ohm gem for encapsulating redis data into models.
|
13
|
+
## At the moment this setting will be shared with all Ohm models, so
|
14
|
+
## if you use any other Ohm models in the app and they need to use different
|
15
|
+
## database, you must hack the gem.
|
16
|
+
##
|
17
|
+
## If you need to initialize connection to redis database specified below,
|
18
|
+
## e.g. in unicorn or puma initializer please use the following syntax:
|
19
|
+
## Leash.establish_connection!
|
20
|
+
# config.redis_url = "redis://127.0.0.1:6379/0"
|
21
|
+
|
22
|
+
|
23
|
+
### ==> Access tokens
|
24
|
+
## Tells whether leash should reuse access tokens if there is already one
|
25
|
+
## present for application/owner combination. True by default.
|
26
|
+
# config.reuse_access_tokens = true
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module ActionDispatch::Routing
|
2
|
+
class Mapper
|
3
|
+
def leash_provider
|
4
|
+
scope :oauth do
|
5
|
+
get "authorize", to: "leash/provider/authorize#authorize", as: "leash_provider_authorize"
|
6
|
+
post "token", to: "leash/provider/token#token", as: "leash_provider_token"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "rails"
|
2
|
+
require "devise"
|
3
|
+
require "ohm"
|
4
|
+
require "leash/provider/version"
|
5
|
+
require "leash/provider/engine"
|
6
|
+
require "leash/provider/routing"
|
7
|
+
|
8
|
+
module Leash
|
9
|
+
module Provider
|
10
|
+
mattr_accessor :user_roles
|
11
|
+
@@user_roles = []
|
12
|
+
|
13
|
+
mattr_accessor :redis_url
|
14
|
+
@@redis_url = "redis://127.0.0.1:6379/0"
|
15
|
+
|
16
|
+
|
17
|
+
mattr_accessor :reuse_access_tokens
|
18
|
+
@@reuse_access_tokens = true
|
19
|
+
|
20
|
+
|
21
|
+
def self.configure
|
22
|
+
yield self
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def self.establish_connection!
|
27
|
+
Ohm.redis = Redic.new(@@redis_url)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Leash::Provider::AuthorizeController, :type => :controller do
|
4
|
+
before do
|
5
|
+
@OLD_ENV = ENV
|
6
|
+
ENV["APP_TEST_OAUTH2_CLIENT_ID"] = "123456"
|
7
|
+
ENV["APP_TEST_OAUTH2_SECRET"] = "qwerty"
|
8
|
+
ENV["APP_TEST_OAUTH2_REDIRECT_URL"] = "http://example.com" # TODO test multiple allowed redirect urls
|
9
|
+
|
10
|
+
allow(Leash::Provider).to receive(:user_roles).and_return([ valid_user_role ])
|
11
|
+
end
|
12
|
+
|
13
|
+
after do
|
14
|
+
(ENV.keys - @OLD_ENV.keys).each{ |key| ENV.delete(key) }
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:app_name) { "test" }
|
18
|
+
let(:valid_user_role) { "Admin" }
|
19
|
+
let(:valid_client_id) { ENV["APP_TEST_OAUTH2_CLIENT_ID"] }
|
20
|
+
let(:valid_redirect_uri) { ENV["APP_TEST_OAUTH2_REDIRECT_URL"] }
|
21
|
+
let(:invalid_user_role) { "Ufo" }
|
22
|
+
let(:invalid_client_id) { "098765" }
|
23
|
+
let(:invalid_redirect_uri) { "http://whatever.com" }
|
24
|
+
let(:authentication_route) { new_admin_session_path }
|
25
|
+
|
26
|
+
|
27
|
+
describe "GET authorize" do
|
28
|
+
context "for authorization code flow" do
|
29
|
+
let(:response_type) { "code" }
|
30
|
+
|
31
|
+
pending "without all necessary params"
|
32
|
+
|
33
|
+
context "with all necessary params" do
|
34
|
+
let(:params) { { user_role: user_role, response_type: response_type, client_id: client_id, redirect_uri: redirect_uri } }
|
35
|
+
|
36
|
+
context "with invalid client_id" do
|
37
|
+
let(:client_id) { invalid_client_id }
|
38
|
+
|
39
|
+
context "with valid redirect_uri" do
|
40
|
+
let(:redirect_uri) { valid_redirect_uri }
|
41
|
+
|
42
|
+
context "with valid user role" do
|
43
|
+
let(:user_role) { valid_user_role }
|
44
|
+
|
45
|
+
before do
|
46
|
+
get :authorize, params
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should return 422 status" do
|
50
|
+
expect(response.status).to eq 422
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should return 'unknown_client_id' in the response" do
|
54
|
+
expect(response.body).to eq "unknown_client_id"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "with valid client_id" do
|
61
|
+
let(:client_id) { valid_client_id }
|
62
|
+
|
63
|
+
context "with invalid redirect_uri" do
|
64
|
+
let(:redirect_uri) { invalid_redirect_uri }
|
65
|
+
|
66
|
+
context "with valid user role" do
|
67
|
+
let(:user_role) { valid_user_role }
|
68
|
+
|
69
|
+
before do
|
70
|
+
get :authorize, params
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should return 422 status" do
|
74
|
+
expect(response.status).to eq 422
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should return 'invalid_redirect_uri' in the response" do
|
78
|
+
expect(response.body).to eq "invalid_redirect_uri"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "with valid redirect_uri" do
|
84
|
+
let(:redirect_uri) { valid_redirect_uri }
|
85
|
+
|
86
|
+
context "but with unknown user_role" do
|
87
|
+
let(:user_role) { invalid_user_role }
|
88
|
+
|
89
|
+
before do
|
90
|
+
get :authorize, params
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should return 422 status" do
|
94
|
+
expect(response.status).to eq 422
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should return 'invalid_user_role' in the response" do
|
98
|
+
expect(response.body).to eq "invalid_user_role"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "with valid user role" do
|
103
|
+
let(:user_role) { valid_user_role }
|
104
|
+
|
105
|
+
|
106
|
+
before do
|
107
|
+
get :authorize, params
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
context "if not authenticated" do
|
112
|
+
it "should redirect to devise sign in screen for this user role" do
|
113
|
+
expect(response).to redirect_to(authentication_route)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "if authenticated" do
|
118
|
+
context "and there is already an access token for this app_name/owner combination" do
|
119
|
+
pending "should redirect to the redirect_uri specified in the app with appended '#access_token=(already present access token)'"
|
120
|
+
end
|
121
|
+
|
122
|
+
context "and there is are no access token for this app_name/owner combination" do
|
123
|
+
pending "should redirect to the redirect_uri specified in the app with appended '#access_token=(newly generated access token)'"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
context "for implicit flow" do
|
134
|
+
let(:response_type) { "token" }
|
135
|
+
|
136
|
+
pending "without all necessary params"
|
137
|
+
|
138
|
+
context "with all necessary params" do
|
139
|
+
let(:params) { { user_role: user_role, response_type: response_type, client_id: client_id, redirect_uri: redirect_uri } }
|
140
|
+
|
141
|
+
context "with invalid client_id" do
|
142
|
+
let(:client_id) { invalid_client_id }
|
143
|
+
|
144
|
+
context "with valid redirect_uri" do
|
145
|
+
let(:redirect_uri) { valid_redirect_uri }
|
146
|
+
|
147
|
+
context "with valid user role" do
|
148
|
+
let(:user_role) { valid_user_role }
|
149
|
+
|
150
|
+
before do
|
151
|
+
get :authorize, params
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should return 422 status" do
|
155
|
+
expect(response.status).to eq 422
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should return 'unknown_client_id' in the response" do
|
159
|
+
expect(response.body).to eq "unknown_client_id"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context "with valid client_id" do
|
166
|
+
let(:client_id) { valid_client_id }
|
167
|
+
|
168
|
+
context "with invalid redirect_uri" do
|
169
|
+
let(:redirect_uri) { invalid_redirect_uri }
|
170
|
+
|
171
|
+
context "with valid user role" do
|
172
|
+
let(:user_role) { valid_user_role }
|
173
|
+
|
174
|
+
before do
|
175
|
+
get :authorize, params
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should redirect to the first redirect_uri specified in the app with appended '#error=invalid_redirect_uri'" do
|
179
|
+
expect(response).to redirect_to("#{valid_redirect_uri}#error=invalid_redirect_uri")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context "with valid redirect_uri" do
|
185
|
+
let(:redirect_uri) { valid_redirect_uri }
|
186
|
+
|
187
|
+
context "but with unknown user_role" do
|
188
|
+
let(:user_role) { invalid_user_role }
|
189
|
+
|
190
|
+
before do
|
191
|
+
get :authorize, params
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should redirect to the first redirect_uri specified in the app with appended '#error=invalid_user_role'" do
|
195
|
+
expect(response).to redirect_to("#{valid_redirect_uri}#error=invalid_user_role")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
context "with valid user role" do
|
200
|
+
let(:user_role) { valid_user_role }
|
201
|
+
|
202
|
+
|
203
|
+
context "if not authenticated" do
|
204
|
+
before do
|
205
|
+
get :authorize, params
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should redirect to devise sign in screen for this user role" do
|
209
|
+
expect(response).to redirect_to(authentication_route)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
context "if authenticated" do
|
215
|
+
let(:admin) { create(:admin) }
|
216
|
+
|
217
|
+
context "and there is already an access token for this app_name/owner combination" do
|
218
|
+
context "and Leash::Provider.reuse_access_tokens is set to true" do
|
219
|
+
before do
|
220
|
+
expect(Leash::Provider).to receive(:reuse_access_tokens).and_return(true)
|
221
|
+
|
222
|
+
@existing_access_token = Leash::Provider::AccessToken.assign! app_name, admin
|
223
|
+
|
224
|
+
sign_in admin
|
225
|
+
get :authorize, params
|
226
|
+
end
|
227
|
+
|
228
|
+
it "should redirect to the redirect_uri specified in the params with appended '#access_token=(already present access token)'" do
|
229
|
+
expect(response).to redirect_to("#{valid_redirect_uri}#access_token=#{URI.encode(@existing_access_token)}")
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
context "and Leash::Provider.reuse_access_tokens is set to false" do
|
234
|
+
before do
|
235
|
+
expect(Leash::Provider).to receive(:reuse_access_tokens).and_return(false)
|
236
|
+
|
237
|
+
@existing_access_token = Leash::Provider::AccessToken.assign! app_name, admin
|
238
|
+
|
239
|
+
sign_in admin
|
240
|
+
get :authorize, params
|
241
|
+
end
|
242
|
+
|
243
|
+
it "should redirect to the redirect_uri specified in the params with appended '#access_token=(newly generated access token)'" do
|
244
|
+
expect(response).not_to redirect_to("#{valid_redirect_uri}#access_token=#{URI.encode(@existing_access_token)}")
|
245
|
+
expect(response).to redirect_to("#{valid_redirect_uri}#access_token=#{Leash::Provider::AccessToken.find_by_app_name_and_owner(app_name, admin).access_token}")
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
context "and there is are no access token for this app_name/owner combination" do
|
251
|
+
before do
|
252
|
+
sign_in admin
|
253
|
+
get :authorize, params
|
254
|
+
end
|
255
|
+
|
256
|
+
it "should redirect to the redirect_uri specified in the app with appended '#access_token=(newly generated access token)'" do
|
257
|
+
expect(response).to redirect_to("#{valid_redirect_uri}#access_token=#{URI.encode(Leash::Provider::AccessToken.find_by_app_name_and_owner(app_name, admin).access_token)}")
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|