Class: Aspera::OAuth::Factory

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

Overview

Factory to create tokens and manage their cache

Constant Summary collapse

TOKEN_FIELD =
'access_token'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#parametersObject (readonly)

Returns the value of attribute parameters.



86
87
88
# File 'lib/aspera/oauth/factory.rb', line 86

def parameters
  @parameters
end

Class Method Details

.bearer_auth?(authorization) ⇒ Boolean

Check if the authorization contains a bearer token

Parameters:

  • authorization (String)

    The authorization header value

Returns:

  • (Boolean)

    true if the authorization contains a bearer token, i.e. auth scheme is bearer



36
37
38
# File 'lib/aspera/oauth/factory.rb', line 36

def bearer_auth?(authorization)
  return authorization.start_with?(SPACE_BEARER_AUTH_SCHEME)
end

.bearer_authorization(token) ⇒ String

Format a token for use in Authorization header

Parameters:

  • token (String)

    The token alone

Returns:

  • (String)

    Value suitable for Authorization header



29
30
31
# File 'lib/aspera/oauth/factory.rb', line 29

def bearer_authorization(token)
  return "#{SPACE_BEARER_AUTH_SCHEME}#{token}"
end

.bearer_token(authorization) ⇒ String

Extract only token from Authorization (remove scheme)

Parameters:

  • authorization (String)

    The authorization header value

Returns:

  • (String)

    The bearer token without the scheme prefix



43
44
45
46
# File 'lib/aspera/oauth/factory.rb', line 43

def bearer_token(authorization)
  Aspera.assert(bearer_auth?(authorization)){'not a bearer token, wrong prefix scheme'}
  return authorization.delete_prefix(SPACE_BEARER_AUTH_SCHEME)
end

.cache_id(url, creator_class, *params) ⇒ String

Generate a unique cache id for a token creator

Parameters:

  • url (String)

    Base URL of the OAuth server

  • creator_class (Class)

    Class of the token creator

  • params (Array)

    List of parameters (can be nested) to uniquely identify the token

Returns:

  • (String)

    a unique cache identifier



53
54
55
# File 'lib/aspera/oauth/factory.rb', line 53

def cache_id(url, creator_class, *params)
  return IdGenerator.from_list(PERSIST_CATEGORY_TOKEN, url, Factory.class_to_id(creator_class), params)
end

.class_to_id(creator_class) ⇒ Symbol

Convert a class name to snake_case symbol

Parameters:

  • creator_class (Class)

    The class to convert

Returns:

  • (Symbol)

    snake_case version of class name



60
61
62
# File 'lib/aspera/oauth/factory.rb', line 60

def class_to_id(creator_class)
  return creator_class.name.split('::').last.capital_to_snake.to_sym
end

.instanceFactory

Returns the singleton instance of Factory

Returns:

  • (Factory)

    the singleton instance



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/aspera/oauth/factory.rb', line 14

