Authentication
Every client (Entity, Content, Workflow, Process Monitor and User) authenticates with Preservica in exactly the same way. The logic lives in one shared, internal Client class that each specific client delegates to, so there is nothing extra you need to do to authenticate: simply create a client (see the “Creating a client” section on each client’s usage page) and every method call will transparently obtain a token before making its API request.
The auth flow
Before any request is sent to Preservica, the client calls an internal getAuthenticationToken method. This returns a TokenDetails (an access token plus the Preservica apiUrl to call), either from the cache or freshly fetched:
- Check the cache for a
TokenDetailsvalue. - If a value is found, return it immediately - no network calls are made.
- If no value is found (e.g. this is the first call, or the previous token has expired), fetch a new one:
- Build a
SecretsManagerAsyncClientpointing at the configured Secrets Manager endpoint (defaults to the regional AWS endpoint, but can be overridden, for example to reach a private VPC endpoint or a test server). - Call
getSecretValueto retrieve the secret named bysecretName. This secret is expected to contain JSON with auserName,passwordandapiUrl. POSTthe username and password to{apiUrl}/api/accesstoken/login. Preservica responds with an access token.- Wrap the token and
apiUrlin aTokenDetailsand write it into the cache, with a time-to-live equal to the configured cacheduration(default 15 minutes). - Add the token to the outgoing request as a
Preservica-Access-Tokenheader, then send the request.
Caching
The token is cached in memory using a Caffeine cache (via scalacache), scoped to a single client instance. This avoids calling Secrets Manager and the Preservica login endpoint on every single API request - instead, a token is fetched once and reused for the configured duration, which should be set to (at most) the actual lifetime of tokens issued by Preservica.
Avoiding duplicate token fetches
Client methods are frequently called from many concurrent fibers/threads at once, for example when paginating through results in parallel. If the cached token has expired, a naive implementation would have every one of those concurrent callers independently notice the empty cache and independently call Secrets Manager and the Preservica login endpoint at the same time. Besides being wasteful, a large burst of simultaneous Secrets Manager calls can overwhelm any proxy or firewall sitting in front of it, causing failures.
To prevent this, the client internally tracks whether a token fetch is already in progress using a single, effectful “single-flight” guard: a cats-effect Ref holding an optional Deferred. The snippets below are a standalone, illustrative version of the same pattern used by the real client:
// None means no fetch is in progress. Some(deferred) means a fetch is in progress, and `deferred` will be
// completed with its result (success or failure) once that fetch finishes.
private val tokenRefreshInFlight: Ref[IO, Option[Deferred[IO, Either[Throwable, TokenDetails]]]] =
Ref.unsafe[IO, Option[Deferred[IO, Either[Throwable, TokenDetails]]]](None)
Nonemeans no fetch is currently in progress.Some(deferred)means a fetch is in progress, anddeferredwill be completed with its result (success or failure) once it finishes.
When the cache is found to be empty, the client calls singleFlightFetch:
def fetchGenerateAndCacheToken: IO[TokenDetails] = ??? //Get token from secrets manager and Preservica
def singleFlightFetch: IO[TokenDetails] =
Deferred[IO, Either[Throwable, TokenDetails]].flatMap { newDeferred =>
tokenRefreshInFlight.modify {
// A fetch is already in progress: discard our own `newDeferred` and wait for the in-flight one instead.
case existing@Some(inFlight) => (existing, inFlight.get.rethrow)
// No fetch is in progress: register ourselves as the fetcher, using our own `newDeferred`.
case None =>
val fetch = fetchGenerateAndCacheToken.attempt
.flatTap(newDeferred.complete) // wake up any other fibers waiting on `newDeferred`
.guarantee(tokenRefreshInFlight.set(None)) // allow a future expiry to trigger a new fetch
.onCancel(newDeferred.complete(Left(PreservicaClientException("Token refresh was cancelled"))).void)
.rethrow
(Some(newDeferred), fetch)
}.flatten // run the atomic Ref update, then run whichever action (wait or fetch) was selected
}
- Every caller speculatively creates its own
Deferredup front (newDeferred), since a new one can’t be allocated inside the atomicRef.modifyblock below. tokenRefreshInFlight.modifyatomically inspects and updates the guard in one step, which is what prevents two callers from both seeing “no fetch in progress” at the same time:- If a fetch is already in progress (
Some(inFlight)), the guard is left unchanged and this caller is simply giveninFlight.get.rethrow- an action that waits for the other fiber’sDeferredto complete, then returns its result (or raises its error). Its ownnewDeferredis discarded, unused. - If no fetch is in progress (
None), this caller “wins”: the guard becomesSome(newDeferred), and it is given thefetchaction to run - which performs the real fetch, completesnewDeferredwith the outcome so any waiting callers are woken up, then clears the guard back toNoneso a future expiry can trigger a new fetch.
- If a fetch is already in progress (
This guarantees that no matter how many callers are waiting on an expired/missing token, only one call is ever made to Secrets Manager and to the Preservica login endpoint.
Token invalidation
If any API request comes back Unauthorized or Forbidden, this is treated as a sign that the cached token is no longer valid (for example, if it has been revoked server-side). The client responds by clearing the entire token cache before retrying, so that the next request goes through the fetch process above and obtains a fresh token, rather than repeatedly retrying with a token that will never succeed.
Configuration
The following parameters, all available when creating any client, affect authentication:
| Name | Description |
|---|---|
| secretName | The name of the Secrets Manager secret containing the Preservica userName, password and apiUrl |
| duration | The time-to-live for the cached token. Defaults to 15 minutes |
| ssmEndpointUri | The endpoint used to call Secrets Manager. Useful for tests or private VPC endpoints |
| potentialProxyUrl | An optional proxy used for both the login call and subsequent API calls |
| retryCount | The number of retries used both when fetching a token and when calling the Preservica API |