Class: HaveAPI::Authentication::OAuth2::Provider

Inherits:
Base
  • Object
show all
Defined in:
lib/haveapi/authentication/oauth2/provider.rb

Overview

OAuth2 authentication and authorization provider

Must be configured with Config using with_config.

Instance Attribute Summary collapse

Attributes inherited from Base

#name

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

auth_method, inherited, #resource_module

Constructor Details

#initialize(server, v, cfg) ⇒ Provider

Returns a new instance of Provider.



76
77
78
79
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 76

def initialize(server, v, cfg)
  @config = cfg.new(self, server, v)
  super(server, v)
end

Instance Attribute Details

#authorize_pathString (readonly)

Returns:

  • (String)


71
72
73
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 71

def authorize_path
  @authorize_path
end

#configConfig (readonly)

Returns:



74
75
76
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 74

def config
  @config
end

Class Method Details

.with_config(cfg) ⇒ Object

Configure the OAuth2 provider

Parameters:



62
63
64
65
66
67
68
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 62

def self.with_config(cfg)
  Module.new do
    define_singleton_method(:new) do |*args|
      Provider.new(*args, cfg)
    end
  end
end

Instance Method Details

#authenticate(request) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 115

def authenticate(request)
  tokens = [
    request.params['access_token'],
    token_from_authorization_header(request),
    token_from_haveapi_header(request)
  ].select { |value| token_present?(value) }

  token =
    case tokens.length
    when 0
      nil
    when 1
      tokens.first
    else
      raise HaveAPI::Authentication::TokenConflict,
            HaveAPI.t('haveapi.authentication.multiple_oauth2_tokens')
    end

  token && config.find_user_by_access_token(request, token)
rescue HaveAPI::AuthenticationError
  nil
end

#authorization_endpoint(handler) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 165

def authorization_endpoint(handler)
  Rack::OAuth2::Server::Authorize.new do |req, res|
    client = config.find_client_by_id(req.client_id)
    req.bad_request! if client.nil?

    res.redirect_uri = req.verify_redirect_uri!(client.redirect_uri)

    if req.post?
      auth_res = config.(
        sinatra_handler: handler,
        sinatra_request: handler.request,
        sinatra_params: handler.params,
        oauth2_request: req,
        oauth2_response: res,
        client:
      )

      if auth_res.nil? || auth_res.cancel
        # Authentication failed / cancel requested
        req.access_denied!
      elsif auth_res.authenticated && auth_res.complete
        # Authentication was successful
        case req.response_type
        when :code
          res.code = config.get_authorization_code(auth_res)
        when :token
          req.unsupported_response_type!
        end

        res.approve!
      end
    else
      auth_res = config.handle_get_authorize(
        sinatra_handler: handler,
        sinatra_request: handler.request,
        sinatra_params: handler.params,
        oauth2_request: req,
        oauth2_response: res,
        client:
      )

      if auth_res.nil?
        # We expect `config.handle_get_authorize` has sent response body
      elsif auth_res.cancel
        # Cancel the process
        req.access_denied!
      elsif auth_res.authenticated && auth_res.complete
        # Authentication was successful
        case req.response_type
        when :code
          res.code = config.get_authorization_code(auth_res)
        when :token
          req.unsupported_response_type!
        end

        res.approve!
      end
    end
  end
end

#call_authorization_endpoint(handler) ⇒ Object



308
309
310
311
312
313
314
315
316
317
318
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 308

def call_authorization_endpoint(handler)
  return invalid_request unless scalar_params?(
    handler.params,
    %w[client_id response_type redirect_uri state scope response_mode
       code_challenge code_challenge_method]
  )

  finish_oauth_errors do
    authorization_endpoint(handler).call(handler.request.env)
  end
end

#call_revoke_endpoint(handler) ⇒ Object



332
333
334
335
336
337
338
339
340
341
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 332

def call_revoke_endpoint(handler)
  return invalid_request unless scalar_params?(
    handler.params,
    %w[token token_type_hint client_id client_secret]
  )

  finish_oauth_errors do
    revoke_endpoint(handler).call(handler.request.env)
  end
end

#call_token_endpoint(handler) ⇒ Object



320
321
322
323
324
325
326
327
328
329
330
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 320

def call_token_endpoint(handler)
  return invalid_request unless scalar_params?(
    handler.params,
    %w[grant_type client_id client_secret code redirect_uri refresh_token
       code_verifier client_assertion client_assertion_type]
  )

  finish_oauth_errors do
    token_endpoint(handler).call(handler.request.env)
  end
