Skip to content

Accept a bearer token for azureblob - #1212

Open
Benmartin92 wants to merge 1 commit into
gaul:masterfrom
Benmartin92:azure-token-credential
Open

Benmartin92 wants to merge 1 commit into
gaul:masterfrom
Benmartin92:azure-token-credential

Conversation

@Benmartin92

@Benmartin92 Benmartin92 commented Oct 30, 2026 •

Copy link
Copy Markdown

Problem

Before 4.0.0, S3Proxy reached Microsoft Entra through jclouds. By default jclouds.azureblob.auth=azureAd took a client id and secret in jclouds.identity and jclouds.credential and fetched and renewed tokens itself. Overriding jclouds.oauth.credential-type to the bearer-token type let the application supply the access token itself instead. Applications that get their tokens from a sidecar or a token broker relied on that override.

The Azure SDK store that replaced jclouds in 4.0.0 accepts only a shared access signature, an account key, or DefaultAzureCredential. The first setup maps onto DefaultAzureCredential with AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET, which renews tokens as jclouds did. The second has no equivalent: a token the application already holds is neither a signature nor a key, and DefaultAzureCredential looks only at the environment, the workload identity token file, managed identity and the developer tools. Those applications cannot upgrade past 3.x.

Change

s3proxy.azureblob.auth names how the store authenticates: sas, key, default, or the new bearer, which says that jclouds.session-token (or the supplier's session token) is a Microsoft Entra access token rather than a shared access signature. Left unset, the store infers sas, key or default from the parts of the credential that are present, exactly as today, so no existing configuration changes. Set, the mode is checked against the credential at construction instead of falling through to another mode on an empty value.

  • Under bearer the store wraps the supplier in a TokenCredential the same way GCloudBlobStore wraps it for a Google OAuth token: nothing in a Credentials says how long the token lasts, so each is handed over already lapsed and the supplier is asked again for every request. The endpoint must be https, since azure-core refuses to send a bearer token over http.
  • The mode lives in an Auth enum field, and both the constructor and signedSourceUrl branch on it explicitly.
  • Server-side copies sign their source URL with a user delegation key, as under DefaultAzureCredential. The javadoc and README say that the token's principal needs a role that includes generateUserDelegationKey, such as Storage Blob Delegator, or copies fail while reads and writes succeed.
  • A copy under sas now signs its source with the signature the client holds, which SasRefreshPolicy keeps at the last non-empty value the supplier returned, instead of re-reading the supplier and failing if it is briefly empty.
  • A package-private constructor takes an Azure HttpClient, for tests.
  • The README and javadoc state the migration from 3.x: jclouds.azureblob.auth is not read any more; the client-id-and-secret setup moves to default with the AZURE_* variables, and the raw-token setup to jclouds.session-token plus s3proxy.azureblob.auth=bearer.

Tests

AzureBearerTokenTest hands the store an HttpClient that records each request and answers 403, so there is no socket, no TLS and no global state. It checks that:

  • the token arrives as Bearer <token>;
  • a token the supplier replaces gives way to its successor on the next request;
  • a copy makes exactly one request, to comp=userdelegationkey, carrying the bearer token;
  • an unknown auth value is refused;
  • a named mode must find its credential: bearer without a session token and key without an account name and key are refused.

mvn verify and the full mvn test suite pass.

🤖 Generated with Claude Code

@gaul gaul left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. The production change is small and leaves the SAS and account-key paths alone, and copies correctly fall through to a user delegation key in signedSourceUrl. A few things before merging, in rough priority:

  1. The new test uses 2-space indentation, but checkstyle's Indentation module runs on test sources with failOnViolation=true. I expect this fails mvn verify. Please reformat to the 4-space style of the sibling AzureSasCredentialTest.
  2. The test swaps the JVM-wide SSLContext default for a trust-all one (inline comment below).
  3. The placeholder Credentials steers both credential if chains implicitly (inline comment below).

A design question too. Since #1161, expiring backend credentials flow through Supplier<Credentials> and Credentials.sessionToken(), re-read on each request. GCloudBlobStore already carries a Google OAuth bearer token that way. A separate TokenCredential constructor serves only applications that embed S3Proxy. Someone configuring through a properties file, who could set jclouds.azureblob.auth=azureAd in 3.x, still has no path. On the other hand, TokenCredential is the idiomatic Azure type and carries an expiry that Credentials cannot. I'd like to weigh this before merging: is there a reason not to select bearer-token mode for the supplier's token with a property?

Comment thread README.md Resolved
@Benmartin92
Benmartin92 force-pushed the azure-token-credential branch from 88b309f to d0e04c1 Compare November 2, 2026 12:27
@Benmartin92

Copy link
Copy Markdown
Author

Hey @gaul, thanks for reviewing!

  1. Yeah, I run it before I touched it with my editor that saved with different spacing, fixed it.
  2. I addressed that by adding the constructor that can take the HTTP client.
  3. Introduced enums, so the branching is now clear.

Regarding the design: fair point, I had only my own embedding use case in mind. I changed it to go through the supplier like GCP does and to work from a properties file.

@Benmartin92 Benmartin92 changed the title Accept a token credential for azureblob Accept a bearer token for azureblob Nov 2, 2026
@gaul

gaul commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thanks, this addresses everything: the enum makes both branches explicit, the injected HttpClient removes the global TLS state, and the property-based design works from a properties file. I checked SuppliedTokenCredential against azure-core's AccessTokenCache: a token that expires at now takes the refresh-immediately path on every request, so the supplier is re-read as intended.

One correction, and it's partly my fault for the framing in my first review. In jclouds 2.7.0, jclouds.azureblob.auth=azureAd defaulted jclouds.oauth.credential-type to clientCredentialsSecret. jclouds.identity was a client id, jclouds.credential a client secret, and jclouds fetched and renewed tokens from login.microsoft.com/${jclouds.azureblob.tenantId}. A raw token in jclouds.credential only worked if you also overrode the credential type to the bearer-token type. So most 3.x azureAd users should move to DefaultAzureCredential via AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET, which renews tokens, and bearer replaces only the raw-token setup. Could you update the README, the javadoc and the commit message to say that?

Smaller things:

  • Please note in the README and javadoc that bearer needs an https endpoint, since azure-core refuses to send a token over http.
  • In signedSourceUrl, the SAS case's requireNonNull turns a briefly empty supplier into a 500, while SasRefreshPolicy keeps the last signature. Reading the held AzureSasCredential's getSignature() would match.
  • One README line runs past 80 columns.
  • Optional: a test that a copy under bearer sends the user-delegation-key request with the bearer token.

Before 4.0.0, S3Proxy reached Microsoft Entra through jclouds.  Its
azureblob provider's jclouds.azureblob.auth=azureAd took a client id
and secret by default and fetched and renewed tokens itself, which
DefaultAzureCredential now does from AZURE_TENANT_ID, AZURE_CLIENT_ID
and AZURE_CLIENT_SECRET.  Overriding jclouds.oauth.credential-type to
the bearer-token type let the application supply the access token
itself instead.  Applications that obtain their tokens from a sidecar
or a token broker relied on that override.

The Azure SDK store that replaced jclouds accepts only a shared access
signature, an account key, or DefaultAzureCredential.  None of those
fits a token the application already holds: it is neither a signature
nor a key, and DefaultAzureCredential looks only at the environment,
the workload identity token file, managed identity and the developer
tools.  Those applications cannot upgrade past 3.x.

Let s3proxy.azureblob.auth name how the store authenticates: sas, key,
default, or the new bearer, which says that the session token is such
a token.  Left unset, the store infers sas, key or default from the
parts of the credential that are present, as it did before; set, the
mode is checked against the credential at construction rather than
falling through to another on an empty value.  Under bearer the store
hands the token to the client as a bearer token, read from the
credential supplier for every request as a signature already is, and
as GCloudBlobStore does with a Google OAuth token: nothing in a
Credentials says how long the token lasts, so each is handed over
already lapsed rather than kept past its withdrawal.  The endpoint
must be https, since the client refuses to send a bearer token over
http.  Server-side copies sign their source URL with a user delegation
key, as under DefaultAzureCredential, so the token's principal needs a
role that includes generateUserDelegationKey.  A copy under sas now
signs its source with the signature the client holds, the last one the
supplier named, rather than failing on a supplier that is briefly
empty, which SasRefreshPolicy already tolerates.

The test hands the store an HttpClient that records each request and
refuses it, since the client sends bearer tokens only to an https
endpoint and only what was presented is in question.  It checks that
the token arrives as a bearer token, that one the supplier replaces
gives way to its successor, that a copy asks for a user delegation
key under the same token, that an unknown mode is refused, and that a
named mode must find its credential.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Benmartin92
Benmartin92 force-pushed the azure-token-credential branch from d0e04c1 to c416f56 Compare November 3, 2026 09:51
@Benmartin92

Copy link
Copy Markdown
Author

Thanks for the review, I added required changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants