devise_oauth 2.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.
- data/MIT-LICENSE +20 -0
- data/README.md +3 -0
- data/Rakefile +24 -0
- data/app/controllers/oauth/access_tokens_controller.rb +183 -0
- data/app/controllers/oauth/accesses_controller.rb +7 -0
- data/app/controllers/oauth/authorizations_controller.rb +84 -0
- data/app/controllers/oauth/clients_controller.rb +24 -0
- data/app/helpers/devise/oauth/helpers.rb +24 -0
- data/app/models/oauth/access.rb +19 -0
- data/app/models/oauth/access_token.rb +61 -0
- data/app/models/oauth/authorization.rb +58 -0
- data/app/models/oauth/client.rb +49 -0
- data/app/views/devise/oauth/authorizations/show.html.erb +34 -0
- data/config/routes.rb +14 -0
- data/db/migrate/20120622164619_devise_create_oauth.rb +76 -0
- data/lib/devise/models/access_token_authenticatable.rb +9 -0
- data/lib/devise/models/client_ownable.rb +14 -0
- data/lib/devise/models/resource_ownable.rb +36 -0
- data/lib/devise/oauth/blockable.rb +27 -0
- data/lib/devise/oauth/engine.rb +18 -0
- data/lib/devise/oauth/scopable.rb +43 -0
- data/lib/devise/oauth/version.rb +5 -0
- data/lib/devise/strategies/access_token_authenticatable.rb +69 -0
- data/lib/devise_oauth.rb +52 -0
- data/lib/tasks/devise_oauth_tasks.rake +4 -0
- metadata +154 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 Yury Korolev
|
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
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
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 = 'Devise::Oauth'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
Bundler::GemHelper.install_tasks
|
24
|
+
|
@@ -0,0 +1,183 @@
|
|
1
|
+
class Devise::Oauth::AccessTokensController < ApplicationController
|
2
|
+
|
3
|
+
include Devise::Oauth::Helpers
|
4
|
+
|
5
|
+
# create access_token flows
|
6
|
+
cattr_accessor :flows
|
7
|
+
@@flows = {
|
8
|
+
## section 4.1.3 (authorization code flow)
|
9
|
+
authorization_code: [
|
10
|
+
:find_auth_by_code,
|
11
|
+
:normalize_scope,
|
12
|
+
:access_blocked?,
|
13
|
+
:create_token_from_auth
|
14
|
+
],
|
15
|
+
|
16
|
+
## section 4.3.2 (Resource Owner Password Credentials flow)
|
17
|
+
password: [
|
18
|
+
:authenticate_resource_owner,
|
19
|
+
:normalize_scope,
|
20
|
+
:access_blocked?,
|
21
|
+
:create_token
|
22
|
+
],
|
23
|
+
# section 6.0 (refresh token)
|
24
|
+
refresh_token: [
|
25
|
+
:find_refresh_token,
|
26
|
+
:normalize_scope,
|
27
|
+
:access_blocked?,
|
28
|
+
:create_token_from_refresh
|
29
|
+
]
|
30
|
+
}
|
31
|
+
|
32
|
+
## common flow
|
33
|
+
before_filter :find_client, only: :create
|
34
|
+
before_filter :client_blocked?, only: :create
|
35
|
+
before_filter :find_grant_type, only: :create
|
36
|
+
before_filter :execute_flow, only: :create
|
37
|
+
|
38
|
+
def create
|
39
|
+
render json: @token_response
|
40
|
+
@authorization.used! if @authorization
|
41
|
+
end
|
42
|
+
|
43
|
+
def destroy
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def find_client
|
49
|
+
client_id, client_secret = request.authorization ?
|
50
|
+
decode_credentials : [params[:client_id], params[:client_secret]]
|
51
|
+
|
52
|
+
if client_id.blank? || client_secret.blank?
|
53
|
+
return invalid_request
|
54
|
+
end
|
55
|
+
|
56
|
+
@client = Devise::Oauth::Client.where(identifier: client_id).first
|
57
|
+
|
58
|
+
return client_not_found if @client.blank? || @client.secret != client_secret
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def find_grant_type
|
63
|
+
@grant_type = case params[:grant_type]
|
64
|
+
when "authorization_code"
|
65
|
+
:authorization_code
|
66
|
+
when "password"
|
67
|
+
:password
|
68
|
+
when "refresh_token"
|
69
|
+
:refresh_token
|
70
|
+
when "client_credentials"
|
71
|
+
:client_credentials
|
72
|
+
else
|
73
|
+
:unsupported
|
74
|
+
end
|
75
|
+
|
76
|
+
return invalid_grant_type(params[:grant_type]) if !Devise::Oauth.supported_grant_types.include?(@grant_type)
|
77
|
+
end
|
78
|
+
|
79
|
+
def execute_flow
|
80
|
+
flow = self.class.flows[@grant_type]
|
81
|
+
flow.each do |method|
|
82
|
+
break if response_body
|
83
|
+
send(method)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def resource_owner_credentials_flow?
|
88
|
+
@grant_type == :password
|
89
|
+
end
|
90
|
+
|
91
|
+
# tokens responses
|
92
|
+
|
93
|
+
def create_token_from_auth
|
94
|
+
@token_response = @authorization.create_access_token.token_response
|
95
|
+
end
|
96
|
+
|
97
|
+
def create_token_from_refresh
|
98
|
+
@token_response = @refresh_token.refresh!
|
99
|
+
end
|
100
|
+
|
101
|
+
def create_token
|
102
|
+
@token_response = Devise::Oauth::AccessToken.create(client: @client, resource_owner: @resource_owner, scope: @scope).token_response
|
103
|
+
end
|
104
|
+
|
105
|
+
def find_auth_by_code
|
106
|
+
code = params[:code]
|
107
|
+
|
108
|
+
@authorization = @client.authorizations.where(code: code).first
|
109
|
+
return auth_not_found if @authorization.blank?
|
110
|
+
return access_denied if @authorization.used?
|
111
|
+
return invalid_request if !@authorization.valid_redirect_uri?(params[:redirect_uri])
|
112
|
+
return auth_expired if @authorization.expired?
|
113
|
+
@resource_owner = @authorization.resource_owner
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_refresh_token
|
117
|
+
refresh_token = params[:refresh_token]
|
118
|
+
|
119
|
+
return invalid_request if refresh_token.blank?
|
120
|
+
|
121
|
+
@refresh_token = @client.access_tokens.where(refresh_token: params[:refresh_token]).first
|
122
|
+
|
123
|
+
return invalid_request if @refresh_token.blank?
|
124
|
+
@resource_owner = @refresh_token.resource_owner
|
125
|
+
end
|
126
|
+
|
127
|
+
def authenticate_resource_owner
|
128
|
+
owner_class = Devise::Oauth.resource_owner.constantize
|
129
|
+
owner = owner_class.find_for_authentication(owner_class.authentication_keys.first => params[:username])
|
130
|
+
if owner && owner.valid_password?(params[:password])
|
131
|
+
@resource_owner = owner
|
132
|
+
else
|
133
|
+
invalid_request
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
|
139
|
+
# errors
|
140
|
+
|
141
|
+
def auth_not_found
|
142
|
+
render_error :unprocessable_entity, error: "invalid_request", error_description: "Authorization not found"
|
143
|
+
end
|
144
|
+
|
145
|
+
def auth_expired
|
146
|
+
render_error :unprocessable_entity, error: "invalid_request", error_description: "Authorization expired"
|
147
|
+
end
|
148
|
+
|
149
|
+
def client_not_found
|
150
|
+
render_error :unprocessable_entity, error: "invalid_request", error_description: "Client not found"
|
151
|
+
end
|
152
|
+
|
153
|
+
def access_denied
|
154
|
+
render_error :unauthorized, error: "invalid_request"
|
155
|
+
end
|
156
|
+
|
157
|
+
def invalid_request
|
158
|
+
render_error :bad_request, error: "invalid_request"
|
159
|
+
end
|
160
|
+
|
161
|
+
def invalid_client
|
162
|
+
render_error :unauthorized, error: "invalid_client"
|
163
|
+
## TODO: check authorization
|
164
|
+
end
|
165
|
+
|
166
|
+
def blocked_client
|
167
|
+
render_error :unprocessable_entity, error: "invalid_request", error_description: "Client Blocked"
|
168
|
+
end
|
169
|
+
|
170
|
+
def blocked_token
|
171
|
+
render_error :unprocessable_entity, error: "invalid_request", error_description: "Client blocked from the user"
|
172
|
+
end
|
173
|
+
|
174
|
+
def invalid_grant
|
175
|
+
render_error :bad_request, error: "invalid_grant"
|
176
|
+
end
|
177
|
+
|
178
|
+
def render_error status, info
|
179
|
+
render json: info, status: status
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class Devise::Oauth::AuthorizationsController < ApplicationController
|
2
|
+
include Devise::Oauth::Helpers
|
3
|
+
|
4
|
+
before_filter :authenticate_user! # TODO: use devise scope here
|
5
|
+
before_filter :find_client
|
6
|
+
before_filter :find_resource_owner
|
7
|
+
before_filter :normalize_scope
|
8
|
+
# before_filter :check_scope # check if the access is authorized
|
9
|
+
before_filter :client_blocked? # check if the client is blocked
|
10
|
+
before_filter :access_blocked? # check if user has blocked the client
|
11
|
+
|
12
|
+
# before_filter :token_blocked?, only: :show # check for an existing token
|
13
|
+
# before_filter :refresh_token, only: :show # create a new token
|
14
|
+
|
15
|
+
def show
|
16
|
+
end
|
17
|
+
|
18
|
+
def create
|
19
|
+
@client.granted!
|
20
|
+
|
21
|
+
# section 4.1.1 - authorization code flow
|
22
|
+
if params[:response_type] == "code"
|
23
|
+
@authorization = Devise::Oauth::Authorization.create(client: @client, resource_owner: @resource_owner, scope: @scope)
|
24
|
+
redirect_to authorization_redirect_uri(@client, @authorization, params[:state])
|
25
|
+
end
|
26
|
+
|
27
|
+
# section 4.2.1 - implicit grant flow
|
28
|
+
if params[:response_type] == "token"
|
29
|
+
@token = Devise::Oauth::AccessToken.create(client: @client, resource_owner: @resource_owner, scope: scope)
|
30
|
+
redirect_to implicit_redirect_uri(@client, @token, params[:state])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def destroy
|
35
|
+
@client.revoked!
|
36
|
+
redirect_to deny_redirect_uri(params[:response_type], params[:state])
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def authorization_redirect_uri(client, authorization, state)
|
42
|
+
uri = client.redirect_uris.first
|
43
|
+
uri += "?code=" + authorization.code
|
44
|
+
uri += "&state=" + state if state
|
45
|
+
uri
|
46
|
+
end
|
47
|
+
|
48
|
+
def implicit_redirect_uri(client, token, state)
|
49
|
+
uri = client.redirect_uris.first
|
50
|
+
uri += "#token=" + token.token
|
51
|
+
uri += "&expires_in=" + Oauth.settings["token_expires_in"]
|
52
|
+
uri += "&state=" + state if state
|
53
|
+
return uri
|
54
|
+
end
|
55
|
+
|
56
|
+
def deny_redirect_uri(response_type, state)
|
57
|
+
uri = @client.redirect_uris.first
|
58
|
+
uri += (response_type == "code") ? "?" : "#"
|
59
|
+
uri += "error=access_denied"
|
60
|
+
uri += "&state=" + state if state
|
61
|
+
return uri
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_resource_owner
|
65
|
+
@resource_owner = current_user
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_client
|
69
|
+
client_id = params[:client_id]
|
70
|
+
|
71
|
+
@client = Devise::Oauth::Client.where(identifier: client_id).first
|
72
|
+
|
73
|
+
client_not_found if @client.blank?
|
74
|
+
end
|
75
|
+
|
76
|
+
def invalid_request
|
77
|
+
raise "sd"
|
78
|
+
end
|
79
|
+
|
80
|
+
def client_not_found
|
81
|
+
rails "bla"
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Devise::Oauth::ClientsController < ApplicationController
|
2
|
+
load_and_authorize_resource if respond_to? :load_and_authorize_resource
|
3
|
+
|
4
|
+
def index
|
5
|
+
end
|
6
|
+
|
7
|
+
def show
|
8
|
+
end
|
9
|
+
|
10
|
+
def update
|
11
|
+
end
|
12
|
+
|
13
|
+
def edit
|
14
|
+
end
|
15
|
+
|
16
|
+
def new
|
17
|
+
end
|
18
|
+
|
19
|
+
def create
|
20
|
+
end
|
21
|
+
|
22
|
+
def destroy
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Devise::Oauth::Helpers
|
2
|
+
|
3
|
+
def normalize_scope
|
4
|
+
scope = (params[:scope] || "").split(" ")
|
5
|
+
scope_mask = Devise::Oauth::AccessToken.scope_to_mask(scope)
|
6
|
+
@requested_scope = Devise::Oauth::AccessToken.mask_to_scope(scope_mask)
|
7
|
+
|
8
|
+
scope_mask = @client.scope_mask & scope_mask
|
9
|
+
scope_mask = @authorization.scope_mask & scope_mask if @authorization
|
10
|
+
scope_mask = @refresh_token.scope_mask & scope_mask if @refresh_token
|
11
|
+
|
12
|
+
@scope = Devise::Oauth::AccessToken.mask_to_scope(scope_mask)
|
13
|
+
end
|
14
|
+
|
15
|
+
def client_blocked?
|
16
|
+
blocked_client if @client.blocked?
|
17
|
+
end
|
18
|
+
|
19
|
+
def access_blocked?
|
20
|
+
@access = Devise::Oauth::Access.find_or_create_by_client_id_and_resource_owner_id(@client.id, @resource_owner.id)
|
21
|
+
blocked_token if @access.blocked?
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Devise::Oauth::Access < ActiveRecord::Base
|
2
|
+
belongs_to :client, class_name: "Devise::Oauth::Client"
|
3
|
+
belongs_to :resource_owner, class_name: Devise::Oauth.resource_owner
|
4
|
+
|
5
|
+
validates :client_id, presence: true
|
6
|
+
validates :resource_owner_id, presence: true
|
7
|
+
|
8
|
+
include Devise::Oauth::Blockable
|
9
|
+
|
10
|
+
def block!
|
11
|
+
super
|
12
|
+
Devise::Oauth::AccessToken.block_access!(client_id, resource_owner_id)
|
13
|
+
Devise::Oauth::Authorization.block_access!(client_id, resource_owner_id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def accessed!
|
17
|
+
self.class.update_counters(id, accessed_times: 1)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class Devise::Oauth::AccessToken < ActiveRecord::Base
|
2
|
+
belongs_to :client, class_name: "Devise::Oauth::Client"
|
3
|
+
belongs_to :resource_owner, class_name: Devise::Oauth.resource_owner
|
4
|
+
|
5
|
+
validates :client_id, presence: true
|
6
|
+
validates :resource_owner_id, presence: true
|
7
|
+
|
8
|
+
attr_accessible :client, :resource_owner, :scope
|
9
|
+
|
10
|
+
before_create :generate_refresh_token if Devise::Oauth.generate_refresh_token
|
11
|
+
|
12
|
+
before_create :generate_value
|
13
|
+
before_create :setup_expiration
|
14
|
+
|
15
|
+
include Devise::Oauth::Scopable
|
16
|
+
include Devise::Oauth::Blockable
|
17
|
+
|
18
|
+
def expired?(at = Time.now)
|
19
|
+
self.expires_at < at
|
20
|
+
end
|
21
|
+
|
22
|
+
def refresh!
|
23
|
+
generate_refresh_token if Devise::Oauth.regenerate_refresh_token
|
24
|
+
|
25
|
+
generate_value
|
26
|
+
setup_expiration
|
27
|
+
|
28
|
+
save
|
29
|
+
token_response(Devise::Oauth.regenerate_refresh_token)
|
30
|
+
end
|
31
|
+
|
32
|
+
def refresh_token_expired?
|
33
|
+
self.refresh_token_expires_at < Time.now
|
34
|
+
end
|
35
|
+
|
36
|
+
def token_response(generated_refresh_token=true)
|
37
|
+
res = {
|
38
|
+
access_token: value,
|
39
|
+
token_type: 'bearer'
|
40
|
+
}
|
41
|
+
res[:scope] = scope_to_response if scope.present?
|
42
|
+
res[:expires_in] = Devise::Oauth.access_token_expires_in if Devise::Oauth.access_token_expires_in
|
43
|
+
res[:refresh_token] = refresh_token if generated_refresh_token
|
44
|
+
res
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def generate_value
|
50
|
+
self.value = Devise.friendly_token
|
51
|
+
end
|
52
|
+
|
53
|
+
def setup_expiration
|
54
|
+
self.expires_at = Time.now + Devise::Oauth.access_token_expires_in
|
55
|
+
end
|
56
|
+
|
57
|
+
def generate_refresh_token
|
58
|
+
self.refresh_token = Devise::Oauth.friendly_token
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
class Devise::Oauth::Authorization < ActiveRecord::Base
|
2
|
+
belongs_to :client, class_name: "Devise::Oauth::Client"
|
3
|
+
belongs_to :resource_owner, class_name: Devise::Oauth.resource_owner
|
4
|
+
|
5
|
+
validates :client_id, presence: true
|
6
|
+
validates :resource_owner_id, presence: true
|
7
|
+
|
8
|
+
before_create :generate_code
|
9
|
+
before_create :create_expiration
|
10
|
+
|
11
|
+
include Devise::Oauth::Scopable
|
12
|
+
include Devise::Oauth::Blockable
|
13
|
+
|
14
|
+
attr_accessible :client, :resource_owner, :scope
|
15
|
+
|
16
|
+
def expired?(at = Time.now)
|
17
|
+
self.expires_at < at
|
18
|
+
end
|
19
|
+
|
20
|
+
def expire!(at = Time.now)
|
21
|
+
self.expires_at = at
|
22
|
+
save
|
23
|
+
end
|
24
|
+
|
25
|
+
def used!(at = Time.now)
|
26
|
+
self.used_at = at
|
27
|
+
save
|
28
|
+
# TODO: May be we should destroy it instead?
|
29
|
+
end
|
30
|
+
|
31
|
+
def used?
|
32
|
+
!!self.used_at
|
33
|
+
end
|
34
|
+
|
35
|
+
def valid_redirect_uri? uri
|
36
|
+
if redirect_uri.blank?
|
37
|
+
client.redirect_uris.include? uri
|
38
|
+
else
|
39
|
+
self.redirect_uri = uri
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_access_token
|
44
|
+
Devise::Oauth::AccessToken.create client: client, resource_owner: resource_owner, scope: scope
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def generate_code
|
50
|
+
self.code = Devise::Oauth.friendly_token
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_expiration
|
54
|
+
self.expires_at = Time.now + Devise::Oauth.authorization_code_expires_in
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Devise::Oauth
|
2
|
+
class Client < ActiveRecord::Base
|
3
|
+
def self.client_ownable?
|
4
|
+
Devise::Oauth.client_owner.constantize.devise_modules.include? :client_ownable
|
5
|
+
end
|
6
|
+
|
7
|
+
belongs_to :owner, class_name: Devise::Oauth.client_owner if self.client_ownable?
|
8
|
+
|
9
|
+
has_many :access_tokens, class_name: "Devise::Oauth::AccessToken", dependent: :destroy
|
10
|
+
has_many :authorizations, class_name: "Devise::Oauth::Authorization", dependent: :destroy
|
11
|
+
has_many :accesses, class_name: "Devise::Oauth::Access", dependent: :destroy
|
12
|
+
|
13
|
+
validates :name, presence: true
|
14
|
+
validates :owner_id, presence: true
|
15
|
+
validates :site_uri, presence: true
|
16
|
+
|
17
|
+
serialize :redirect_uris, Array
|
18
|
+
|
19
|
+
include Devise::Oauth::Scopable
|
20
|
+
include Devise::Oauth::Blockable
|
21
|
+
|
22
|
+
def block!
|
23
|
+
super
|
24
|
+
AccessToken.block_client! id
|
25
|
+
Authorization.block_client! id
|
26
|
+
end
|
27
|
+
|
28
|
+
before_create :generate_identifier
|
29
|
+
before_create :generate_secret
|
30
|
+
|
31
|
+
def granted!
|
32
|
+
self.class.update_counters(id, granted_times: 1)
|
33
|
+
end
|
34
|
+
|
35
|
+
def revoked!
|
36
|
+
self.class.update_counters(id, revoked_times: 1)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def generate_identifier
|
42
|
+
self.identifier = Devise::Oauth.friendly_token
|
43
|
+
end
|
44
|
+
|
45
|
+
def generate_secret
|
46
|
+
self.secret = Devise::Oauth.friendly_token
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<% unless flash.alert %>
|
2
|
+
<h2>Authorization request</h2>
|
3
|
+
<div>
|
4
|
+
<b>The application <em><%=@client.name%></em> would like to access your resources</b>
|
5
|
+
<div>You are giving access to <%= @scope.join(", ") %></div>
|
6
|
+
</div>
|
7
|
+
|
8
|
+
<div>
|
9
|
+
<form method="POST" action="/oauth/authorization">
|
10
|
+
<input type="hidden" name="response_type" value="<%=params[:response_type]%>">
|
11
|
+
<input type="hidden" name="client_id" value="<%=params[:client_id]%>">
|
12
|
+
<input type="hidden" name="redirect_uri" value="<%=params[:redirect_uri]%>">
|
13
|
+
<input type="hidden" name="scope" value="<%=@scope.join(" ")%>">
|
14
|
+
<% if params[:state] %>
|
15
|
+
<input type="hidden" name="state" value="<%=params[:state]%>">
|
16
|
+
<% end %>
|
17
|
+
<button>Grant Access</button>
|
18
|
+
</form>
|
19
|
+
</div>
|
20
|
+
|
21
|
+
<div>
|
22
|
+
<form method="POST" action="/oauth/authorization">
|
23
|
+
<input type="hidden" name="_method" value="delete">
|
24
|
+
<input type="hidden" name="response_type" value="<%=params[:response_type]%>">
|
25
|
+
<input type="hidden" name="client_id" value="<%=params[:client_id]%>">
|
26
|
+
<input type="hidden" name="redirect_uri" value="<%=params[:redirect_uri]%>">
|
27
|
+
<input type="hidden" name="scope" value="<%=@scope.join(" ")%>">
|
28
|
+
<% if params[:state] %>
|
29
|
+
<input type="hidden"name="state" value="<%=params[:state]%>">
|
30
|
+
<% end %>
|
31
|
+
<button>Deny Access</button>
|
32
|
+
</form>
|
33
|
+
</div>
|
34
|
+
<% end %>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Devise::Oauth::Engine.routes.draw do
|
2
|
+
resource :authorization, path: :authorize, only: [:create, :show, :destroy], defaults: {format: :html}
|
3
|
+
resource :access_token, path: :token, only: [:create, :destroy], defaults: {format: :json}
|
4
|
+
|
5
|
+
resources :clients do
|
6
|
+
put :block, on: :member
|
7
|
+
put :unblock, on: :member
|
8
|
+
end
|
9
|
+
|
10
|
+
resources :access, only: [:index, :show] do
|
11
|
+
put :block, on: :member
|
12
|
+
put :unblock, on: :member
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
class DeviseCreateOauth < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :oauth_clients do |t|
|
4
|
+
# client_ownable devise strategy
|
5
|
+
t.integer :owner_id
|
6
|
+
|
7
|
+
t.string :identifier, null: false
|
8
|
+
t.string :name, null: false
|
9
|
+
t.string :secret, null: false
|
10
|
+
t.string :site_uri, null: false
|
11
|
+
t.text :redirect_uris
|
12
|
+
t.text :info
|
13
|
+
t.integer :scope_mask, null: false, default: 0
|
14
|
+
|
15
|
+
t.integer :granted_times, null: false, default: 0
|
16
|
+
t.integer :revoked_times, null: false, default: 0
|
17
|
+
|
18
|
+
t.datetime :blocked_at
|
19
|
+
t.timestamps
|
20
|
+
end
|
21
|
+
|
22
|
+
add_index :oauth_clients, :identifier, unique: true
|
23
|
+
add_index :oauth_clients, :secret, unique: true
|
24
|
+
add_index :oauth_clients, :owner_id
|
25
|
+
|
26
|
+
create_table :oauth_authorizations do |t|
|
27
|
+
t.integer :client_id, null: false
|
28
|
+
t.integer :resource_owner_id, null: false
|
29
|
+
t.integer :scope_mask, null: false, default: 0
|
30
|
+
t.string :redirect_uri
|
31
|
+
t.string :code, null: false
|
32
|
+
t.datetime :expires_at, null: false
|
33
|
+
|
34
|
+
t.datetime :used_at
|
35
|
+
|
36
|
+
t.datetime :blocked_at
|
37
|
+
t.timestamps
|
38
|
+
end
|
39
|
+
|
40
|
+
add_index :oauth_authorizations, :client_id
|
41
|
+
add_index :oauth_authorizations, :resource_owner_id
|
42
|
+
add_index :oauth_authorizations, :code, unique: true
|
43
|
+
|
44
|
+
# for authorization and access tokens
|
45
|
+
create_table :oauth_access_tokens do |t|
|
46
|
+
t.integer :client_id, null: false
|
47
|
+
t.integer :resource_owner_id, null: false
|
48
|
+
t.integer :scope_mask, null: false, default: 0
|
49
|
+
t.string :value, null: false
|
50
|
+
t.datetime :expires_at, null: false
|
51
|
+
|
52
|
+
t.string :refresh_token
|
53
|
+
|
54
|
+
t.datetime :blocked_at
|
55
|
+
t.timestamps
|
56
|
+
end
|
57
|
+
|
58
|
+
add_index :oauth_access_tokens, :client_id
|
59
|
+
add_index :oauth_access_tokens, :resource_owner_id
|
60
|
+
add_index :oauth_access_tokens, :value, unique: true
|
61
|
+
add_index :oauth_access_tokens, :refresh_token, unique: true
|
62
|
+
|
63
|
+
create_table :oauth_accesses do |t|
|
64
|
+
t.integer :client_id, null: false
|
65
|
+
t.integer :resource_owner_id, null: false
|
66
|
+
|
67
|
+
t.integer :accessed_times, null: false, default: 0
|
68
|
+
|
69
|
+
t.datetime :blocked_at
|
70
|
+
t.timestamps
|
71
|
+
end
|
72
|
+
|
73
|
+
add_index :oauth_accesses, :client_id
|
74
|
+
add_index :oauth_accesses, :resource_owner_id
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Devise
|
2
|
+
module Models
|
3
|
+
module ResourceOwnable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
included do
|
6
|
+
|
7
|
+
has_many :oauth_access_tokens,
|
8
|
+
class_name: "Devise::Oauth::AccessToken",
|
9
|
+
foreign_key: "resource_owner_id",
|
10
|
+
dependent: :destroy
|
11
|
+
|
12
|
+
has_many :oauth_authorizations,
|
13
|
+
class_name: "Devise::Oauth::Authorization",
|
14
|
+
foreign_key: "resource_owner_id",
|
15
|
+
dependent: :destroy
|
16
|
+
|
17
|
+
has_many :oauth_accesses,
|
18
|
+
class_name: "Devise::Oauth::Access",
|
19
|
+
foreign_key: "resource_owner_id",
|
20
|
+
dependent: :destroy
|
21
|
+
|
22
|
+
attr_accessor :oauth_token
|
23
|
+
end
|
24
|
+
|
25
|
+
def oauth_token?
|
26
|
+
oauth_token.present?
|
27
|
+
end
|
28
|
+
|
29
|
+
def oauth_scope? *scope
|
30
|
+
return false if oauth_token.nil?
|
31
|
+
|
32
|
+
oauth_token.has_scope? scope
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Devise::Oauth::Blockable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def block!(at = Time.now)
|
5
|
+
self.blocked_at = at
|
6
|
+
save
|
7
|
+
end
|
8
|
+
|
9
|
+
def blocked?
|
10
|
+
blocked_at.present?
|
11
|
+
end
|
12
|
+
|
13
|
+
def unblock!
|
14
|
+
self.blocked_at = nil
|
15
|
+
save
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def block_access!(client_id, resource_owner_id)
|
20
|
+
update_all({ blocked_at: Time.now }, { client_id: client_id, resource_owner_id: resource_owner_id })
|
21
|
+
end
|
22
|
+
|
23
|
+
def block_client!(client_id)
|
24
|
+
update_all({ blocked_at: Time.now }, { client_id: client_id })
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Devise::Oauth
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
def table_name_prefix
|
4
|
+
"oauth"
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.generate_railtie_name(mod)
|
8
|
+
"oauth"
|
9
|
+
end
|
10
|
+
|
11
|
+
isolate_namespace Devise::Oauth
|
12
|
+
|
13
|
+
initializer "devise_oauth.initialize_application", before: :load_config_initializers do |app|
|
14
|
+
app.config.filter_parameters << :client_secret
|
15
|
+
app.config.filter_parameters << :refresh_token
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Devise::Oauth::Scopable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def scope=(scope)
|
5
|
+
self.scope_mask = self.class.scope_to_mask(scope)
|
6
|
+
end
|
7
|
+
|
8
|
+
def scope
|
9
|
+
self.class.mask_to_scope(scope_mask)
|
10
|
+
end
|
11
|
+
|
12
|
+
def has_scope?(scope)
|
13
|
+
self.scope_mask & self.class.scope_to_mask(scope) > 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def scope_to_response
|
17
|
+
scope.join(" ")
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def scopes
|
22
|
+
@@scopes ||= Devise::Oauth.scopes.map {|s| s.to_s}
|
23
|
+
end
|
24
|
+
|
25
|
+
def scope_to_mask(scope=[])
|
26
|
+
return 0 if scope.blank?
|
27
|
+
(scope.map(&:to_s) & scopes).map { |r| 2**scopes.index(r) }.sum
|
28
|
+
end
|
29
|
+
|
30
|
+
def mask_to_scope(mask)
|
31
|
+
return [] if mask == 0
|
32
|
+
scopes.reject {|r| (mask & 2**scopes.index(r)).zero? }
|
33
|
+
end
|
34
|
+
|
35
|
+
def where_scope(scope=[])
|
36
|
+
if scope.blank?
|
37
|
+
where "scope_mask = 0"
|
38
|
+
else
|
39
|
+
where "scope_mask & ? > 0", scope_to_mask(scope)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'devise/strategies/base'
|
2
|
+
module Devise
|
3
|
+
module Strategies
|
4
|
+
class AccessTokenAuthenticatable < Authenticatable
|
5
|
+
def store?
|
6
|
+
false # no no for session here
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
@access_tokens = [access_token_in_header, access_token_in_payload].compact
|
11
|
+
@access_tokens.present?
|
12
|
+
end
|
13
|
+
|
14
|
+
def authenticate!
|
15
|
+
return oauth_error! if @access_tokens.length > 1
|
16
|
+
|
17
|
+
access_token = Devise::Oauth::AccessToken.where(value: @access_tokens.first).first
|
18
|
+
|
19
|
+
return oauth_error!(403, :access_denied) unless access_token
|
20
|
+
return oauth_error!(403, :access_denied) if access_token.expired?
|
21
|
+
|
22
|
+
resource = access_token.resource_owner
|
23
|
+
if validate(resource)
|
24
|
+
env["devise.oauth.access_token"] = access_token
|
25
|
+
resource.oauth_token = access_token
|
26
|
+
success!(resource)
|
27
|
+
else
|
28
|
+
oauth_error!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def oauth_error!(status = 400, error_code = :invalid_request, description = nil)
|
34
|
+
body = {error: error_code}
|
35
|
+
body[:error_description] = description if description
|
36
|
+
|
37
|
+
headers = {"Content-Type" => "application/json; charset=utf-8"}
|
38
|
+
|
39
|
+
custom! [status, headers, [body.to_json]]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Access Token Authenticatable can be authenticated with params in any controller and any verb.
|
43
|
+
def valid_params_request?
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
# Do not use remember_me behavior with token.
|
48
|
+
def remember_me?
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
def access_token_in_payload
|
53
|
+
params['access_token']
|
54
|
+
end
|
55
|
+
|
56
|
+
def access_token_in_header
|
57
|
+
auth_header = ::Rack::Auth::AbstractRequest.new(env)
|
58
|
+
if auth_header.provided? && auth_header.scheme == :bearer
|
59
|
+
auth_header.params
|
60
|
+
else
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
Warden::Strategies.add(:access_token_authenticatable, Devise::Strategies::AccessTokenAuthenticatable)
|
data/lib/devise_oauth.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
require 'devise'
|
3
|
+
|
4
|
+
module Devise
|
5
|
+
module Oauth
|
6
|
+
mattr_accessor :resource_owner
|
7
|
+
@@resource_owner = "User"
|
8
|
+
|
9
|
+
mattr_accessor :client_owner
|
10
|
+
@@client_owner = self.resource_owner
|
11
|
+
|
12
|
+
mattr_accessor :scopes
|
13
|
+
@@scopes = []
|
14
|
+
|
15
|
+
mattr_accessor :access_token_expires_in
|
16
|
+
@@access_token_expires_in = 1.hour
|
17
|
+
|
18
|
+
mattr_accessor :authorization_code_expires_in
|
19
|
+
@@authorization_code_expires_in = 1.minute
|
20
|
+
|
21
|
+
mattr_accessor :generate_refresh_token
|
22
|
+
@@generate_refresh_token = true
|
23
|
+
|
24
|
+
mattr_accessor :regenerate_refresh_token
|
25
|
+
@@regenerate_refresh_token = true
|
26
|
+
|
27
|
+
mattr_accessor :supported_grant_types
|
28
|
+
@@supported_grant_types = [:authorization_code, :password, :refresh_token]
|
29
|
+
|
30
|
+
def self.friendly_token(length = 20)
|
31
|
+
SecureRandom.base64(length).tr('+/=lIO0', 'pqrsxyz')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
require "devise/oauth/scopable"
|
37
|
+
require "devise/oauth/blockable"
|
38
|
+
|
39
|
+
require "devise/models/client_ownable"
|
40
|
+
require "devise/models/resource_ownable"
|
41
|
+
|
42
|
+
require "devise/strategies/access_token_authenticatable"
|
43
|
+
require "devise/models/access_token_authenticatable"
|
44
|
+
|
45
|
+
|
46
|
+
require "devise/oauth/engine"
|
47
|
+
|
48
|
+
Devise.add_module(
|
49
|
+
:access_token_authenticatable,
|
50
|
+
:strategy => true,
|
51
|
+
:model => 'devise/models/access_token_authenticatable'
|
52
|
+
)
|
metadata
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: devise_oauth
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Yury Korolev
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: &70245897777140 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70245897777140
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: devise
|
27
|
+
requirement: &70245897776200 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.1'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70245897776200
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: sqlite3
|
38
|
+
requirement: &70245897775620 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70245897775620
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: factory_girl_rails
|
49
|
+
requirement: &70245897774900 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70245897774900
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rspec-rails
|
60
|
+
requirement: &70245897773960 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '2.0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70245897773960
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: database_cleaner
|
71
|
+
requirement: &70245897773500 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70245897773500
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: shoulda-matchers
|
82
|
+
requirement: &70245897772840 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70245897772840
|
91
|
+
description: The OAuth 2.0 Authorization Framework draft-ietf-oauth-v2-28 implementation
|
92
|
+
on top of devise.
|
93
|
+
email:
|
94
|
+
- yury.korolev@gmail.com
|
95
|
+
executables: []
|
96
|
+
extensions: []
|
97
|
+
extra_rdoc_files: []
|
98
|
+
files:
|
99
|
+
- app/controllers/oauth/access_tokens_controller.rb
|
100
|
+
- app/controllers/oauth/accesses_controller.rb
|
101
|
+
- app/controllers/oauth/authorizations_controller.rb
|
102
|
+
- app/controllers/oauth/clients_controller.rb
|
103
|
+
- app/helpers/devise/oauth/helpers.rb
|
104
|
+
- app/models/oauth/access.rb
|
105
|
+
- app/models/oauth/access_token.rb
|
106
|
+
- app/models/oauth/authorization.rb
|
107
|
+
- app/models/oauth/client.rb
|
108
|
+
- app/views/devise/oauth/authorizations/show.html.erb
|
109
|
+
- config/routes.rb
|
110
|
+
- db/migrate/20120622164619_devise_create_oauth.rb
|
111
|
+
- lib/devise/models/access_token_authenticatable.rb
|
112
|
+
- lib/devise/models/client_ownable.rb
|
113
|
+
- lib/devise/models/resource_ownable.rb
|
114
|
+
- lib/devise/oauth/blockable.rb
|
115
|
+
- lib/devise/oauth/engine.rb
|
116
|
+
- lib/devise/oauth/scopable.rb
|
117
|
+
- lib/devise/oauth/version.rb
|
118
|
+
- lib/devise/strategies/access_token_authenticatable.rb
|
119
|
+
- lib/devise_oauth.rb
|
120
|
+
- lib/tasks/devise_oauth_tasks.rake
|
121
|
+
- MIT-LICENSE
|
122
|
+
- Rakefile
|
123
|
+
- README.md
|
124
|
+
homepage: https://github.com/anjlab/devise_oauth
|
125
|
+
licenses: []
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
none: false
|
132
|
+
requirements:
|
133
|
+
- - ! '>='
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
segments:
|
137
|
+
- 0
|
138
|
+
hash: -2188440318074512588
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
|
+
none: false
|
141
|
+
requirements:
|
142
|
+
- - ! '>='
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
segments:
|
146
|
+
- 0
|
147
|
+
hash: -2188440318074512588
|
148
|
+
requirements: []
|
149
|
+
rubyforge_project:
|
150
|
+
rubygems_version: 1.8.17
|
151
|
+
signing_key:
|
152
|
+
specification_version: 3
|
153
|
+
summary: Oauth 2.0 provider implementation on top of devise.
|
154
|
+
test_files: []
|