class Factory
  include Singleton

  # prefix for persistency of tokens (simplify garbage collect)
  PERSIST_CATEGORY_TOKEN = 'token'
  # prefix for bearer authorization when in header
  SPACE_BEARER_AUTH_SCHEME = 'Bearer '
  TOKEN_FIELD = 'access_token'

  private_constant :PERSIST_CATEGORY_TOKEN, :SPACE_BEARER_AUTH_SCHEME

  class << self
    # Format a token for use in Authorization header
    # @param token [String] The token alone
    # @return [String] Value suitable for Authorization header
    def bearer_authorization(token)
      return "#{SPACE_BEARER_AUTH_SCHEME}#{token}"
    end

    # Check if the authorization contains a bearer token
    # @param authorization [String] The authorization header value
    # @return [Boolean] true if the authorization contains a bearer token, i.e. auth scheme is bearer
    def bearer_auth?(authorization)
      return authorization.start_with?(SPACE_BEARER_AUTH_SCHEME)
    end

    # Extract only token from Authorization (remove scheme)
    # @param authorization [String] The authorization header value
    # @return [String] The bearer token without the scheme prefix
    def bearer_token(authorization)
      Aspera.assert(bearer_auth?(authorization)){'not a bearer token, wrong prefix scheme'}
      return authorization.delete_prefix(SPACE_BEARER_AUTH_SCHEME)
    end

    # Generate a unique cache id for a token creator
    # @param url           [String] Base URL of the OAuth server
    # @param creator_class [Class]  Class of the token creator
    # @param params        [Array]  List of parameters (can be nested) to uniquely identify the token
    # @return [String] a unique cache identifier
    def cache_id(url, creator_class, *params)
      return IdGenerator.from_list(PERSIST_CATEGORY_TOKEN, url, Factory.class_to_id(creator_class), params)
    end

    # Convert a class name to snake_case symbol
    # @param creator_class [Class] The class to convert
    # @return [Symbol] snake_case version of class name
    def class_to_id(creator_class)
      return creator_class.name.split('::').last.capital_to_snake.to_sym
    end
  end

  private

  # Initialize the factory with default parameters and empty collections
  def initialize
    # persistency manager
    @persist = nil
    # token creation methods
    @token_type_classes = {}
    # list of lambda
    @decoders = []
    # default parameters, others can be added by handlers
    @parameters = {
      # tokens older than this duration in sec. will be discarded from cache
      token_cache_max_age:     1800,
      # tokens valid for less than this duration in sec. will be regenerated
      token_refresh_threshold: 120
    }
  end

  public

  attr_reader :parameters

  # Set the persistence manager for token caching
  # @param manager [Object] The persistence manager instance
  def persist_mgr=(manager)
    @persist = manager
    # cleanup expired tokens
    @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:token_cache_max_age])
  end

  # Get or initialize the persistence manager
  # @return [Object] The persistence manager instance
  def persist_mgr
    if @persist.nil?
      # use OAuth::Factory.instance.persist_mgr=PersistencyFolder.new)
      Log.log.debug('Not using persistency')
      # 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 Style/Semicolon
      end.new
    end
    return @persist
  end

  # Delete all existing tokens in cache
  # @return [void]
  def flush_tokens
    persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN)
  end

  # Retrieve all persisted tokens with their decoded information
  # @return [Array<Hash>] Array of token information hashes
  def persisted_tokens
    data = persist_mgr.current_items(PERSIST_CATEGORY_TOKEN)
    data.each.map do |k, v|
      info = {id: k}
      info.merge!(JSON.parse(v)) rescue nil
      d = decode_token(info.delete(TOKEN_FIELD))
      info.merge(d) if d
      info
    end
  end

  # Get token information from cache
  # @param id [String] identifier of token
  # @return [Hash] token internal information , including Date object for `expiration_date`
  def get_token_info(id)
    token_raw_string = persist_mgr.get(id)
    return if token_raw_string.nil?
    token_data = JSON.parse(token_raw_string)
    Aspera.assert_type(token_data, Hash)
    decoded_token = decode_token(token_data[TOKEN_FIELD])
    info = {data: token_data}
    if decoded_token.is_a?(Hash)
      info[:decoded] = decoded_token
      # TODO: move date decoding to token decoder ?
      expiration_date =
        if    decoded_token['expires_at'].is_a?(String) then Time.parse(decoded_token['expires_at']).to_time
        elsif decoded_token['exp'].is_a?(Integer)       then Time.at(decoded_token['exp'])
        end
      unless expiration_date.nil?
        info[:expiration] = expiration_date
        info[:ttl_sec] = expiration_date - Time.now
        info[:expired] = info[:ttl_sec] < @parameters[:token_refresh_threshold]
      end
    end
    Log.dump(:token_info, info)
    return info
  end

  # Register a bearer token decoder for inspecting token properties
  # @param method [Proc] The decoder lambda/proc to register
  # @return [void]
  def register_decoder(method)
    @decoders.push(method)
  end

  # Decode a token using all registered decoders
  # @param token [String] The token to decode
  # @return [Hash, nil] Decoded token data or nil if no decoder succeeded
  def decode_token(token)
    @decoders.each do |decoder|
      result = decoder.call(token) rescue nil
      return result unless result.nil?
    end
    return
  end

  # Register a token creation method
  # @param creator_class [Class] The token creator class to register
  # @return [void]
  def register_token_creator(creator_class)
    Aspera.assert_type(creator_class, Class)
    id = Factory.class_to_id(creator_class)
    Log.log.debug{"registering creator for #{id}"}
    @token_type_classes[id] = creator_class
  end

  # Create a token creator instance for the specified grant method
  # @param parameters [Hash] Parameters including :grant_method and creator-specific options
  # @return [Object] An instance of the registered token creator class
  def create(**parameters)
    Aspera.assert_type(parameters, Hash)
    id = parameters[:grant_method]
    Aspera.assert(@token_type_classes.key?(id)){"token grant method unknown: '#{id}'"}
    create_parameters = parameters.reject{ |k, _v| k.eql?(:grant_method)}
    @token_type_classes[id].new(**create_parameters)
  end
end

Instance Method Details

#create(**parameters) ⇒ Object

