@@ -304,12 +304,13 @@ def fetch_all(method, params = nil, auth: default_auth_mode,
304304 # - `:logged_in` if a login using a password was performed
305305 # - `:refreshed` if the access token was expired and was refreshed
306306 # - `:ok` if no refresh was needed
307+ # - `:unknown` if the token is not a valid JWT (e.g. an opaque blob)
307308 #
308309 # @raise [BadResponse] if login or refresh returns an error status code
309310 # @raise [AuthError]
311+ # - if the client doesn't include user config at all
310312 # - if logging in is required, but login or password isn't provided
311313 # - if token refresh is needed, but refresh token is missing
312- # - if a token has invalid format
313314
314315 def check_access
315316 if !user
@@ -318,8 +319,16 @@ def check_access
318319 raise AuthError , "User id or password is missing"
319320 elsif !user . logged_in?
320321 log_in
321- :logged_in
322- elsif access_token_expired?
322+ return :logged_in
323+ end
324+
325+ begin
326+ expired = access_token_expired?
327+ rescue AuthError
328+ return :unknown
329+ end
330+
331+ if expired
323332 perform_token_refresh
324333 :refreshed
325334 else
@@ -390,29 +399,58 @@ def perform_token_refresh
390399 json
391400 end
392401
402+ # Attempts to parse a given token as JWT and extract the expiration date from the payload.
403+ # An access token technically isn't required to be a (valid) JWT, so if the parsing fails
404+ # for whatever reason, nil is returned.
405+ #
406+ # @return [Time, nil] parsed expiration time, or nil if token is not a valid JWT
407+
393408 def token_expiration_date ( token )
409+ return nil unless token . valid_encoding?
410+
394411 parts = token . split ( '.' )
395- raise AuthError , "Invalid access token format" unless parts . length == 3
412+ return nil unless parts . length == 3
396413
397414 begin
398415 payload = JSON . parse ( Base64 . decode64 ( parts [ 1 ] ) )
399416 rescue JSON ::ParserError
400- raise AuthError , "Couldn't decode payload from access token"
417+ return nil
401418 end
402419
403420 exp = payload [ 'exp' ]
404- raise AuthError , "Invalid token expiry data" unless exp . is_a? ( Numeric ) && exp > 0
421+ return nil unless exp . is_a? ( Numeric ) && exp > 0
405422
406- Time . at ( exp )
423+ time = Time . at ( exp )
424+ return nil if time . year < 2026 || time . year > 2100
425+
426+ time
407427 end
408428
429+ # Attempts to parse the user's access token as JWT, extract the expiration date from the
430+ # payload, and check if the token hasn't expired yet.
431+ #
432+ # @return [Boolean] true if the token's expiration time is more than a minute away
433+ # @raise [AuthError] if the token is not a valid JWT, or user is not logged in
434+
409435 def access_token_expired?
410- token_expiration_date ( user . access_token ) < Time . now + 60
436+ if user &.access_token . nil?
437+ raise AuthError , "No access token (user is not logged in)"
438+ end
439+
440+ exp_date = token_expiration_date ( user . access_token )
441+
442+ if exp_date
443+ exp_date < Time . now + 60
444+ else
445+ raise AuthError , "Token expiration date cannot be decoded"
446+ end
411447 end
412448
413449 #
414450 # Clear stored access and refresh tokens, effectively logging out the user.
415451 #
452+ # @raise [AuthError] if the client doesn't have a user config
453+ #
416454
417455 def reset_tokens
418456 if !user
0 commit comments