Class: Aspera::Oauth

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/oauth.rb

Overview

Implement OAuth 2 for the REST client and generate a bearer token call get_authorization() to get a token. bearer tokens are kept in memory and also in a file cache for later re-use if a token is expired (api returns 4xx), call again get_authorization(true) tools.ietf.org/html/rfc6749

Constant Summary collapse

DEFAULT_CREATE_PARAMS =
{
  path_token:  'token', # default endpoint for /token to generate token
  token_field: 'access_token', # field with token in result of call to path_token
  web:         {path_authorize: 'authorize'} # default endpoint for /authorize, used for code exchange
}.freeze
STD_AUTH_TYPES =

OAuth methods supported by default

i[web jwt].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#apiObject (readonly)

Returns the value of attribute api.



171
172
173
# File 'lib/aspera/oauth.rb', line 171

def api
  @api
end

#generic_parametersObject (readonly)

Returns the value of attribute generic_parameters.



171
172
173
# File 'lib/aspera/oauth.rb', line 171

def generic_parameters
  @generic_parameters
end

#specific_parametersObject (readonly)

Returns the value of attribute specific_parameters.



171
172
173
# File 'lib/aspera/oauth.rb', line 171

def specific_parameters
  @specific_parameters
end

Class Method Details

.decode_token(token) ⇒ Object

decode token using all registered decoders



77
78
79
80
81
82
83
# File 'lib/aspera/oauth.rb', line 77

def decode_token(token)
  @decoders.each do |decoder|
    result = decoder.call(token) rescue nil
    return result unless result.nil?
  end
  return nil
end

.flush_tokensObject

delete all existing tokens



66
67
68
# File 'lib/aspera/oauth.rb', line 66

def flush_tokens
  persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN, nil)
end

.id_creator(id) ⇒ Object

list of identifiers found in creation parameters that can be used to uniquely identify the token



103
104
105
106
# File 'lib/aspera/oauth.rb', line 103

def id_creator(id)
  raise "id creator type unknown: #{id}/#{id.class}" unless @id_handlers.key?(id)
  @id_handlers[id]
end

.persist_mgrObject



54
55
56
57
58
59
60
61
62
63
# File 'lib/aspera/oauth.rb', line 54

def persist_mgr
  if @persist.nil?
    Log.log.debug('Not using persistency') # (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)
    # create NULL persistency class
    @persist = Class.new do
      def get(_x); nil; end; def delete(_x); nil; end; def put(_x, _y); nil; end; def garbage_collect(_x, _y); nil; end # rubocop:disable Layout/EmptyLineBetweenDefs, Style/Semicolon, Layout/LineLength
    end.new
  end
  return @persist
end

.persist_mgr=(manager) ⇒ Object



48
49
50
51
52
# File 'lib/aspera/oauth.rb', line 48

def persist_mgr=(manager)
  @persist = manager
  # cleanup expired tokens
  @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, TOKEN_CACHE_EXPIRY_SEC)
end

.register_decoder(method) ⇒ Object

register a bearer token decoder, mainly to inspect expiry date



71
72
73
74
# File 'lib/aspera/oauth.rb', line 71

def register_decoder(method)
  @decoders ||= []
  @decoders.push(method)
end

.register_token_creator(id, lambda_create, id_create) ⇒ Object

register a token creation method

Parameters:

  • id

    creation type from field :grant_method in constructor

  • lambda_create

    called to create token

  • id_create

    called to generate unique id for token, for cache



89
90
91
92
93
94
# File 'lib/aspera/oauth.rb', line 89

def register_token_creator(id, lambda_create, id_create)
  Log.log.debug{"registering token creator #{id}"}
  raise 'ERROR: requites Symbol and 2 lambdas' unless id.is_a?(Symbol) && lambda_create.is_a?(Proc) && id_create.is_a?(Proc)
  @create_handlers[id] = lambda_create
  @id_handlers[id] = id_create
end

.token_creator(id) ⇒ Object

Returns one of the registered creators for the given create type.

Returns:

  • one of the registered creators for the given create type



97
98
99
100
# File 'lib/aspera/oauth.rb', line 97

def token_creator(id)
  raise "token grant method unknown: '#{id}' (#{id.class})" unless @create_handlers.key?(id)
  @create_handlers[id]
end