Create a token creator instance for the specified grant method

Parameters:

  • parameters (Hash)

    Parameters including :grant_method and creator-specific options

Returns:

  • (Object)

    An instance of the registered token creator class



187
188
189
190
191
192
193
# File 'lib/aspera/oauth/factory.rb', line 187

def create(**parameters)
  Aspera.assert_type(parameters, Hash)
  id = parameters[:grant_method]
  Aspera.assert(@token_type_classes.key?(id)){"token grant method unknown: '#{id}'"}
  create_parameters = parameters.reject{ |k, _v| k.eql?(:grant_method)}
  @token_type_classes[id].new(**create_parameters)
end

#decode_token(token) ⇒ Hash?

Decode a token using all registered decoders

Parameters:

  • token (String)

    The token to decode

Returns:

  • (Hash, nil)

    Decoded token data or nil if no decoder succeeded



166
167
168
169
170
171
172
# File 'lib/aspera/oauth/factory.rb', line 166

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

#flush_tokensvoid

This method returns an undefined value.

Delete all existing tokens in cache



112
113
114
# File 'lib/aspera/oauth/factory.rb', line 112

def flush_tokens
  persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN)
end

#get_token_info(id) ⇒ Hash

Get token information from cache

Parameters:

  • id (String)

    identifier of token

Returns:

  • (Hash)

    token internal information , including Date object for expiration_date



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/aspera/oauth/factory.rb', line 132

def get_token_info(id)
  token_raw_string = persist_mgr.get(id)
  return if token_raw_string.nil?
  token_data = JSON.parse(token_raw_string)
  Aspera.assert_type(token_data, Hash)
  decoded_token = decode_token(token_data[TOKEN_FIELD])
  info = {data: token_data}
  if decoded_token.is_a?(Hash)
    info[:decoded] = decoded_token
    # TODO: move date decoding to token decoder ?
    expiration_date =
      if    decoded_token['expires_at'].is_a?(String) then Time.parse(decoded_token['expires_at']).to_time
      elsif decoded_token['exp'].is_a?(Integer)       then Time.at(decoded_token['exp'])
      end
    unless expiration_date.nil?
      info[:expiration] = expiration_date
      info[:ttl_sec] = expiration_date - Time.now
      info[:expired] = info[:ttl_sec] < @parameters[:token_refresh_threshold]
    end
  end
  Log.dump(:token_info, info)
  return info
end

#persist_mgrObject

Get or initialize the persistence manager

Returns:

  • (Object)

    The persistence manager instance



98
99
100
101
102
103
104
105
106
107
108
# File 'lib/aspera/oauth/factory.rb', line 98

def persist_mgr
  if @persist.nil?
    # use OAuth::Factory.instance.persist_mgr=PersistencyFolder.new)
    Log.log.debug('Not using persistency')
    # 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 Style/Semicolon
    end.new
  end
  return @persist
end

#persist_mgr=(manager) ⇒ Object

Set the persistence manager for token caching

Parameters:

  • manager (Object)

    The persistence manager instance



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

def persist_mgr=(manager)
  @persist = manager
  # cleanup expired tokens
  @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:token_cache_max_age])
end

#persisted_tokensArray<Hash>

Retrieve all persisted tokens with their decoded information

Returns:

  • (Array<Hash>)

    Array of token information hashes



118
119
120
121
122
123
124
125
126
127
# File 'lib/aspera/oauth/factory.rb', line 118

def persisted_tokens
  data = persist_mgr.current_items(PERSIST_CATEGORY_TOKEN)
  data.each.map do |k, v|
    info = {id: k}
    info.merge!(JSON.parse(v)) rescue nil
    d = decode_token(info.delete(TOKEN_FIELD))
    info.merge(d) if d
    info
  end
end

#register_decoder(method) ⇒ void

This method returns an undefined value.

Register a bearer token decoder for inspecting token properties

Parameters:

  • method (Proc)

    The decoder lambda/proc to register



159
160
161
# File 'lib/aspera/oauth/factory.rb', line 159

def register_decoder(method)
  @decoders.push(method)
end

#register_token_creator(creator_class) ⇒ void

This method returns an undefined value.

Register a token creation method

Parameters:

  • creator_class (Class)

    The token creator class to register



177
178
179
180
181
182
# File 'lib/aspera/oauth/factory.rb', line 177

def register_token_creator(creator_class)
  Aspera.assert_type(creator_class, Class)
  id = Factory.class_to_id(creator_class)
  Log.log.debug{"registering creator for #{id}"}
  @token_type_classes[id] = creator_class
end