end

#describeObject



152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 152

def describe
  {
    description: HaveAPI.message('haveapi.authentication.oauth2.description'),
    http_header: config.http_header,
    authorize_url: @authorize_url,
    authorize_path: @authorize_path,
    token_url: @token_url,
    token_path: @token_path,
    revoke_url: @revoke_url,
    revoke_path: @revoke_path
  }
end

#register_routes(sinatra, prefix) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 85

def register_routes(sinatra, prefix)
  @authorize_path = File.join(prefix, 'authorize')
  @token_path = File.join(prefix, 'token')
  @revoke_path = File.join(prefix, 'revoke')

  base_url = config.base_url

  @authorize_url = File.join(base_url, @authorize_path)
  @token_url = File.join(base_url, @token_path)
  @revoke_url = File.join(base_url, @revoke_path)

  that = self

  sinatra.get @authorize_path do
    that.call_authorization_endpoint(self)
  end

  sinatra.post @authorize_path do
    that.call_authorization_endpoint(self)
  end

  sinatra.post @token_path do
    that.call_token_endpoint(self)
  end

  sinatra.post @revoke_path do
    that.call_revoke_endpoint(self)
  end
end

#revoke_endpoint(handler) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 283

def revoke_endpoint(handler)
  RevokeEndpoint.new do |req, _res|
    req.invalid_client! if req.client_id.nil? || req.client_id.empty?
    req.invalid_client! if req.client_secret.nil? || req.client_secret.empty?

    client = config.find_client_by_id(req.client_id)
    req.invalid_client! if client.nil? || !client.check_secret(req.client_secret)

    ret = config.handle_post_revoke(
      handler.request,
      req.token,
      **revoke_kwargs(req, client)
    )

    case ret
    when :revoked
      # ok
    when :unsupported
      req.unsupported_token_type!
    else
      raise Rack::OAuth2::Server::Abstract::ServerError
    end
  end
end

#setupObject



81
82
83
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 81

def setup
  @server.allow_header(config.http_header)
end

#token_endpoint(handler) ⇒ Object



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 226

def token_endpoint(handler)
  Rack::OAuth2::Server::Token.new do |req, res|
    client = config.find_client_by_id(req.client_id)
    req.invalid_client! if client.nil? || !client.check_secret(req.client_secret)

    res.access_token =
      case req.grant_type
      when :authorization_code
        authorization = config.find_authorization_by_code(client, req.code)

        if authorization.nil? || !authorization.check_code_validity(req.redirect_uri)
          req.invalid_grant!
        end

        if authorization.code_challenge
          req.invalid_grant! unless authorization.code_challenge.is_a?(String)

          code_challenge_method = authorization.code_challenge_method || 'plain'
          req.invalid_grant! unless code_challenge_method.is_a?(String)

          req.verify_code_verifier!(
            authorization.code_challenge,
            code_challenge_method.to_sym
          )
        end

        access_token, expires_at, refresh_token = config.get_tokens(authorization, handler.request)

        bearer_token = Rack::OAuth2::AccessToken::Bearer.new(
          access_token:,
          expires_in: expires_at - Time.now
        )
        bearer_token.refresh_token = refresh_token if refresh_token
        bearer_token

      when :refresh_token
        authorization = config.find_authorization_by_refresh_token(client, req.refresh_token)

        if authorization.nil?
          req.invalid_grant!
        end

        access_token, expires_at, refresh_token = config.refresh_tokens(authorization, handler.request)

        bearer_token = Rack::OAuth2::AccessToken::Bearer.new(
          access_token:,
          expires_in: expires_at - Time.now
        )
        bearer_token.refresh_token = refresh_token if refresh_token
        bearer_token

      else # :password, :client_credentials
        req.unsupported_grant_type!
      end
  end
end

#token_from_authorization_header(request) ⇒ Object



138
139
140
141
142
143
144
145
146
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 138

def token_from_authorization_header(request)
  auth_header = Rack::Auth::AbstractRequest.new(request.env)

  return unless auth_header.provided? && !auth_header.parts.first.nil? && auth_header.scheme.to_s == 'bearer'

  auth_header.params
rescue ArgumentError, EncodingError
  nil
end

#token_from_haveapi_header(request) ⇒ Object



148
149
150
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 148

def token_from_haveapi_header(request)
  request.env[header_to_env(config.http_header)]
end