Instance Method Details

#create_token(www_params) ⇒ Object

helper method to create token as per RFC



223
224
225
226
227
228
229
230
# File 'lib/aspera/oauth.rb', line 223

def create_token(www_params)
  Log.log.debug{'Generating a new token'.bg_green}
  return @api.call({
    operation:       'POST',
    subpath:         @generic_parameters[:path_token],
    headers:         {'Accept' => 'application/json'},
    www_body_params: www_params})
end

#get_authorization(use_refresh_token: false, use_cache: true) ⇒ Object

Oauth v2 token generation

Parameters:

  • use_refresh_token (defaults to: false)

    set to true to force refresh or re-generation (if previous failed)



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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/aspera/oauth.rb', line 243

def get_authorization(use_refresh_token: false, use_cache: true)
  # generate token unique identifier for persistency (memory/disk cache)
  token_id = IdGenerator.from_list([
    PERSIST_CATEGORY_TOKEN,
    @api.params[:base_url],
    @generic_parameters[:grant_method],
    self.class.id_creator(@generic_parameters[:grant_method]).call(self), # array, so we flatten later
    @generic_parameters[:scope],
    @api.params.dig(i[auth username])
  ].flatten)

  # get token_data from cache (or nil), token_data is what is returned by /token
  token_data = self.class.persist_mgr.get(token_id) if use_cache
  token_data = JSON.parse(token_data) unless token_data.nil?
  # Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
  # might help in case the transfer agent cannot refresh himself
  # `direct` agent is equipped with refresh code
  if !use_refresh_token && !token_data.nil?
    decoded_token = self.class.decode_token(token_data[@generic_parameters[:token_field]])
    Log.dump('decoded_token', decoded_token) unless decoded_token.nil?
    if decoded_token.is_a?(Hash)
      expires_at_sec =
        if    decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
        elsif decoded_token['exp'].is_a?(Integer)       then Time.at(decoded_token['exp'])
        end
      # force refresh if we see a token too close from expiration
      use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < TOKEN_EXPIRATION_GUARD_SEC
      Log.log.debug{"Expiration: #{expires_at_sec} / #{use_refresh_token}"}
    end
  end

  # an API was already called, but failed, we need to regenerate or refresh
  if use_refresh_token
    if token_data.is_a?(Hash) && token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
      # save possible refresh token, before deleting the cache
      refresh_token = token_data['refresh_token']
    end
    # delete cache
    self.class.persist_mgr.delete(token_id)
    token_data = nil
    # lets try the existing refresh token
    if !refresh_token.nil?
      Log.log.info{"refresh=[#{refresh_token}]".bg_green}
      # try to refresh
      # note: AoC admin token has no refresh, and lives by default 1800secs
      resp = create_token(optional_scope_client_id.merge(grant_type: 'refresh_token', refresh_token: refresh_token))
      if resp[:http].code.start_with?('2')
        # save only if success
        json_data = resp[:http].body
        token_data = JSON.parse(json_data)
        self.class.persist_mgr.put(token_id, json_data)
      else
        Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
      end
    end
  end

  # no cache, nor refresh: generate a token
  if token_data.nil?
    resp = self.class.token_creator(@generic_parameters[:grant_method]).call(self)
    json_data = resp[:http].body
    token_data = JSON.parse(json_data)
    self.class.persist_mgr.put(token_id, json_data)
  end # if ! in_cache
  raise "API error: No such field in answer: #{@generic_parameters[:token_field]}" unless token_data.key?(@generic_parameters[:token_field])
  # ok we shall have a token here
  return 'Bearer ' + token_data[@generic_parameters[:token_field]]
end

#optional_scope_client_id(add_secret: false) ⇒ Object

Returns Hash with optional general parameters.

Returns:

  • Hash with optional general parameters



233
234
235
236
237
238
239
# File 'lib/aspera/oauth.rb', line 233

def optional_scope_client_id(add_secret: false)
  call_params = {}
  call_params[:scope] = @generic_parameters[:scope] unless @generic_parameters[:scope].nil?
  call_params[:client_id] = @generic_parameters[:client_id] unless @generic_parameters[:client_id].nil?
  call_params[:client_secret] = @generic_parameters[:client_secret] if add_secret && !@generic_parameters[:client_id].nil?
  return call_params
end