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
@return [String] The token data
@return [URI] The base API url, e.g. myjamf.jamfcloud.com/
@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
@return [Time] when was this Jamf::Connection::Token
originally created?
@return [Time]
@return [Time]
@return [Boolean] does this token automatically refresh itself before
expiring?
@return [Boolean] does this token automatically refresh itself before
expiring?
@return [Time] when was this token last refreshed?
@return [Time] when was this Jamf::Connection::Token
originally created?
@return [Boolean] Should the provided passwd be cached in memory, to be
used to generate a new token, if a normal refresh fails?
@return [Boolean] Should the provided passwd be cached in memory, to be
used to generate a new token, if a normal refresh fails?
@return [Hash] the ssl version and verify cert, to pass into faraday connections
@return [String] the SSL version being used
@return [String] The token data
@return [String] The token data
@return [String] The user who generated this token
@return [Boolean] are we verifying SSL certs?
@return [Boolean] are we verifying SSL certs?
Public Class Methods
@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
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
@return [Boolean]
# File lib/jamf/api/connection/token.rb 270 def expired? 271 return unless @expires 272 273 Time.now >= @expires 274 end
# File lib/jamf/api/connection/token.rb 244 def host 245 @base_url.host 246 end
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
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
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
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
@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
@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
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
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
@return [Integer]
# File lib/jamf/api/connection/token.rb 250 def port 251 @base_url.port 252 end
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
@return [Float]
# File lib/jamf/api/connection/token.rb 308 def secs_remaining 309 return unless @expires 310 311 @expires - Time.now 312 end
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
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
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
@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
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
@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
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
@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
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 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 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 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
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