haveapi 0.16.3 → 0.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/haveapi.gemspec +6 -5
- data/lib/haveapi/authentication/base.rb +10 -0
- data/lib/haveapi/authentication/chain.rb +2 -0
- data/lib/haveapi/authentication/oauth2/config.rb +143 -0
- data/lib/haveapi/authentication/oauth2/provider.rb +231 -0
- data/lib/haveapi/authentication/oauth2.rb +9 -0
- data/lib/haveapi/client_example.rb +23 -0
- data/lib/haveapi/client_examples/curl.rb +5 -2
- data/lib/haveapi/client_examples/fs_client.rb +3 -0
- data/lib/haveapi/client_examples/http.rb +22 -1
- data/lib/haveapi/client_examples/js_client.rb +5 -3
- data/lib/haveapi/client_examples/php_client.rb +5 -2
- data/lib/haveapi/client_examples/ruby_cli.rb +6 -3
- data/lib/haveapi/client_examples/ruby_client.rb +5 -2
- data/lib/haveapi/server.rb +7 -0
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/version_page/auth_body.erb +7 -0
- metadata +29 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fedb085c210494a3173ef597e44f63a280485cc0490bd0c0b74610d26d52e5b4
|
4
|
+
data.tar.gz: 372f4a9cb544426ad1e06c3bb60e26a50077b5e3bef70e7b77ac008ca5008aaa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ab713fd64ead24d6a21095bda69ddbc536da56fdd26745c0c60721a8c9e5767addafdf7906a3c98a23401472106e83f8d341903291e4c5e3b1c08974c3f12b3
|
7
|
+
data.tar.gz: 58b3cfc406d09868675f07b51050c5b914368c1c4ee70dbd9cf919a6f2b7e9124133e0f6f33292822f69e614cd1090e6f605ba5179d0c98802d4b1023e09cc6e
|
data/haveapi.gemspec
CHANGED
@@ -16,13 +16,14 @@ Gem::Specification.new do |s|
|
|
16
16
|
|
17
17
|
s.add_runtime_dependency 'require_all', '~> 2.0.0'
|
18
18
|
s.add_runtime_dependency 'json'
|
19
|
-
s.add_runtime_dependency 'activesupport', '>=
|
20
|
-
s.add_runtime_dependency 'sinatra', '~> 3.0
|
21
|
-
s.add_runtime_dependency 'tilt', '~> 2.0
|
22
|
-
s.add_runtime_dependency 'redcarpet', '~> 3.
|
19
|
+
s.add_runtime_dependency 'activesupport', '>= 7.0'
|
20
|
+
s.add_runtime_dependency 'sinatra', '~> 3.1.0'
|
21
|
+
s.add_runtime_dependency 'tilt', '~> 2.3.0'
|
22
|
+
s.add_runtime_dependency 'redcarpet', '~> 3.6'
|
23
23
|
s.add_runtime_dependency 'rake'
|
24
24
|
s.add_runtime_dependency 'github-markdown'
|
25
25
|
s.add_runtime_dependency 'nesty', '~> 1.0'
|
26
|
-
s.add_runtime_dependency 'haveapi-client', '~> 0.
|
26
|
+
s.add_runtime_dependency 'haveapi-client', '~> 0.18.0'
|
27
27
|
s.add_runtime_dependency 'mail'
|
28
|
+
s.add_runtime_dependency 'rack-oauth2', '~> 2.2.0'
|
28
29
|
end
|
@@ -13,6 +13,10 @@ module HaveAPI
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
+
def self.inherited(subclass)
|
17
|
+
subclass.send(:instance_variable_set, '@auth_method', @auth_method)
|
18
|
+
end
|
19
|
+
|
16
20
|
# @return [Symbol]
|
17
21
|
attr_reader :name
|
18
22
|
|
@@ -23,6 +27,12 @@ module HaveAPI
|
|
23
27
|
setup
|
24
28
|
end
|
25
29
|
|
30
|
+
# Register custom path handlers in sinatra
|
31
|
+
# @param sinatra [Sinatra::Base]
|
32
|
+
# @param prefix [String]
|
33
|
+
def register_routes(sinatra, prefix)
|
34
|
+
end
|
35
|
+
|
26
36
|
# @return [Module, nil]
|
27
37
|
def resource_module
|
28
38
|
nil
|
@@ -99,6 +99,8 @@ module HaveAPI::Authentication
|
|
99
99
|
instance = p.new(@server, v)
|
100
100
|
@instances[v] << instance
|
101
101
|
|
102
|
+
@server.add_auth_routes(v, instance, prefix: instance.name.to_s)
|
103
|
+
|
102
104
|
if resource_module = instance.resource_module
|
103
105
|
@server.add_auth_module(
|
104
106
|
v,
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module HaveAPI::Authentication
|
2
|
+
module OAuth2
|
3
|
+
# Config passed to the OAuth2 provider
|
4
|
+
#
|
5
|
+
# Create your own subclass and pass it to {HaveAPI::Authentication::OAuth2.with_config}.
|
6
|
+
# The created provider can then be added to authentication chain.
|
7
|
+
#
|
8
|
+
# In general, it is up to the implementation to provide the authentication flow
|
9
|
+
# -- render HTML page in {#render_authorize_page} and then process it in
|
10
|
+
# {#handle_post_authorize}. The implementation must also handle generation
|
11
|
+
# of all needed tokens, their persistence and validity checking.
|
12
|
+
class Config
|
13
|
+
def initialize(provider, server, v)
|
14
|
+
@provider = provider
|
15
|
+
@server = server
|
16
|
+
@version = v
|
17
|
+
end
|
18
|
+
|
19
|
+
# Render authorization page
|
20
|
+
#
|
21
|
+
# This method can be called on both GET and POST requests, e.g. if the user
|
22
|
+
# provided incorrect credentials or if there are multiple authentication
|
23
|
+
# steps.
|
24
|
+
#
|
25
|
+
# It should return full HTML page that will be sent to the user. The page
|
26
|
+
# usually contains a login form.
|
27
|
+
#
|
28
|
+
# @param oauth2_request [Rack::OAuth2::Server::Authorize::Request]
|
29
|
+
# @param sinatra_params [Hash] request params
|
30
|
+
# @param client [Client]
|
31
|
+
# @param auth_result [AuthResult, nil]
|
32
|
+
# @return [String] HTML
|
33
|
+
def render_authorize_page(oauth2_request, sinatra_params, client, auth_result: nil)
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
# Handle POST requests made from {#render_authorize_page}
|
38
|
+
#
|
39
|
+
# Process form data and return {AuthResult} or nil. When nil is returned
|
40
|
+
# the authorization process is aborted and the user is redirected back
|
41
|
+
# to the client.
|
42
|
+
#
|
43
|
+
# @param sinatra_request [Sinatra::Request]
|
44
|
+
# @param sinatra_params [Hash] request params
|
45
|
+
# @param oauth2_request [Rack::OAuth2::Server::Authorize::Request]
|
46
|
+
# @param client [Client]
|
47
|
+
# @return [AuthResult, nil]
|
48
|
+
def handle_post_authorize(sinatra_request, sinatra_params, oauth2_request, client)
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get oauth2 authorization code
|
53
|
+
#
|
54
|
+
# Called when the authentication is successful and complete. This method
|
55
|
+
# must generate and return authorization_code which is then sent to the
|
56
|
+
# client. It is up to the API implementation to persist the code.
|
57
|
+
#
|
58
|
+
# @param auth_res [AuthResult] value returned by {#handle_post_authorize}
|
59
|
+
# @return [String]
|
60
|
+
def get_authorization_code(auth_res)
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
# Get access token, its expiration date and optionally a refresh token
|
65
|
+
#
|
66
|
+
# The client has used the authorization_code returned by {#get_authorization_code}
|
67
|
+
# and now requests its access token. It is up to the implementation to create
|
68
|
+
# and persist the tokens. The authorization code should be invalidated.
|
69
|
+
#
|
70
|
+
# @param authorization [Authorization]
|
71
|
+
# @param sinatra_request [Sinatra::Request]
|
72
|
+
# @return [Array] access token, expiration date and optional refresh token
|
73
|
+
def get_tokens(authorization, sinatra_request)
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
# Refresh access token and optionally generate new refresh token
|
78
|
+
#
|
79
|
+
# The implementation should invalidate the current tokens and generate
|
80
|
+
# and persist new ones.
|
81
|
+
#
|
82
|
+
# @param authorization [Authorization]
|
83
|
+
# @param sinatra_request [Sinatra::Request]
|
84
|
+
# @return [Array] access token, expiration date and optional refresh token
|
85
|
+
def refresh_tokens(authorization, sinatra_request)
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
# Find client by ID
|
90
|
+
# @param client_id [String]
|
91
|
+
# @return [Client, nil]
|
92
|
+
def find_client_by_id(client_id)
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
# Find authorization by code
|
97
|
+
# @param client [Client]
|
98
|
+
# @param code [String]
|
99
|
+
# @return [Authorization, nil]
|
100
|
+
def find_authorization_by_code(client, code)
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
# Find authorization by refresh token
|
105
|
+
# @param client [Client]
|
106
|
+
# @param refresh_token [String]
|
107
|
+
# @return [Authorization, nil]
|
108
|
+
def find_authorization_by_refresh_token(client, refresh_token)
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
# Find user by the bearer token sent in HTTP header or as a query parameter
|
113
|
+
# @param sinatra_request [Sinatra::Request]
|
114
|
+
# @param access_token [String]
|
115
|
+
# @return [Object, nil] user
|
116
|
+
def find_user_by_access_token(request, access_token)
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
# Path to the authorization endpoint on this API
|
121
|
+
# @return [String]
|
122
|
+
def authorize_path
|
123
|
+
@provider.authorize_path
|
124
|
+
end
|
125
|
+
|
126
|
+
# Parameters needed for the authorization process
|
127
|
+
#
|
128
|
+
# Use these in {#render_authorization_page}, put them e.g. in hidden form
|
129
|
+
# fields.
|
130
|
+
#
|
131
|
+
# @return [Hash<String, String>]
|
132
|
+
def oauth2_params(req)
|
133
|
+
{
|
134
|
+
client_id: req.client_id,
|
135
|
+
response_type: req.response_type,
|
136
|
+
redirect_uri: req.redirect_uri,
|
137
|
+
scope: req.scope.join(' '),
|
138
|
+
state: req.state,
|
139
|
+
}
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'haveapi/authentication/base'
|
2
|
+
require 'rack/oauth2'
|
3
|
+
|
4
|
+
module HaveAPI::Authentication
|
5
|
+
module OAuth2
|
6
|
+
# Abstract class describing the client and what methods it must respond to
|
7
|
+
class Client
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :client_id
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :redirect_uri
|
13
|
+
|
14
|
+
# @param client_secret [String]
|
15
|
+
# @return [Boolean]
|
16
|
+
def check_secret(client_secret)
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Abstract class describing the authentication result and what methods it must respond to
|
22
|
+
class AuthResult
|
23
|
+
# True of the user was authenticated
|
24
|
+
# @return [Boolean]
|
25
|
+
attr_reader :authenticated
|
26
|
+
|
27
|
+
# True if the authentication process is complete, false if other steps are needed
|
28
|
+
# @return [Boolean]
|
29
|
+
attr_reader :complete
|
30
|
+
|
31
|
+
# True if the user asked to cancel the authorization process
|
32
|
+
# @return [Boolean]
|
33
|
+
attr_reader :cancel
|
34
|
+
end
|
35
|
+
|
36
|
+
# Abstract class describing ongoing authorization and what methods it must respond to
|
37
|
+
class Authorization
|
38
|
+
# @return [String]
|
39
|
+
attr_reader :redirect_uri
|
40
|
+
|
41
|
+
# @param redirect_uri [String]
|
42
|
+
# @return [Boolean]
|
43
|
+
def check_code_validity(redirect_uri)
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# OAuth2 authentication and authorization provider
|
49
|
+
#
|
50
|
+
# Must be configured with {Config} using {OAuth2.with_config}.
|
51
|
+
class Provider < Base
|
52
|
+
auth_method :oauth2
|
53
|
+
|
54
|
+
# Configure the OAuth2 provider
|
55
|
+
# @param cfg [Config]
|
56
|
+
def self.with_config(cfg)
|
57
|
+
Module.new do
|
58
|
+
define_singleton_method(:new) do |*args|
|
59
|
+
Provider.new(*args, cfg)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [String]
|
65
|
+
attr_reader :authorize_path
|
66
|
+
|
67
|
+
# @return [Config]
|
68
|
+
attr_reader :config
|
69
|
+
|
70
|
+
def initialize(server, v, cfg)
|
71
|
+
@config = cfg.new(self, server, v)
|
72
|
+
super(server, v)
|
73
|
+
end
|
74
|
+
|
75
|
+
def register_routes(sinatra, prefix)
|
76
|
+
@authorize_path = File.join(prefix, 'authorize')
|
77
|
+
@token_path = File.join(prefix, 'token')
|
78
|
+
that = self
|
79
|
+
|
80
|
+
sinatra.get @authorize_path do
|
81
|
+
that.authorization_endpoint(self).call(request.env)
|
82
|
+
end
|
83
|
+
|
84
|
+
sinatra.post @authorize_path do
|
85
|
+
that.authorization_endpoint(self).call(request.env)
|
86
|
+
end
|
87
|
+
|
88
|
+
sinatra.post @token_path do
|
89
|
+
that.token_endpoint(self).call(request.env)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def authenticate(request)
|
94
|
+
tokens = [
|
95
|
+
request['access_token'],
|
96
|
+
token_from_header(request)
|
97
|
+
].compact
|
98
|
+
|
99
|
+
token =
|
100
|
+
case tokens.length
|
101
|
+
when 0
|
102
|
+
nil
|
103
|
+
when 1
|
104
|
+
tokens.first
|
105
|
+
else
|
106
|
+
fail 'Too many oauth2 tokens'
|
107
|
+
end
|
108
|
+
|
109
|
+
token && config.find_user_by_access_token(request, token)
|
110
|
+
end
|
111
|
+
|
112
|
+
def token_from_header(request)
|
113
|
+
auth_header = Rack::Auth::AbstractRequest.new(request.env)
|
114
|
+
|
115
|
+
if auth_header.provided? && !auth_header.parts.first.nil? && auth_header.scheme.to_s == 'bearer'
|
116
|
+
auth_header.params
|
117
|
+
else
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def describe
|
123
|
+
desc = <<-END
|
124
|
+
OAuth2 authorization provider. While OAuth2 is not supported by HaveAPI
|
125
|
+
clients, it is possible to use your API as an authentication source.
|
126
|
+
|
127
|
+
HaveAPI partially implements RFC 6749: authorization response type "code"
|
128
|
+
and token grant types "authorization_code" and "refresh_token". Other
|
129
|
+
response and grant types are not supported at this time.
|
130
|
+
|
131
|
+
The access token can be passed as bearer token according to RFC 6750.
|
132
|
+
END
|
133
|
+
|
134
|
+
{
|
135
|
+
description: desc,
|
136
|
+
authorize_path: @authorize_path,
|
137
|
+
token_path: @token_path,
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def authorization_endpoint(handler)
|
142
|
+
Rack::OAuth2::Server::Authorize.new do |req, res|
|
143
|
+
client = config.find_client_by_id(req.client_id)
|
144
|
+
req.bad_request! if client.nil?
|
145
|
+
|
146
|
+
res.redirect_uri = req.verify_redirect_uri!(client.redirect_uri)
|
147
|
+
|
148
|
+
if req.post?
|
149
|
+
auth_res = config.handle_post_authorize(handler.request, handler.params, req, client)
|
150
|
+
|
151
|
+
if auth_res.nil?
|
152
|
+
# Authentication failed
|
153
|
+
req.access_denied!
|
154
|
+
elsif auth_res.cancel
|
155
|
+
# Cancel the process
|
156
|
+
req.access_denied!
|
157
|
+
elsif auth_res.authenticated && auth_res.complete
|
158
|
+
# Authentication was successful
|
159
|
+
case req.response_type
|
160
|
+
when :code
|
161
|
+
res.code = config.get_authorization_code(auth_res)
|
162
|
+
when :token
|
163
|
+
req.unsupported_response_type!
|
164
|
+
end
|
165
|
+
|
166
|
+
res.approve!
|
167
|
+
elsif auth_res.authenticated && !auth_res.complete
|
168
|
+
# Continue with another authentication step
|
169
|
+
res.content_type = 'text/html'
|
170
|
+
res.write(config.render_authorize_page(req, handler.params, client, auth_result: auth_res))
|
171
|
+
else
|
172
|
+
# Authentication failed, report errors and let the user retry
|
173
|
+
res.content_type = 'text/html'
|
174
|
+
res.write(config.render_authorize_page(req, handler.params, client, auth_result: auth_res))
|
175
|
+
end
|
176
|
+
else
|
177
|
+
res.content_type = 'text/html'
|
178
|
+
res.write(config.render_authorize_page(req, handler.params, client))
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def token_endpoint(handler)
|
184
|
+
Rack::OAuth2::Server::Token.new do |req, res|
|
185
|
+
client = config.find_client_by_id(req.client_id)
|
186
|
+
req.invalid_client! if client.nil? || !client.check_secret(req.client_secret)
|
187
|
+
|
188
|
+
res.access_token =
|
189
|
+
case req.grant_type
|
190
|
+
when :authorization_code
|
191
|
+
authorization = config.find_authorization_by_code(client, req.code)
|
192
|
+
|
193
|
+
if authorization.nil? || authorization.check_code_validity(req.redirect_uri)
|
194
|
+
req.invalid_grant!
|
195
|
+
end
|
196
|
+
|
197
|
+
access_token, expires_at, refresh_token = config.get_tokens(authorization, handler.request)
|
198
|
+
|
199
|
+
bearer_token = Rack::OAuth2::AccessToken::Bearer.new(
|
200
|
+
access_token: access_token,
|
201
|
+
expires_in: expires_at - Time.now,
|
202
|
+
)
|
203
|
+
bearer_token.refresh_token = refresh_token if refresh_token
|
204
|
+
bearer_token
|
205
|
+
|
206
|
+
when :password
|
207
|
+
req.unsupported_grant_type!
|
208
|
+
|
209
|
+
when :client_credentials
|
210
|
+
req.unsupported_grant_type!
|
211
|
+
|
212
|
+
when :refresh_token
|
213
|
+
config.find_authorization_by_refresh_token(client, req.refresh_token)
|
214
|
+
|
215
|
+
access_token, expires_at, refresh_token = config.refresh_tokens(authorization, handler.request)
|
216
|
+
|
217
|
+
bearer_token = Rack::OAuth2::AccessToken::Bearer.new(
|
218
|
+
access_token: access_token,
|
219
|
+
expires_in: expires_at - Time.now,
|
220
|
+
)
|
221
|
+
bearer_token.refresh_token = refresh_token if refresh_token
|
222
|
+
bearer_token
|
223
|
+
|
224
|
+
else
|
225
|
+
req.unsupported_grant_type!
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -79,5 +79,28 @@ module HaveAPI
|
|
79
79
|
def version_url
|
80
80
|
File.join(base_url, "v#{version}", '/')
|
81
81
|
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
# @param password [Boolean] include password parameter
|
85
|
+
# @return [Hash<String, String>] parameter => example value
|
86
|
+
def auth_token_credentials(desc, password: true)
|
87
|
+
passwords = %i(password pass passwd)
|
88
|
+
params = desc[:resources][:token][:actions]['request'][:input][:parameters].keys - %i(lifetime interval)
|
89
|
+
|
90
|
+
unless password
|
91
|
+
params.reject! { |param| passwords.include?(param) }
|
92
|
+
end
|
93
|
+
|
94
|
+
Hash[params.map do |param|
|
95
|
+
value =
|
96
|
+
if passwords.include?(param)
|
97
|
+
'secret'
|
98
|
+
else
|
99
|
+
param
|
100
|
+
end
|
101
|
+
|
102
|
+
[param, value]
|
103
|
+
end]
|
104
|
+
end
|
82
105
|
end
|
83
106
|
end
|
@@ -12,8 +12,6 @@ module HaveAPI::ClientExamples
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def auth(method, desc)
|
15
|
-
login = {user: 'user', password: 'password', lifetime: 'fixed'}
|
16
|
-
|
17
15
|
case method
|
18
16
|
when :basic
|
19
17
|
<<END
|
@@ -30,6 +28,8 @@ $ curl --request OPTIONS \\
|
|
30
28
|
END
|
31
29
|
|
32
30
|
when :token
|
31
|
+
login = auth_token_credentials(desc).merge(lifetime: 'fixed')
|
32
|
+
|
33
33
|
<<END
|
34
34
|
# Acquire the token
|
35
35
|
$ curl --request POST \\
|
@@ -42,6 +42,9 @@ $ curl --request OPTIONS \\
|
|
42
42
|
--header '#{desc[:http_header]}: thetoken' \\
|
43
43
|
'#{base_url}'
|
44
44
|
END
|
45
|
+
|
46
|
+
when :oauth2
|
47
|
+
'# See RFC 6749 for authorization process and RFC 6750 for access token usage.'
|
45
48
|
end
|
46
49
|
end
|
47
50
|
|
@@ -28,12 +28,33 @@ Authorization: Basic dXNlcjpzZWNyZXQ=
|
|
28
28
|
END
|
29
29
|
|
30
30
|
when :token
|
31
|
+
login = auth_token_credentials(desc).merge(lifetime: 'fixed')
|
32
|
+
|
31
33
|
<<END
|
32
34
|
POST /_auth/token/tokens HTTP/1.1
|
33
35
|
Host: #{host}
|
34
36
|
Content-Type: application/json
|
35
37
|
|
36
|
-
#{JSON.pretty_generate({token:
|
38
|
+
#{JSON.pretty_generate({token: login})}
|
39
|
+
END
|
40
|
+
|
41
|
+
when :oauth2
|
42
|
+
<<END
|
43
|
+
# 1) Request authorization code
|
44
|
+
GET #{desc[:authorize_path]}?response_type=code&client_id=$client_id&state=$state&redirect_uri=$client_redirect_uri HTTP/1.1
|
45
|
+
Host: #{host}
|
46
|
+
|
47
|
+
# 2) The user logs in using this API
|
48
|
+
|
49
|
+
# 3) The API then redirects the user back to the client application
|
50
|
+
GET $client_redirect_uri?code=$authorization_code&state=$state
|
51
|
+
Host: client-application
|
52
|
+
|
53
|
+
# 4) The client application requests access token
|
54
|
+
POST #{desc[:token_path]}
|
55
|
+
Content-Type: application/x-www-form-urlencoded
|
56
|
+
|
57
|
+
grant_type=authorization_code&code=$authorization_code&redirect_uri=$client_redirect_uri&client_id=$client_id&client_secret=$client_secret
|
37
58
|
END
|
38
59
|
end
|
39
60
|
end
|
@@ -21,7 +21,7 @@ END
|
|
21
21
|
#{init}
|
22
22
|
|
23
23
|
api.authenticate("basic", {
|
24
|
-
|
24
|
+
user: "user",
|
25
25
|
password: "secret"
|
26
26
|
}, function (client, status) {
|
27
27
|
console.log("Authenticated?", status);
|
@@ -34,8 +34,7 @@ END
|
|
34
34
|
|
35
35
|
// Request a new token
|
36
36
|
api.authenticate("token", {
|
37
|
-
|
38
|
-
password: "secret"
|
37
|
+
#{auth_token_credentials(desc).map { |k, v| "#{k}: \"#{v}\"" }.join(",\n ")}
|
39
38
|
}, function (client, status) {
|
40
39
|
console.log("Authenticated?", status);
|
41
40
|
|
@@ -50,6 +49,9 @@ api.authenticate("token", {
|
|
50
49
|
console.log("Authenticated?", status);
|
51
50
|
});
|
52
51
|
END
|
52
|
+
|
53
|
+
when :oauth2
|
54
|
+
'// OAuth2 is not supported by HaveAPI JavaScript client.'
|
53
55
|
end
|
54
56
|
end
|
55
57
|
|
@@ -18,7 +18,7 @@ END
|
|
18
18
|
<<END
|
19
19
|
#{init}
|
20
20
|
|
21
|
-
$api->authenticate("basic", ["
|
21
|
+
$api->authenticate("basic", ["user" => "user", "password" => "secret"]);
|
22
22
|
END
|
23
23
|
|
24
24
|
when :token
|
@@ -26,13 +26,16 @@ END
|
|
26
26
|
#{init}
|
27
27
|
|
28
28
|
// Get token using username and password
|
29
|
-
$api->authenticate("token", [
|
29
|
+
$api->authenticate("token", [#{auth_token_credentials(desc).map { |k, v| "\"#{k}\" => \"#{v}\"" }.join(', ')}]);
|
30
30
|
|
31
31
|
echo "Token = ".$api->getAuthenticationProvider()->getToken();
|
32
32
|
|
33
33
|
// Next time, the client can authenticate using the token directly
|
34
34
|
$api->authenticate("token", ["token" => $savedToken]);
|
35
35
|
END
|
36
|
+
|
37
|
+
when :oauth2
|
38
|
+
'// OAuth2 is not supported by HaveAPI PHP client.'
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
@@ -20,10 +20,10 @@ module HaveAPI::ClientExamples
|
|
20
20
|
when :basic
|
21
21
|
<<END
|
22
22
|
# Provide credentials on command line
|
23
|
-
#{init} --auth basic --
|
23
|
+
#{init} --auth basic --user user --password secret
|
24
24
|
|
25
25
|
# If username or password isn't provided, the user is asked on stdin
|
26
|
-
#{init} --auth basic --
|
26
|
+
#{init} --auth basic --user user
|
27
27
|
Password: secret
|
28
28
|
END
|
29
29
|
|
@@ -33,13 +33,16 @@ END
|
|
33
33
|
# Note that the client always has to call some action. APIs should provide
|
34
34
|
# an action to get information about the current user, so that's what we're
|
35
35
|
# calling now.
|
36
|
-
#{init} --auth token
|
36
|
+
#{init} --auth token #{auth_token_credentials(desc, password: false).map { |k, v| "--#{k} #{v}" }.join(' ')} --save user current
|
37
37
|
Password: secret
|
38
38
|
|
39
39
|
# Now the token is read from disk and the user does not have to provide username
|
40
40
|
# nor password and be authenticated
|
41
41
|
#{init} user current
|
42
42
|
END
|
43
|
+
|
44
|
+
when :oauth2
|
45
|
+
'# OAuth2 is not supported by HaveAPI Ruby CLI.'
|
43
46
|
end
|
44
47
|
end
|
45
48
|
|
@@ -21,7 +21,7 @@ END
|
|
21
21
|
<<END
|
22
22
|
#{init}
|
23
23
|
|
24
|
-
client.authenticate(:basic,
|
24
|
+
client.authenticate(:basic, user: "user", password: "secret")
|
25
25
|
END
|
26
26
|
|
27
27
|
when :token
|
@@ -29,13 +29,16 @@ END
|
|
29
29
|
#{init}
|
30
30
|
|
31
31
|
# Get token using username and password
|
32
|
-
client.authenticate(:token,
|
32
|
+
client.authenticate(:token, #{auth_token_credentials(desc).map { |k, v| "#{k}: \"#{v}\"" }.join(', ')})
|
33
33
|
|
34
34
|
puts "Token = \#{client.auth.token}"
|
35
35
|
|
36
36
|
# Next time, the client can authenticate using the token directly
|
37
37
|
client.authenticate(:token, token: saved_token)
|
38
38
|
END
|
39
|
+
|
40
|
+
when :oauth2
|
41
|
+
'# OAuth2 is not supported by HaveAPI Ruby client.'
|
39
42
|
end
|
40
43
|
end
|
41
44
|
|
data/lib/haveapi/server.rb
CHANGED
@@ -576,6 +576,13 @@ module HaveAPI
|
|
576
576
|
"#{@root}v#{v}/"
|
577
577
|
end
|
578
578
|
|
579
|
+
# @param v [String] API version
|
580
|
+
# @param provider [Authentication::Base]
|
581
|
+
# @param prefix [String]
|
582
|
+
def add_auth_routes(v, provider, prefix: '')
|
583
|
+
provider.register_routes(@sinatra, "#{@root}_auth/#{prefix}")
|
584
|
+
end
|
585
|
+
|
579
586
|
def add_auth_module(v, name, mod, prefix: '')
|
580
587
|
@routes[v] ||= {authentication: {name => {resources: {}}}}
|
581
588
|
|
data/lib/haveapi/version.rb
CHANGED
@@ -10,6 +10,13 @@
|
|
10
10
|
<dt>Query parameter:</dt>
|
11
11
|
<dd><%= info[:query_parameter] %></dd>
|
12
12
|
</dl>
|
13
|
+
<% elsif name == :oauth2 %>
|
14
|
+
<dl>
|
15
|
+
<dt>Authorize path:</dt>
|
16
|
+
<dd><%= info[:authorize_path] %></dd>
|
17
|
+
<dt>Token path:</dt>
|
18
|
+
<dd><%= info[:token_path] %></dd>
|
19
|
+
</dl>
|
13
20
|
<% end %>
|
14
21
|
|
15
22
|
<% if info[:resources] %>
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: haveapi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.18.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jakub Skokan
|
@@ -44,56 +44,56 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '7.0'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '7.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: sinatra
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 3.0
|
61
|
+
version: 3.1.0
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 3.0
|
68
|
+
version: 3.1.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: tilt
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 2.0
|
75
|
+
version: 2.3.0
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 2.0
|
82
|
+
version: 2.3.0
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: redcarpet
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '3.
|
89
|
+
version: '3.6'
|
90
90
|
type: :runtime
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '3.
|
96
|
+
version: '3.6'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: rake
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,14 +142,14 @@ dependencies:
|
|
142
142
|
requirements:
|
143
143
|
- - "~>"
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: 0.
|
145
|
+
version: 0.18.0
|
146
146
|
type: :runtime
|
147
147
|
prerelease: false
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
149
149
|
requirements:
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: 0.
|
152
|
+
version: 0.18.0
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
154
|
name: mail
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -164,6 +164,20 @@ dependencies:
|
|
164
164
|
- - ">="
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rack-oauth2
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: 2.2.0
|
174
|
+
type: :runtime
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: 2.2.0
|
167
181
|
description: Framework for creating self-describing APIs
|
168
182
|
email: jakub.skokan@vpsfree.cz
|
169
183
|
executables: []
|
@@ -193,6 +207,9 @@ files:
|
|
193
207
|
- lib/haveapi/authentication/base.rb
|
194
208
|
- lib/haveapi/authentication/basic/provider.rb
|
195
209
|
- lib/haveapi/authentication/chain.rb
|
210
|
+
- lib/haveapi/authentication/oauth2.rb
|
211
|
+
- lib/haveapi/authentication/oauth2/config.rb
|
212
|
+
- lib/haveapi/authentication/oauth2/provider.rb
|
196
213
|
- lib/haveapi/authentication/token.rb
|
197
214
|
- lib/haveapi/authentication/token/action_config.rb
|
198
215
|
- lib/haveapi/authentication/token/action_request.rb
|
@@ -315,7 +332,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
315
332
|
- !ruby/object:Gem::Version
|
316
333
|
version: '0'
|
317
334
|
requirements: []
|
318
|
-
rubygems_version: 3.
|
335
|
+
rubygems_version: 3.4.13
|
319
336
|
signing_key:
|
320
337
|
specification_version: 4
|
321
338
|
summary: Framework for creating self-describing APIs
|