haveapi 0.17.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4799af18ce8de3147686153493e32210e780c650bdcec3e18f5ff714da50ee03
4
- data.tar.gz: 4cfe8ff071d7d4ea6adcb28e25ab0d491008e9402058c34293bdf70881bf13e2
3
+ metadata.gz: fedb085c210494a3173ef597e44f63a280485cc0490bd0c0b74610d26d52e5b4
4
+ data.tar.gz: 372f4a9cb544426ad1e06c3bb60e26a50077b5e3bef70e7b77ac008ca5008aaa
5
5
  SHA512:
6
- metadata.gz: 936f0c86ff2b766339c3dbc08d49c2ae46868002733124dad9987ad932f9232807160a6a36777fc6fe45b952682e89bf9b0a5856939d6a7a1775e94e9d8ffe4c
7
- data.tar.gz: 3527985845485151a532aa42ce235eed35827f8d9f6a2aa053b208e7621ce4d93f578cd531f06aec8bcc27ecd1079c8023b7fc63f29bf36d4b6e38807121f0f6
6
+ metadata.gz: 6ab713fd64ead24d6a21095bda69ddbc536da56fdd26745c0c60721a8c9e5767addafdf7906a3c98a23401472106e83f8d341903291e4c5e3b1c08974c3f12b3
7
+ data.tar.gz: 58b3cfc406d09868675f07b51050c5b914368c1c4ee70dbd9cf919a6f2b7e9124133e0f6f33292822f69e614cd1090e6f605ba5179d0c98802d4b1023e09cc6e
data/haveapi.gemspec CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
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.17.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
@@ -0,0 +1,9 @@
1
+ module HaveAPI::Authentication
2
+ module OAuth2
3
+ # Configure the oauth2 provider
4
+ # @param cfg [Config]
5
+ def self.with_config(cfg)
6
+ Provider.with_config(cfg)
7
+ end
8
+ end
9
+ end
@@ -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
 
@@ -34,6 +34,9 @@ Password: secret
34
34
  # Note that the file system can read config file from haveapi-client, so if
35
35
  # you set up authentication there, the file system will use it.
36
36
  END
37
+
38
+ when :oauth2
39
+ '# OAuth2 is not supported by haveapi-fs.'
37
40
  end
38
41
  end
39
42
 
@@ -36,6 +36,25 @@ Host: #{host}
36
36
  Content-Type: application/json
37
37
 
38
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
39
58
  END
40
59
  end
41
60
  end
@@ -49,6 +49,9 @@ api.authenticate("token", {
49
49
  console.log("Authenticated?", status);
50
50
  });
51
51
  END
52
+
53
+ when :oauth2
54
+ '// OAuth2 is not supported by HaveAPI JavaScript client.'
52
55
  end
53
56
  end
54
57
 
@@ -33,6 +33,9 @@ echo "Token = ".$api->getAuthenticationProvider()->getToken();
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
 
@@ -40,6 +40,9 @@ Password: secret
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
 
@@ -36,6 +36,9 @@ puts "Token = \#{client.auth.token}"
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
 
@@ -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
 
@@ -1,4 +1,4 @@
1
1
  module HaveAPI
2
2
  PROTOCOL_VERSION = '2.0'
3
- VERSION = '0.17.0'
3
+ VERSION = '0.18.0'
4
4
  end
@@ -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.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: 0.17.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.17.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