Skip to content

Conversation

@arturobernalg
Copy link
Member

Implement SCRAM-SHA-256 auth (RFC 7804/5802/7677) for HttpClient. Full round-trip with constant-time server signature verification from Authentication-Info, SASLprep and zeroized secrets, correct header quoting, optional preemptive client-first, and iteration policy warnings.

@arturobernalg arturobernalg requested a review from ok2c September 5, 2025 15:54
@michael-o
Copy link
Member

As far as I understood the SASL SCRAM mech it was always connection-bound which always contracted the multistream nature of h2. How does this reconcile? E.g., PHA or NTLM on h2 are completely not working.

@michael-o
Copy link
Member

I hope this can finally kill Digest scheme.

@arturobernalg
Copy link
Member Author

As far as I understood the SASL SCRAM mech it was always connection-bound which always contracted the multistream nature of h2. How does this reconcile? E.g., PHA or NTLM on h2 are completely not working.

IMO we’re fine on h2 because this is HTTP SCRAM (RFC 7804) which is per-request—no channel binding (GS2 “n,,” / c=biws)—so each stream carries its own exchange.
The connection-bound pain is NTLM/Negotiate/PHA; if we ever add SCRAM-PLUS, it can bind via the TLS exporter shared by h2.

@michael-o
Copy link
Member

As far as I understood the SASL SCRAM mech it was always connection-bound which always contracted the multistream nature of h2. How does this reconcile? E.g., PHA or NTLM on h2 are completely not working.

IMO we’re fine on h2 because this is HTTP SCRAM (RFC 7804) which is per-request—no channel binding (GS2 “n,,” / c=biws)—so each stream carries its own exchange. The connection-bound pain is NTLM/Negotiate/PHA; if we ever add SCRAM-PLUS, it can bind via the TLS exporter shared by h2.

So one round is enough to complete auth?

@arturobernalg
Copy link
Member Author

As far as I understood the SASL SCRAM mech it was always connection-bound which always contracted the multistream nature of h2. How does this reconcile? E.g., PHA or NTLM on h2 are completely not working.

IMO we’re fine on h2 because this is HTTP SCRAM (RFC 7804) which is per-request—no channel binding (GS2 “n,,” / c=biws)—so each stream carries its own exchange. The connection-bound pain is NTLM/Negotiate/PHA; if we ever add SCRAM-PLUS, it can bind via the TLS exporter shared by h2.

So one round is enough to complete auth?

SCRAM needs two exchanges: client-first → 401 (server-first), then client-final → 200 with Authentication-Info (v=).
For what I understood, if the server sends an empty announce first, it’s one extra 401 (so 3 total); with preemptive client-first it’s 2.

@michael-o
Copy link
Member

michael-o commented Sep 7, 2025

As far as I understood the SASL SCRAM mech it was always connection-bound which always contracted the multistream nature of h2. How does this reconcile? E.g., PHA or NTLM on h2 are completely not working.

IMO we’re fine on h2 because this is HTTP SCRAM (RFC 7804) which is per-request—no channel binding (GS2 “n,,” / c=biws)—so each stream carries its own exchange. The connection-bound pain is NTLM/Negotiate/PHA; if we ever add SCRAM-PLUS, it can bind via the TLS exporter shared by h2.

So one round is enough to complete auth?

SCRAM needs two exchanges: client-first → 401 (server-first), then client-final → 200 with Authentication-Info (v=). For what I understood, if the server sends an empty announce first, it’s one extra 401 (so 3 total); with preemptive client-first it’s 2.

So from a client's perspective, it is always stateful, right? From the server's perspective, it can be stateful. How can this be bound to an h2 stream, if h2 is used?

For instance, SPNEGO/Kerberos only works reliably via h2 IF there is a single roundtrip only, everything else is undefined per sé.

@arturobernalg
Copy link
Member Author

As far as I understood the SASL SCRAM mech it was always connection-bound which always contracted the multistream nature of h2. How does this reconcile? E.g., PHA or NTLM on h2 are completely not working.

