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(:refresh=>true)

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.auth_typesObject

OAuth methods supported



22
23
24
# File 'lib/aspera/oauth.rb', line 22

def auth_types
  [ :body_userpass, :header_userpass, :web, :jwt, :url_token, :ibm_apikey ]
end

.flush_tokensObject



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

def flush_tokens
  persist_mgr.flush_by_prefix(PERSIST_CATEGORY_TOKEN)
end

.goto_page_and_get_request(redirect_uri, login_page_url, html_page = THANK_YOU_HTML) ⇒ Object

open the login page, wait for code and return parameters



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/aspera/oauth.rb', line 292

def self.goto_page_and_get_request(redirect_uri,,html_page=THANK_YOU_HTML)
  Log.log.info "login_page_url=#{login_page_url}".bg_red().gray()
  # browser start is not blocking, we hope here that starting is slower than opening port
  OpenApplication.instance.uri()
  port=URI.parse(redirect_uri).port
  Log.log.info "listening on port #{port}"
  request_params=nil
  TCPServer.open('127.0.0.1', port) { |webserver|
    Log.log.info "server=#{webserver}"
    websession = webserver.accept
    sleep 1 # TODO: sometimes: returns nil ? use webrick ?
    line = websession.gets.chomp
    Log.log.info "line=#{line}"
    if ! line.start_with?('GET /?') then
      raise "unexpected request"
    end
    request = line.partition('?').last.partition(' ').first
    data=URI.decode_www_form(request)
    request_params=data.to_h
    Log.log.debug "request_params=#{request_params}"
    websession.print "HTTP/1.1 200/OK\r\nContent-type:text/html\r\n\r\n#{html_page}"
    websession.close
  }
  return request_params
end

.persist_mgrObject



30
31
32
33
# File 'lib/aspera/oauth.rb', line 30

def persist_mgr
  raise "set persistency manager first" if @persist.nil?
  return @persist
end

.persist_mgr=(manager) ⇒ Object



26
27
28
# File 'lib/aspera/oauth.rb', line 26

def persist_mgr=(manager)
  @persist=manager
end

Instance Method Details

#get_authorization(options = {}) ⇒ Object

Parameters:

  • options (defaults to: {})

    : :scope and :refresh



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
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
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
282
283
284
285
286
287
288
289
# File 'lib/aspera/oauth.rb', line 121

