Class IssuerHttpClient

java.lang.Object
co.ankatech.ankasecure.sdk.auth.IssuerHttpClient
All Implemented Interfaces:
AutoCloseable

public final class IssuerHttpClient extends Object implements AutoCloseable
The SECOND HTTP client: the one that talks to the external authorization server, and to nothing else.

Why it is built from scratch rather than derived from the ANKASecure client

Deriving it with newBuilder() would be one line and would inherit everything the other client carries, which is exactly the problem. That client honours openapi.insecureSkipTlsVerify, a property this SDK's own cli.properties ships as true for the demo playground: when it is set, the ANKASecure client installs a trust-all X509TrustManager AND a hostname verifier that returns true for everything. It also carries a headers logger when debugging is on, and an origin pin aimed at the ANKASecure deployment. Inheriting that set would mean a consumer's credential is posted to a host whose certificate was never checked, because of a property about a different server.

So this client shares nothing with it: a fresh builder, no TLS relaxation of any kind, no logger on the fetch path, and its own origin pin.

What it enforces

  • The pinned addresses. Its Dns answers with exactly the addresses admission resolved and screened, and throws for any other host. There is no second lookup, so there is no window between the check and the use.
  • No proxy, unless the operator asked for one in the process environment. A client that goes through a proxy never consults its own Dns: it hands the issuer NAME to the proxy, which resolves it, so every address admission screened is bypassed and the whole egress screening is void. That is a JVM-wide setting - one https.proxyHost system property, or a ProxySelector someone installed - so the control would otherwise be switched off from outside this SDK with no signal at all. This client therefore pins Proxy.NO_PROXY and REFUSES, before any socket exists, when a proxy is configured for the issuer without "ANKASECURE_ALLOW_PROXIED_EXTERNAL_ISSUER" being set. See proxiedIssuerAllowed().
  • The origin pin, twice. ConfiguredOriginInterceptor is registered as an APPLICATION interceptor and again as a NETWORK interceptor. The network registration alone refuses only after ConnectInterceptor has run - DNS, TCP and a full TLS handshake with the foreign host have already happened by then, which is a blind connect primitive against any host a response can name. The application registration refuses before the connection exists; the network one still guards anything that rewrites a URL later.
  • No redirects, in either form. A redirect is the responding server choosing where the next request goes.
  • A total deadline. Connect and read timeouts do not bound a TLS handshake that never finishes; CALL_TIMEOUT does.

It is owned, and therefore closed

One of these is built per resolved credential, not one per process, and it holds a connection pool of its own. That pool is what makes abandoning one costly: it keeps a live keep-alive TLS connection to a THIRD-PARTY authorization server, and a long-running application that opened a session per unit of work would accumulate one per session. So this type is AutoCloseable and the provider that owns it closes it.

close() releases the dispatcher's executor as well, and the reason for that half is narrower and worth stating exactly, because the obvious reason is false here. Every call this SDK makes on this client is SYNCHRONOUS - newCall(...).execute(), with no enqueue anywhere on the path - and OkHttp creates the dispatcher's executor lazily, so on the SDK's own path there are no dispatcher threads in existence to leak. okHttpClient() is public, however, and a caller that enqueues on it would create exactly the non-daemon threads that keep a JVM alive after the work is done. The pool is why this type must be closed; the dispatcher is closed because the client it belongs to is reachable.

  • Field Details

    • CONNECT_TIMEOUT

      public static final Duration CONNECT_TIMEOUT
      TCP connect budget.
    • READ_TIMEOUT

      public static final Duration READ_TIMEOUT
      Per-read budget once a connection exists.
    • CALL_TIMEOUT

      public static final Duration CALL_TIMEOUT
      The whole-call budget, which is the one that matters here: it covers name resolution, the TLS handshake and every read, so a server that accepts a connection and then stalls the handshake cannot hold this thread indefinitely.
    • ALLOW_PROXY_ENV_VARIABLE

      public static final String ALLOW_PROXY_ENV_VARIABLE
      The one variable that admits an issuer reached through a proxy, and with it the loss of the address screening. Deliberately absent from AuthSetting, exactly like ExternalIssuerAdmission.ALLOW_PRIVATE_ENV_VARIABLE: a properties file must never be able to widen where this process posts a credential, nor to decide that the screening which chose the destination may be skipped.
      See Also:
    • ALLOW_PROXY_ENV_VALUE

      public static final String ALLOW_PROXY_ENV_VALUE
      The one value that enables it. Anything else - including true - does not.
      See Also:
  • Method Details

    • forIssuer

      public static IssuerHttpClient forIssuer(AdmittedIssuer issuer)
      Builds the issuer client for an admitted issuer, trusting the platform's certificate authorities and nothing else.
      Parameters:
      issuer - the admitted issuer; must not be null
      Returns:
      the client
      Throws:
      ExternalTokenException - if a proxy is configured for this issuer and "ANKASECURE_ALLOW_PROXIED_EXTERNAL_ISSUER" is not set
    • proxiedIssuerAllowed

      public static boolean proxiedIssuerAllowed()
      Whether this process environment permits reaching an external issuer through a proxy.

      Reads System.getenv(String) directly, at the point of use, and takes no argument - the same shape, and for the same reason, as ExternalIssuerAdmission.privateIssuerAllowed(). Nothing a caller passes in and nothing a properties file contains can change the answer.

      What setting it gives up. ExternalIssuerAdmission resolves the issuer name and refuses every address in the egress reject set - loopback, RFC 1918, link-local and the cloud metadata address among them - and this client then dials exactly those addresses. A proxy makes all of that inert: the name, not an address, is what reaches the proxy, and where the proxy sends the request is the proxy's decision. TLS still binds the hostname, so the credential is not exposed to the proxy; what is given up is the SSRF and egress screening.

      Returns:
      true only when the variable is present with exactly the enabling value
    • okHttpClient

      public okhttp3.OkHttpClient okHttpClient()
      The underlying client.
      Returns:
      the client; never null, and never the ANKASecure client's
    • issuer

      public AdmittedIssuer issuer()
      Returns:
      the issuer this client is pinned to; never null
    • origin

      public String origin()
      Returns:
      the origin this client is pinned to, as scheme://host:port
    • close

      public void close()
      Releases this client's connection pool and dispatcher.

      Idempotent, and it does not interrupt a call in flight - it evicts idle connections and lets the dispatcher's executor finish what it has. Calls already dispatched still complete; what ends is the pool holding a connection to somebody else's authorization server open after the session that needed it is over.

      The pool is the half that always has something to release. The dispatcher's executor is created lazily by OkHttp and every call this SDK makes on this client is synchronous, so on that path the shutdown is over an executor with no thread in it; it matters for a caller that enqueued on the okHttpClient() this class hands out.

      Specified by:
      close in interface AutoCloseable
    • toString

      public String toString()
      Overrides:
      toString in class Object