Class: Aspera::OAuth::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/oauth/base.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) tools.ietf.org/html/rfc6749

Direct Known Subclasses

Generic, Jwt, UrlJson, Web

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(base_url:, auth: nil, client_id: nil, client_secret: nil, scope: nil, path_token: 'token', token_field: 'access_token') ⇒ Base

[M]=mandatory [D]=has default value [O]=Optional/nil



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
# File 'lib/aspera/oauth/base.rb', line 28

def initialize(
  base_url:,
  auth: nil,
  client_id: nil,
  client_secret: nil,
  scope: nil,
  path_token:  'token',       # default endpoint for /token to generate token
  token_field: 'access_token' # field with token in result of call to path_token
)
  Aspera.assert_type(base_url, String)
  Aspera.assert(respond_to?(:create_token), 'create_token method must be defined', exception_class: InternalError)
  @base_url = base_url
  @path_token = path_token
  @token_field = token_field
  @client_id = client_id
  @client_secret = client_secret
  @scope = scope
  @identifiers = []
  @identifiers.push(auth[:username]) if auth.is_a?(Hash) && auth.key?(:username)
  # this is the OAuth API
  @api = Rest.new(
    base_url:     @base_url,
    redirect_max: 2,
    auth:         auth)
end

Instance Attribute Details

#scope=(value) ⇒ Object (writeonly)

scope can be modified after creation



18
19
20
# File 'lib/aspera/oauth/base.rb', line 18

def scope=(value)
  @scope = value
end

Instance Method Details

#create_token_call(www_params) ⇒ Object

helper method to create token as per RFC



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

def create_token_call(www_params)
  Log.log.debug{'Generating a new token'.bg_green}
  return @api.call(
    operation:       'POST',
    subpath:         @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



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
# File 'lib/aspera/oauth/base.rb', line 75

def get_authorization(use_refresh_token: false, use_cache: true)
  # generate token unique identifier for persistency (memory/disk cache)
  token_id = IdGenerator.from_list(Factory.id(
    @base_url,
    @grant_method,
    @identifiers,
    @scope
  ))

  # get token_data from cache (or nil), token_data is what is returned by /token
  token_data = Factory.instance.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 = OAuth::Factory.instance.decode_token(token_data[@token_field])
    Log.log.debug{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) < OAuth::Factory.instance.globals[: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
    Factory.instance.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_call(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)
        Factory.instance.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 = create_token
    json_data = resp[:http].body
    token_data = JSON.parse(json_data)
    Factory.instance.persist_mgr.put(token_id, json_data)
  end # if ! in_cache
  Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
  # ok we shall have a token here
  return OAuth::Factory.bearer_build(token_data[@token_field])
end

#optional_scope_client_id(add_secret: false) ⇒ Object



65
66
67
68
69
70
71
# File 'lib/aspera/oauth/base.rb', line 65

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