IMO we’re fine on h2 because this is HTTP SCRAM (RFC 7804) which is per-request—no channel binding (GS2 “n,,” / c=biws)—so each stream carries its own exchange. The connection-bound pain is NTLM/Negotiate/PHA; if we ever add SCRAM-PLUS, it can bind via the TLS exporter shared by h2.

So one round is enough to complete auth?

SCRAM needs two exchanges: client-first → 401 (server-first), then client-final → 200 with Authentication-Info (v=). For what I understood, if the server sends an empty announce first, it’s one extra 401 (so 3 total); with preemptive client-first it’s 2.

So from a client's perspective, it is always stateful, right? From the server's perspective, it can be stateful. How can this be bound to an h2 stream, if h2 is used?

Client keeps only transient handshake state (nonce/authMessage/expected-v) per request/stream, not per connection.
With h2, all SCRAM messages are headers, so each stream’s Authorization ↔ WWW-Authenticate ↔ Authentication-Info cycle is self-contained; the server can use an opaque sid to correlate, but no connection binding is required.

@michael-o
Copy link
Member

As far as I understood the SASL SCRAM mech it was always connection-bound which always contracted the multistream nature of h2. How does this reconcile? E.g., PHA or NTLM on h2 are completely not working.

IMO we’re fine on h2 because this is HTTP SCRAM (RFC 7804) which is per-request—no channel binding (GS2 “n,,” / c=biws)—so each stream carries its own exchange. The connection-bound pain is NTLM/Negotiate/PHA; if we ever add SCRAM-PLUS, it can bind via the TLS exporter shared by h2.

So one round is enough to complete auth?

SCRAM needs two exchanges: client-first → 401 (server-first), then client-final → 200 with Authentication-Info (v=). For what I understood, if the server sends an empty announce first, it’s one extra 401 (so 3 total); with preemptive client-first it’s 2.

So from a client's perspective, it is always stateful, right? From the server's perspective, it can be stateful. How can this be bound to an h2 stream, if h2 is used?

Client keeps only transient handshake state (nonce/authMessage/expected-v) per request/stream, not per connection. With h2, all SCRAM messages are headers, so each stream’s Authorization ↔ WWW-Authenticate ↔ Authentication-Info cycle is self-contained; the server can use an opaque sid to correlate, but no connection binding is required.

Ah, perfect. This is what I wanted to hear. As long it is associated with the stream only and not the connection I am fine with that in general.

StandardAuthScheme.BASIC));
Collections.unmodifiableList(Arrays.asList(
StandardAuthScheme.BEARER,
StandardAuthScheme.SCRAM_SHA_256,
Copy link
Member

Choose a reason for hiding this comment

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

This order is of course questionable because if Bearer is peformed via client_credentials first it isn't better than SCRAM, from my PoV

@arturobernalg
Copy link
Member Author

Please @michael-o do another pass

Copy link
Member

@michael-o michael-o left a comment

Choose a reason for hiding this comment

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

I don't have any objections, but at least some other committer should look over!

@arturobernalg
Copy link
Member Author

I don't have any objections, but at least some other committer should look over!

Agree. thank you @michael-o

Copy link
Member

@ok2c ok2c left a comment

Choose a reason for hiding this comment

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

@arturobernalg There is nothing I can say. If it is good enough for @michael-o it must be good enough for me.

How confident are you however about the implementation? Should not we make it experimental for a release cycle?

@michael-o
Copy link
Member

How confident are you however about the implementation? Should not we make it experimental for a release cycle?

Marking as experimental sounds like a good idea.

Implements HTTP SCRAM with SCRAM-SHA-256 per RFC 7804 and SCRAM mechanics per RFC 5802/7677.
@arturobernalg
Copy link
Member Author

@arturobernalg There is nothing I can say. If it is good enough for @michael-o it must be good enough for me.

How confident are you however about the implementation? Should not we make it experimental for a release cycle?

@ok2c @michael-o agree to make it @experimental for a release cycle

@arturobernalg arturobernalg merged commit 5e73e17 into apache:master Oct 5, 2025
10 checks passed
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.

4 participants