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
# 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)
  ].compact

  token =
    case tokens.length
    when 0
      nil
    when 1
      tokens.first
    else
      raise 'Too many oauth2 tokens'
    end

  token && config.find_user_by_access_token(request, token)
end

#authorization_endpoint(handler) ⇒ Object



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
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 176

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

#describeObject



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 147

def describe
  desc = <<-END
  OAuth2 authorization provider. While OAuth2 support in HaveAPI clients
  is limited, it is possible to use your API as an authentication source
  and to use OAuth2 tokens to access the API.

  HaveAPI partially implements RFC 6749: authorization response type "code"
  and token grant types "authorization_code" and "refresh_token". Other
  response and grant types are not supported at this time.

  The access token can be passed as bearer token according to RFC 6750,
  or using a custom HTTP header when the Authorization header is not
  practical.

  The access and refresh tokens can be revoked as per RFC 7009.
  END

  {
    description: desc,
    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.authorization_endpoint(self).call(request.env)
  end

  sinatra.post @authorize_path do
    that.authorization_endpoint(self).call(request.env)
  end

  sinatra.post @token_path do
    that.token_endpoint(self).call(request.env)
  end

  sinatra.post @revoke_path do
    that.revoke_endpoint(self).call(request.env)
  end
end

#revoke_endpoint(handler) ⇒ Object



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 289

def revoke_endpoint(handler)
  RevokeEndpoint.new do |req, _res|
    ret = config.handle_post_revoke(
      handler.request,
      req.token,
      token_type_hint: req.token_type_hint
    )

    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



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
282
283
284
285
286
287
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 237

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 && authorization.code_challenge_method
          req.verify_code_verifier!(
            authorization.code_challenge,
            authorization.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



135
136
137
138
139
140
141
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 135

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
end

#token_from_haveapi_header(request) ⇒ Object



143
144
145
# File 'lib/haveapi/authentication/oauth2/provider.rb', line 143

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