Class CachingTokenSource

java.lang.Object
co.ankatech.ankasecure.sdk.auth.CachingTokenSource
All Implemented Interfaces:
TokenSource, AutoCloseable
Direct Known Subclasses:
FederatedAssertionTokenSource, NativeSecretTokenSource, PrivateKeyJwtTokenSource

public abstract class CachingTokenSource extends Object implements TokenSource
The caching, single-flight, fail-without-poisoning half of every TokenSource, so the three mechanisms differ in exactly one method and cannot drift apart in the parts that are not about the credential at all.

Four guarantees, taken from a cache the platform already runs

These are the guarantees OAuthTokenCache in ankasecure-integration-support provides on the server side. They are re-implemented rather than imported because the SDK is a standalone client-layer artifact with no internal dependencies, and acquiring one to reuse seventy lines would put the whole integration tier on a customer's classpath.

  • Reuse within validity - a cached token outside the refresh window is returned without touching the token endpoint.
  • Refresh-before-expiry skew - a token within sixty seconds of expiry is treated as already expired. The caller's clock and the server's clock are not the same clock, and the token still has to survive the flight time of the operation it was fetched for. Sixty seconds is the platform's own number: the same window the server-side cache uses and the same skew the actor-proof verifier admits.
  • Single-flight - one thread mints and the rest observe its result, so a thousand threads waking to an expired token produce one token request, not a thousand.
  • No poison on failure - a failed mint leaves any previously cached token exactly as it was. A transient outage must not turn a working session into a broken one.

Tokens live only in this object's memory. They are never logged, never written to disk, never placed in an exception message, and are zeroized as far as the JVM permits on invalidate() and close().

Minting binds

Every path that produces a token also binds it to the transport, and accessToken() re-binds even on a cache hit. Binding is therefore idempotent and the transport cannot be left holding a token this source has already discarded.

  • Method Details

    • accessToken

      public final String accessToken() throws AnkaSecureSdkException
      Description copied from interface: TokenSource
      Returns a token that is valid now, minting one if none is cached or the cached one is inside the refresh window, and binding whatever it returns to the transport.

      Concurrent callers of a stale source do not stampede the token endpoint: exactly one mints and the rest observe its result.

      Specified by:
      accessToken in interface TokenSource
      Returns:
      the ANKASecure access token; never null and never blank
      Throws:
      AnkaSecureSdkException - if a token cannot be obtained. A failure leaves any previously cached token untouched rather than poisoning the cache with the failure.
    • invalidate

      public void invalidate()
      Discards the cached token and unbinds it from the transport.

      Not final, and the reason is a defect it used to have. A source whose credential is itself produced by something that caches - the federated one, whose provider may hold an external token under that issuer's stated lifetime - needs invalidation to reach through to it. While this method was sealed it could not, so a caller invalidating after an external secret was rotated got a fresh ANKASecure mint built from the same stale external token, which is exactly the failure they were clearing. An override extends the reach; it must still call super.invalidate(), because the unbinding below is the part no subclass may skip.

      Specified by:
      invalidate in interface TokenSource
    • close

      public void close()
      Discards the cached token. Subclasses holding a credential for re-minting override this, release it, and call super.close().

      It performs the discard directly rather than by calling invalidate(). Once that method became overridable, routing close through it re-entered the override - so closing a federated source told its provider twice that its token was dead, the second time after the provider had already been closed. Closing releases; invalidating is a caller saying a credential went stale. They are different statements and only one of them belongs in a shutdown path.

      Specified by:
      close in interface AutoCloseable
      Specified by:
      close in interface TokenSource
    • currentTokenExpiry

      public final Instant currentTokenExpiry()
      Description copied from interface: TokenSource
      When the token currently in force expires, if this source knows.

      Known for any source that minted its own token, because the issuer states the lifetime in the same response that carried it. Absent for a token the caller supplied from elsewhere - that one arrived with no accompanying statement, and this SDK does not read a token's claims to find out.

      Specified by:
      currentTokenExpiry in interface TokenSource
      Returns:
      the expiry, or null when nothing is cached or the lifetime is not known
    • cachedExpiry

      public final Instant cachedExpiry()
      The instant the currently cached token expires.
      Returns:
      the expiry, or null when nothing is cached
    • hasCachedToken

      public final boolean hasCachedToken()
      Whether a token is currently cached. Reveals no token value.
      Returns:
      true when a token is held