class Jamf::Connection::Token

A token used for a connection to either API

Constants

API_CLIENT_GRANT_TYPE
API_CLIENT_TOKEN_RSRC
AUTH_RSRC
AUTH_RSRC_VERSION
INVALIDATE_RSRC
JAMF_TRYITOUT_HOST

Recognize the tryitout server, cuz its /auth endpoint is disabled, and it needs no tokens TODO: MOVE THIS TO THE CONNECTION CLASS

JAMF_TRYITOUT_TOKEN_BODY
JAMF_VERSION_RSRC
JAMF_VERSION_RSRC_VERSION
KEEP_ALIVE_RSRC
MIN_REFRESH_BUFFER

Minimum seconds before expiration that the token will automatically refresh. Used as the default if :refresh is not provided in the init params

NEW_TOKEN_RSRC
OAUTH_RSRC
REFRESH_RESULTS

Used bu the last_refresh_result method

Attributes

auth_token[R]

@return [String] The token data

base_url[R]

@return [URI] The base API url, e.g. myjamf.jamfcloud.com/

creation_http_response[R]

@return [Faraday::Response] The response object from instantiating

a new Token object by creating a new token or validating a token
string. This is not updated when refreshing a token, only when
calling Token.new
creation_time[R]

@return [Time] when was this Jamf::Connection::Token originally created?

expiration[R]

@return [Time]

expires[R]

@return [Time]

keep_alive[R]

@return [Boolean] does this token automatically refresh itself before

expiring?
keep_alive?[R]

@return [Boolean] does this token automatically refresh itself before

expiring?
last_refresh[R]

@return [Time] when was this token last refreshed?

login_time[R]

@return [Time] when was this Jamf::Connection::Token originally created?

pw_fallback[R]

@return [Boolean] Should the provided passwd be cached in memory, to be

used to generate a new token, if a normal refresh fails?
pw_fallback?[R]

@return [Boolean] Should the provided passwd be cached in memory, to be

used to generate a new token, if a normal refresh fails?
ssl_options[R]

@return [Hash] the ssl version and verify cert, to pass into faraday connections

ssl_version[R]

@return [String] the SSL version being used

token[R]

@return [String] The token data

token_string[R]

@return [String] The token data

user[R]

@return [String] The user who generated this token

verify_cert[R]

@return [Boolean] are we verifying SSL certs?

verify_cert?[R]

@return [Boolean] are we verifying SSL certs?

Public Class Methods

new(**params) click to toggle source

@param params [Hash] The data for creating and maintaining the token

@option params [String] :token_string An existing valid token string.

If pw_fallback is true (the default) you will also need to provide
the password for the original user in the pw: parameter. If you don't,
pw_fallback will be false even if you set it to true explicitly.

@option params [String, URI] :base_url The url for the Jamf Pro server

including host and port, e.g. 'https://myjss.school.edu:8443/'