def get_authorization(options={})
  # api scope can be overriden to get auth for other scope
  api_scope=options[:scope] || @params[:scope]
  # as it is optional in many place: create struct
  p_scope={}
  p_scope[:scope] = api_scope unless api_scope.nil?
  p_client_id_and_scope=p_scope.clone
  p_client_id_and_scope[:client_id] = @params[:client_id] if @params.has_key?(:client_id)
  use_refresh_token=options[:refresh]

  # generate token identifier to use with cache
  token_ids=token_cache_ids(api_scope)

  # get token_data from cache (or nil), token_data is what is returned by /token
  token_data=self.class.persist_mgr.get(token_ids)
  token_data=JSON.parse(token_data) unless token_data.nil?

  # Optional optimization: check if node token is expired, then force refresh
  # in case the transfer agent cannot refresh himself
  # else, anyway, faspmanager is equipped with refresh code
  if !token_data.nil?
    decoded_node_token = Node.decode_bearer_token(token_data['access_token']) rescue nil
    if decoded_node_token.is_a?(Hash) and decoded_node_token['expires_at'].is_a?(String)
      Log.dump('decoded_node_token',decoded_node_token)
      expires_at=DateTime.parse(decoded_node_token['expires_at'])
      one_hour_as_day_fraction=Rational(1,24)
      use_refresh_token=true if DateTime.now > (expires_at-one_hour_as_day_fraction)
    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) and token_data.has_key?('refresh_token')
      # save possible refresh token, before deleting the cache
      refresh_token=token_data['refresh_token']
    end
    # delete caches
    self.class.persist_mgr.delete(token_ids)
    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: admin token has no refresh, and lives by default 1800secs
      # Note: scope is mandatory in Files, and we can either provide basic auth, or client_Secret in data
      resp=create_token_www_body(p_client_id_and_scope.merge({
        :grant_type   =>'refresh_token',
        :refresh_token=>refresh_token}))
      if resp[:http].code.start_with?('2') then
        # save only if success ?
        json_data=resp[:http].body
        token_data=JSON.parse(json_data)
        self.class.persist_mgr.put(token_ids,json_data)
      else
        Log.log.debug("refresh failed: #{resp[:http].body}".bg_red)
      end
    end
  end

  # no cache
  if token_data.nil? then
    resp=nil
    case @params[:grant]
    when :web
      # AoC Web based Auth
      check_code=SecureRandom.uuid
      =Rest.build_uri(
      "#{@params[:base_url]}/#{@params[:path_authorize]}",
      p_client_id_and_scope.merge({
        :response_type => 'code',
        :redirect_uri  => @params[:redirect_uri],
        :client_secret => @params[:client_secret],
        :state         => check_code
      }))
      # here, we need a human to authorize on a web page
      code=goto_page_and_get_code(,check_code)
      # exchange code for token
      resp=create_token_www_body(p_client_id_and_scope.merge({
        :grant_type   => 'authorization_code',
        :code         => code,
        :redirect_uri => @params[:redirect_uri]
      }))
    when :jwt
      # https://tools.ietf.org/html/rfc7519
      # https://tools.ietf.org/html/rfc7523
      require 'jwt'
      seconds_since_epoch=Time.new.to_i
      Log.log.info("seconds=#{seconds_since_epoch}")

      payload = {
        :iss => @params[:client_id],    # issuer
        :sub => @params[:jwt_subject],  # subject
        :aud => @params[:jwt_audience], # audience
        :nbf => seconds_since_epoch-JWT_NOTBEFORE_OFFSET, # not before
        :exp => seconds_since_epoch+JWT_EXPIRY_OFFSET # expiration
      }

      # non standard, only for global ids
      payload.merge!(@params[:jwt_add]) if @params.has_key?(:jwt_add)

      rsa_private=@params[:jwt_private_key_obj]  # type: OpenSSL::PKey::RSA

      Log.log.debug("private=[#{rsa_private}]")

      Log.log.debug("JWT assertion=[#{payload}]")
      assertion = JWT.encode(payload, rsa_private, 'RS256')

      Log.log.debug("assertion=[#{assertion}]")

      resp=create_token_www_body(p_scope.merge({
        :grant_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        :assertion  => assertion
      }))
    when :url_token
      # AoC Public Link
      resp=create_token_advanced({
        :json_params => {:url_token=>@params[:url_token]},
        :url_params  => p_scope.merge({
        :grant_type    => 'url_token'
        })})
    when :ibm_apikey
      # ATS
      resp=create_token_www_body({
        'grant_type'    => 'urn:ibm:params:oauth:grant-type:apikey',
        'response_type' => 'cloud_iam',
        'apikey'        => @params[:api_key]
      })
    when :delegated_refresh
      # COS
      resp=create_token_www_body({
        'grant_type'          => 'urn:ibm:params:oauth:grant-type:apikey',
        'response_type'       => 'delegated_refresh_token',
        'apikey'              => @params[:api_key],
        'receiver_client_ids' => 'aspera_ats'
      })
    when :header_userpass
      # used in Faspex apiv4 and shares2
      resp=create_token_advanced({
        :auth        => {
        :type          => :basic,
        :username      => @params[:user_name],
        :password      => @params[:user_pass]},
        :json_params => p_client_id_and_scope.merge({:grant_type => 'password'}), #:www_body_params also works
      })
    when :body_userpass
      # legacy, not used
      resp=create_token_www_body(p_client_id_and_scope.merge({
        :grant_type => 'password',
        :username   => @params[:user_name],
        :password   => @params[:user_pass]
      }))
    when :body_data
      # used in Faspex apiv5
      resp=create_token_advanced({
        :auth        => {:type => :none},
        :json_params => @params[:userpass_body],
      })
    else
      raise "auth grant type unknown: #{@params[:grant]}"
    end
    # TODO: test return code ?
    json_data=resp[:http].body
    token_data=JSON.parse(json_data)
    self.class.persist_mgr.put(token_ids,json_data)
  end # if ! in_cache

  # ok we shall have a token here
  return 'Bearer '+token_data[@params[:token_field]]
end