Class: Ksef::Auth::AccessToken
- Inherits:
-
Object
- Object
- Ksef::Auth::AccessToken
- Defined in:
- lib/ksef/auth/access_token.rb
Overview
Holds the redeemed token pair and keeps the access token fresh (DESIGN.md §5.1, §6.3; docs/REFERENCE.md §4.2).
Expiry comes from the response, never from the JWT
The access token is a JWT, and it is tempting to read exp out of it. This gem does
not, and that is a locked decision: DESIGN.md §4.3 excludes the jwt dependency and
treats the token as an opaque bearer string, and the contract's TokenInfo carries
validUntil precisely so no decoding is needed. (§6.3 used to say "expiry in exp",
contradicting §4.3; corrected 2026-08-23.)
Why refresh early rather than on expiry
Refreshing at ~80% of the token's life means a request never carries a credential that expires mid-flight. Waiting for expiry guarantees the opposite: the first request after the deadline fails, and on a non-idempotent call — an invoice submission — a failure that might have been delivered is exactly the situation this gem works hardest to avoid (DESIGN.md §6.7).
Thread safety
A Ksef::Client is shareable across threads (DESIGN.md §5.2), so this is the piece
that has to be safe: one mutex guards the pair, and the staleness check is re-run
inside the lock so a burst of threads produces one refresh rather than a stampede.
Not in scope here: the 401 refresh-and-replay of §6.3. That needs to see the response, so it belongs to the HTTP layer; this class only refreshes on time.
Constant Summary collapse
- REDACTED =
"[REDACTED]"- REFRESH_THRESHOLD =
Fraction of the access token's observed lifetime after which it is considered stale. "~80%" per §6.3 — the docs describe the lifetime only as "kilkanaście minut", so a proportion travels better than a fixed number of seconds.
0.8
Instance Method Summary collapse
-
#bearer ⇒ String
The bearer string for an
Authorizationheader, refreshing first if the token has gone stale. - #expired?(now = @clock.call) ⇒ Boolean
-
#initialize(tokens, client:, clock: -> { Time.now }, threshold: REFRESH_THRESHOLD) ⇒ AccessToken
constructor
A new instance of AccessToken.
- #inspect ⇒ Object
-
#refresh! ⇒ self
Forces a refresh regardless of staleness.
-
#refresh_token_expired?(now = @clock.call) ⇒ Boolean
The refresh token is valid up to seven days and is reusable (§4.2).
-
#stale?(now = @clock.call) ⇒ Boolean
True once the token is past REFRESH_THRESHOLD of its observed lifetime.
-
#to_s ⇒ Object
Both redacted,
#to_sincluded: these are live credentials, and interpolating one into a log line is how they escape (DESIGN.md §4.5). -
#valid_until ⇒ Time?
When the current access token stops being valid.
Constructor Details
#initialize(tokens, client:, clock: -> { Time.now }, threshold: REFRESH_THRESHOLD) ⇒ AccessToken
Returns a new instance of AccessToken.
44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/ksef/auth/access_token.rb', line 44 def initialize(tokens, client:, clock: -> { Time.now }, threshold: REFRESH_THRESHOLD) @client = client @clock = clock @threshold = threshold @mutex = Mutex.new @access = tokens.access_token @refresh = tokens.refresh_token # The issue time is not in the response, so the lifetime is measured from when we # took delivery. That can only *under*-estimate the remaining life, which errs the # safe way: we refresh slightly early rather than slightly late. @acquired_at = @clock.call end |
Instance Method Details
#bearer ⇒ String
The bearer string for an Authorization header, refreshing first if the token has
gone stale.
This may perform a network call — deliberately named #bearer rather than
#token so that is not a surprise at the call site.
@access.nil? is not a paranoid guard: TokenInfo.from(nil) returns nil, so a
redeem response missing its accessToken produces a pair with no token at all.
Without this arm that state reaches nil.token and reports NoMethodError from
deep inside the client, instead of saying the credential is unusable.
68 69 70 71 72 73 |
# File 'lib/ksef/auth/access_token.rb', line 68 def bearer @mutex.synchronize do renew! if @access.nil? || stale_unlocked? @access.token end end |
#expired?(now = @clock.call) ⇒ Boolean
86 |
# File 'lib/ksef/auth/access_token.rb', line 86 def expired?(now = @clock.call) = @access.nil? || @access.expired?(now) |
#inspect ⇒ Object
106 107 108 109 |
# File 'lib/ksef/auth/access_token.rb', line 106 def inspect "#<Ksef::Auth::AccessToken token=#{REDACTED} valid_until=#{valid_until.inspect} " \ "stale=#{stale?} refresh=#{REDACTED}>" end |
#refresh! ⇒ self
Forces a refresh regardless of staleness.
78 79 80 81 |
# File 'lib/ksef/auth/access_token.rb', line 78 def refresh! @mutex.synchronize { renew! } self end |
#refresh_token_expired?(now = @clock.call) ⇒ Boolean
The refresh token is valid up to seven days and is reusable (§4.2). Once it lapses there is no way back but a full re-authentication.
100 |
# File 'lib/ksef/auth/access_token.rb', line 100 def refresh_token_expired?(now = @clock.call) = @refresh.nil? || @refresh.expired?(now) |
#stale?(now = @clock.call) ⇒ Boolean
True once the token is past REFRESH_THRESHOLD of its observed lifetime.
False when the lifetime cannot be established, which is the conservative answer: a
validUntil we could not parse is no reason to spend a refresh, and a genuinely
expired token still surfaces as a 401 from the API.
93 94 95 96 |
# File 'lib/ksef/auth/access_token.rb', line 93 def stale?(now = @clock.call) deadline = refresh_deadline !deadline.nil? && now >= deadline end |
#to_s ⇒ Object
Both redacted, #to_s included: these are live credentials, and interpolating one
into a log line is how they escape (DESIGN.md §4.5).
104 |
# File 'lib/ksef/auth/access_token.rb', line 104 def to_s = REDACTED |
#valid_until ⇒ Time?
Returns when the current access token stops being valid.
84 |
# File 'lib/ksef/auth/access_token.rb', line 84 def valid_until = @access&.valid_until |