@option params [String] :user (see Connection#initialize)

@option params [String] :pw (see Connection#initialize)

@option params [Integer] :timeout The timeout for creating or refreshing

the token

@option params [Boolean] :keep_alive (see Connection#connect)

@option params [Integer] :refresh_buffer (see Connection#connect)

@option params [Boolean] :pw_fallback (see Connection#connect)

@option params [String, Symbol] :ssl_version (see Connection#connect)

@option params [Boolean] :verify_cert (see Connection#connect)

    # File lib/jamf/api/connection/token.rb
156 def initialize(**params)
157   @valid = false
158   parse_params(**params)
159 
160   if params[:token_string]
161     @pw_fallback = false unless @pw
162     init_from_token_string params[:token_string]
163 
164   elsif @client_id
165     init_for_api_client
166 
167   elsif @user && @pw
168     init_from_pw
169 
170   else
171     raise ArgumentError, 'Must provide either user: & pw: or token:'
172   end
173 
174   start_keep_alive if @keep_alive
175   @creation_time = Time.now
176 end

Public Instance Methods

account() click to toggle source

the Jamf account assciated with this token, which contains info about privileges and Jamf acct group memberships and Jamf Acct settings

@return [Jamf::OAPISchemas::AuthorizationV1] the Authorization object

    # File lib/jamf/api/connection/token.rb
349 def account
350   return @account if @account
351 
352   resp = token_connection(AUTH_RSRC, token: @token).get
353   return unless resp.success?
354 
355   @account = Jamf::OAPISchemas::AuthorizationV1.new resp.body
356 end
destroy()
Alias for: invalidate
expired?() click to toggle source

@return [Boolean]

    # File lib/jamf/api/connection/token.rb
270 def expired?
271   return unless @expires
272 
273   Time.now >= @expires
274 end
host() click to toggle source
    # File lib/jamf/api/connection/token.rb
244 def host
245   @base_url.host
246 end
init_for_api_client() click to toggle source

Initialize for an API client

    # File lib/jamf/api/connection/token.rb
180 def init_for_api_client
181   data = {
182     client_id: @client_id,
183     client_secret: Base64.decode64(@pw),
184     grant_type: API_CLIENT_GRANT_TYPE
185   }
186 
187   resp = api_client_auth_connection(API_CLIENT_TOKEN_RSRC).post { |req| req.body = data }
188 
189   if resp.success?
190     parse_token_from_api_client_auth resp
191     @creation_http_response = resp
192     whoami = token_connection(AUTH_RSRC, token: @token).get
193     @user = "#{whoami.body[:account][:username]} (API Client)"
194   elsif resp.status == 401
195     raise Jamf::AuthenticationError, 'Incorrect client_id or client_secret'
196   else
197     # TODO: better error reporting here
198     raise Jamf::AuthenticationError, "An error occurred while authenticating client_id: #{resp.body}"
199   end
200 ensure
201   @pw = nil
202 end
init_from_pw() click to toggle source

Initialize from password

    # File lib/jamf/api/connection/token.rb
206 def init_from_pw
207   resp = token_connection(NEW_TOKEN_RSRC).post
208 
209   if resp.success?
210     parse_token_from_response resp
211     @last_refresh = Time.now
212     @creation_http_response = resp
213   elsif resp.status == 401
214     raise Jamf::AuthenticationError, 'Incorrect name or password'
215   else
216     # TODO: better error reporting here
217     raise Jamf::AuthenticationError, "An error occurred while authenticating: #{resp.body}"
218   end
219 ensure
220   @pw = nil unless @pw_fallback
221 end
init_from_token_string(str) click to toggle source

Initialize from token string

    # File lib/jamf/api/connection/token.rb
225 def init_from_token_string(str)
226   resp = token_connection(AUTH_RSRC, token: str).get
227   raise Jamf::InvalidDataError, 'Token is not valid' unless resp.success?
228 
229   @creation_http_response = resp
230   @token = str
231   @user = resp.body.dig :account, :username
232 
233   # if we were given a pw for the user, and expect to use it, validate it now
234   if @pw && @pw_fallback
235     resp = token_connection(NEW_TOKEN_RSRC).post
236     raise Jamf::AuthenticationError, "Incorrect password provided for token string (user: #{@user})" unless resp.success?
237   end
238 
239   # use this token to get a fresh one with a known expiration
240   refresh
241 end
invalidate() click to toggle source

Make this token invalid

    # File lib/jamf/api/connection/token.rb
399 def invalidate
400   @valid = !token_connection(INVALIDATE_RSRC, token: @token).post.success?
401   @pw = nil
402   stop_keep_alive
403 end
Also aliased as: destroy
jamf_build() click to toggle source

@return [String]

    # File lib/jamf/api/connection/token.rb
263 def jamf_build
264   fetch_jamf_version unless @jamf_build
265   @jamf_build
266 end
jamf_version() click to toggle source

@return [Gem::Version]

    # File lib/jamf/api/connection/token.rb
256 def jamf_version
257   fetch_jamf_version unless @jamf_version
258   @jamf_version
259 end
last_refresh_result() click to toggle source

What happened the last time we tried to refresh? See REFRESH_RESULTS

@return [String, nil] result or nil if never refreshed

    # File lib/jamf/api/connection/token.rb
340 def last_refresh_result
341   REFRESH_RESULTS[@last_refresh_result]
342 end
next_refresh() click to toggle source

when is the next rerefresh going to happen, if we are set to keep alive?

@return [Time, nil] the time of the next scheduled refresh, or nil if not keep_alive?

    # File lib/jamf/api/connection/token.rb
279 def next_refresh
280   return unless keep_alive?
281 
282   @expires - @refresh_buffer
283 end
port() click to toggle source

@return [Integer]

    # File lib/jamf/api/connection/token.rb
250 def port
251   @base_url.port
252 end
refresh() click to toggle source

Use this token to get a fresh one. If a pw is provided try to use it to get a new token if a proper refresh fails.

@param pw [String] Optional password to use if token refresh fails.

Must be the correct passwd or the token's user (obviously)

@return [Time] the new expiration time

    # File lib/jamf/api/connection/token.rb
367 def refresh
368   # already expired?
369   if expired?
370     # try the passwd if we have it
371     return refresh_with_pw(:expired_refreshed, :expired_failed) if @pw
372 
373     # no passwd fallback? no chance!
374     @last_refresh_result = :expired_no_pw_fallback
375     raise Jamf::InvalidTokenError, 'Token has expired'
376   end
377 
378   # Now try a normal refresh of our non-expired token
379   keep_alive_token_resp = token_connection(KEEP_ALIVE_RSRC, token: @token).post
380 
381   if keep_alive_token_resp.success?
382     parse_token_from_response keep_alive_token_resp
383     @last_refresh_result = :refreshed
384     @last_refresh = Time.now
385     return expires
386   end
387 
388   # if we're here, the normal refresh failed, so try the pw
389   return refresh_with_pw(:refreshed_pw, :refresh_failed) if @pw
390 
391   # if we're here, no pw? no chance!
392   @last_refresh_result = :refresh_failed_no_pw_fallback
393   raise 'An error occurred while refreshing the token'
394 end
Also aliased as: keep_alive
secs_remaining() click to toggle source

@return [Float]

    # File lib/jamf/api/connection/token.rb
308 def secs_remaining
309   return unless @expires
310 
311   @expires - Time.now
312 end
secs_to_refresh() click to toggle source

how many secs until the next refresh? will return 0 during the actual refresh process.

@return [Float, nil] Seconds until the next scheduled refresh, or nil if not keep_alive?

    # File lib/jamf/api/connection/token.rb
290 def secs_to_refresh
291   return unless keep_alive?
292 
293   secs = next_refresh - Time.now
294   secs.negative? ? 0 : secs
295 end
start_keep_alive() click to toggle source

creates a thread that loops forever, sleeping most of the time, but waking up every 60 seconds to see if the token is expiring in the next @refresh_buffer seconds.

If so, the token is refreshed, and we keep looping and sleeping.

Sets @keep_alive_thread to the Thread object

@return [void]

    # File lib/jamf/api/connection/token.rb
416 def start_keep_alive
417   return if @keep_alive_thread
418   raise 'Token expired, cannot refresh' if expired?
419 
420   @keep_alive_thread =
421     Thread.new do
422       loop do
423         sleep 60
424         begin
425           next if secs_remaining > @refresh_buffer
426 
427           refresh
428         rescue
429           # TODO: Some kind of error reporting
430           next
431         end
432       end # loop
433     end # thread
434 end
stop_keep_alive() click to toggle source

Kills the @keep_alive_thread, if it exists, and sets @keep_alive_thread to nil

@return [void]

    # File lib/jamf/api/connection/token.rb
441 def stop_keep_alive
442   return unless @keep_alive_thread
443 
444   @keep_alive_thread.kill if @keep_alive_thread.alive?
445   @keep_alive_thread = nil
446 end
time_remaining() click to toggle source

@return [String] e.g. “1 week 6 days 23 hours 49 minutes 56 seconds”

    # File lib/jamf/api/connection/token.rb
316 def time_remaining
317   return unless @expires
318 
319   JSS.humanize_secs secs_remaining
320 end
time_to_refresh() click to toggle source

Returns e.g. “1 week 6 days 23 hours 49 minutes 56 seconds”

@return [String, nil]

    # File lib/jamf/api/connection/token.rb
300 def time_to_refresh
301   return unless keep_alive?
302 
303   Jamf.humanize_secs secs_to_refresh
304 end
valid?() click to toggle source

@return [Boolean]

    # File lib/jamf/api/connection/token.rb
324 def valid?
325   @valid =
326     if expired?
327       false
328     elsif !@token
329       false
330     else
331       token_connection(AUTH_RSRC, token: @token).get.success?
332     end
333 end

Private Instance Methods

api_client_auth_connection(rsrc) click to toggle source

a generic, one-time Faraday connection for API Client token acquision & manipulation

    # File lib/jamf/api/connection/token.rb
548 def api_client_auth_connection(rsrc)
549   Faraday.new("#{@base_url}/#{Jamf::Connection::JPAPI_RSRC_BASE}/#{rsrc}", ssl: @ssl_options) do |fcnx|
550     # activates the url_encoded request middleware, which converts a hash in the request body to url-encoded form data
551     fcnx.request :url_encoded
552     # activates the json response middleware, parsing all valid  response bodies with JSON.parse
553     fcnx.response :json, parser_options: { symbolize_names: true }
554     fcnx.adapter  :net_http
555   end
556 end
fetch_jamf_version() click to toggle source

@return [void]

    # File lib/jamf/api/connection/token.rb
516 def fetch_jamf_version
517   resp = token_connection(JAMF_VERSION_RSRC, token: @token).get
518   if resp.success?
519     jamf_version, @jamf_build = resp.body[:version].split('-')
520     @jamf_version = Gem::Version.new jamf_version
521     return
522   end
523 
524   raise Jamf::InvalidConnectionError, 'Unable to read Jamf version from the API'
525 end
parse_params(**params) click to toggle source

set values from params & defaults

    # File lib/jamf/api/connection/token.rb
454 def parse_params(**params)
455   # This process of deleting suffixes will leave in place any
456   # URL paths before the the CAPI_RSRC_BASE or JPAPI_RSRC_BASE
457   # e.g.  https://my.jamf.server:8443/some/path/before/api
458   # as is the case at some on-prem sites.
459   baseurl = params[:base_url].to_s.dup
460   baseurl.delete_suffix! '/'
461   baseurl.delete_suffix! Jamf::Connection::CAPI_RSRC_BASE
462   baseurl.delete_suffix! Jamf::Connection::JPAPI_RSRC_BASE
463   baseurl.delete_suffix! '/'
464   @base_url = URI.parse baseurl
465 
466   @timeout = params[:timeout] || Jamf::Connection::DFT_TIMEOUT
467 
468   if params[:client_id]
469     @client_id = params[:client_id]
470     params[:keep_alive] = false
471     params[:refresh_buffer] = nil
472     params[:pw_fallback] = false
473   else
474     @user = params[:user]
475   end
476 
477   # when using an API client, the client secret can be
478   # given in params[:client_secret] or params[:pw]
479   params[:pw] ||= params[:client_secret]
480 
481   # @pw will be deleted after use if pw_fallback is false
482   # It is stored as base64 merely for visual security in irb sessions
483   # and the like.
484   @pw = params[:pw] ? Base64.encode64(params[:pw]) : nil
485   @pw_fallback = params[:pw_fallback].instance_of?(FalseClass) ? false : true
486 
487   # backwards compatibility
488   params[:refresh_buffer] ||= params[:refresh]
489   params[:refresh_buffer] = params[:refresh_buffer].to_i
490   @refresh_buffer = params[:refresh_buffer] > MIN_REFRESH_BUFFER ? params[:refresh_buffer] : MIN_REFRESH_BUFFER
491 
492   @ssl_version = params[:ssl_version] || Jamf::Connection::DFT_SSL_VERSION
493   @verify_cert = params[:verify_cert].instance_of?(FalseClass) ? false : true
494   @ssl_options = { version: @ssl_version, verify: @verify_cert }
495 
496   @keep_alive = params[:keep_alive].instance_of?(FalseClass) ? false : true
497 end
parse_token_from_api_client_auth(resp) click to toggle source

Parse the API Client token data into instance vars.

    # File lib/jamf/api/connection/token.rb
569 def parse_token_from_api_client_auth(resp)
570   @token_response_body = resp.body
571   @token = @token_response_body[:access_token]
572   @expires = Time.now + @token_response_body[:expires_in] - 1
573   @api_role_ids = @token_response_body[:scope].split(' ').map { |r| r.split(':').last }
574   @valid = true
575 end
parse_token_from_response(resp) click to toggle source

Parse the API token data into instance vars.

    # File lib/jamf/api/connection/token.rb
560 def parse_token_from_response(resp)
561   @token_response_body = resp.body
562   @token = @token_response_body[:token]
563   @expires = Jamf.parse_time(@token_response_body[:expires]).localtime
564   @valid = true
565 end
refresh_with_pw(success, failure) click to toggle source

refresh a token using the pw cached when @pw_fallback is true

@param success [Sumbol] the key from REFRESH_RESULTS to use when successful @param failure [Sumbol] the key from REFRESH_RESULTS to use when not successful @return [Time] the new expiration

    # File lib/jamf/api/connection/token.rb
505 def refresh_with_pw(success, failure)
506   init_from_pw
507   @last_refresh_result = success
508   expires
509 rescue => e
510   @last_refresh_result = failure
511   raise e, "#{e}. Status: :#{REFRESH_RESULTS[failure]}"
512 end
token_connection(rsrc, token: nil) click to toggle source

a generic, one-time Faraday connection for token acquision & manipulation

    # File lib/jamf/api/connection/token.rb
530 def token_connection(rsrc, token: nil)
531   Faraday.new("#{@base_url}/#{Jamf::Connection::JPAPI_RSRC_BASE}/#{rsrc}", ssl: @ssl_options) do |con|
532     con.headers[:accept] = Jamf::Connection::MIME_JSON
533     con.response :json, parser_options: { symbolize_names: true }
534     con.options[:timeout] = @timeout
535     con.options[:open_timeout] = @timeout
536     if token
537       con.request :authorization, 'Bearer', token
538     else
539       con.request :authorization, :basic, @user, Base64.decode64(@pw)
540     end
541     con.adapter :net_http
542   end # Faraday.new
543 end