feat(auth): send from: on connect and reuse its challenge#410
Conversation
|
I can see what the objective is and what you did clearly works but I wonder if there is a simpler way. Namely leaving the executor as is. But just adding a default onReady consumer when an atsign is specified (and no onReady consumer has been set). This default onReady consumer would send the from command and store the challenge. The changes to the CRAM and PKAM commands would then check for a stored challenge in their implementation. We would need to decide where the appropriate place to cache the challenge is, I don't think it's the command executor. One option would be AtKeys as an instance is already provided to the auth command implementations. The latter also has the advantage of simplifying and homogenizing the existing command method signatures. This context instance would "live in" AtClientImpl. The place to wire this default in is AtCommandExecutors.java#createOnReady() This already adds a default onReady if no onReady is provided to the builder. At the moment that is a pkamAuthenticator if atSign and keys are set or "do nothing" (talking to root server). I think we just need a middle case which is atSign is set but keys are not set in which case, it should create this new "send from" onReady. Happy to discuss further / help if you want |
|
Thanks Fred I will noodle on that today |
|
@akafredperry i made an attempt along the lines you suggested here and in chat ... ptal |
There was a problem hiding this comment.
Since we are using Lombok, I would recommend for this class too (as will remove boiler plate). Something like this...
@Value
public class AtCommandExecutorContext {
AtSign atSign;
AtKeys keys;
Map<String, Object> config;
@Getter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
@ToString.Exclude
AtomicReference<String> challenge = new AtomicReference<>();
public void setChallenge(String challenge)...
public String consumeChallenge()...
public void clearChallenge()...
}|
This change still "lives on" the command executor rather than within the AtClientImpl as I was suggesting. The intention was that the AtCommandExecutor(s) are solely transport specific implementations that are capable of sending commands but with no protocol knowledge. The protocol for initial connection is currently the onReady impl. What we currently do is almost compliant with the requirement (I think), just need to cover the case where AtCommandExecutorBuilder (in AtCommandExecutors.java) has atSign field set but not the AtKeys field. Having said that we need to ensure that everything is using AtCommandExecutorBuilder. |
|
OK .... maybe I should scrap this branch and start again |
The executor now issues from:@atsign as its first command once the connection is ready, so the connection's atSign is established up front and the server's challenge is retained. CRAM/PKAM authentication reuses that challenge instead of sending a second from:, saving a round trip. The challenge is single-use — getFromChallenge() consumes it atomically, so a second authentication on the same connection (onboarding does CRAM then PKAM) gets null and falls back to issuing its own from:. It is also cleared on disconnect, since a from: challenge is only valid for the server session that issued it. Adds NettyAtCommandExecutorTest coverage for the challenge lifecycle: retained and consumed once, absent with no atSign, cleared on disconnect, and refreshed on reconnect.
…s connect paths Reworks the earlier attempts per @akafredperry's review: the command executor stays transport-only (no protocol knowledge, no challenge cache), so NettyAtCommandExecutor and the AtCommandExecutor interface revert to trunk. The "send from: first, remember the challenge" behaviour lives in the onReady layer instead. Every connection that has an atSign now issues from:@atsign as its first command (via an onReady consumer wired by AtCommandExecutors.createOnReady), so proxies / gateways can route it; authentication then reuses that challenge rather than sending a second from:. Connections with no atSign (the atDirectory / root lookup) send no from:. AtCommandExecutorContext bundles the connection identity (atSign, keys, config) and the single-use challenge. By default the builder owns it and closes it over the onReady consumers; a caller may also supply one via context() so an imperative flow that authenticates on the connection can reach the same challenge. CLI consistency (Activate): - onboard threads the connection context into EnrollCommands.onboard, whose CRAM reuses the from: challenge issued on connect (before the intervening scan); PKAM then issues its own from: (challenge is single-use, and it authenticates with the freshly-enrolled keys) -> 2 from: total, matching trunk. - complete uses a connection with no onReady from:, so its own retried PKAM is from-first -> 1 from: per attempt. Reusing a connect-time challenge would be stale by the time the deliberately-delayed, retried PKAM runs. - AbstractCli now builds every connection through AtCommandExecutorBuilder. Cross-tier safety properties: the from: challenge is single-use (consumeChallenge() clears it) and per-connection; on reconnect the onReady re-runs and refreshes it, so a reused challenge always belongs to the currently-live connection. onboard's CRAM reuse assumes the atServer keeps the challenge valid across the intervening unauthenticated scan -- to be confirmed by the functional / e2e suites (mocks cannot exercise the onReady from:).
The AtCommandExecutorContext class doc linked to getAtSign()/getKeys()/ getConfig(), which Lombok's @value generates at compile time. There is no delombok step feeding the javadoc tool, so it cannot resolve them and the `mvn install` javadoc:jar goal failed with "reference not found" (caught by CI on JDK 25; local `mvn test` does not run javadoc). Replaced the links with plain {@code} references.
…der input, clarify factory names Post-review cleanup of the from:-first / challenge-reuse change (no wire behaviour change; 503 unit tests + javadoc green): - EnrollCommands.onboard takes its identity solely from the AtCommandExecutorContext (atSign AND keys) rather than a separate AtKeys param — the context is the single source of identity for onboarding. - Removed the AtCommandExecutorBuilder.context() input. It was redundant with .atSign() at its only call site (the from: challenge is shared caller-side: sendFrom and the imperative CRAM close over the same context object, not the builder) and silently overrode .atSign()/.keys()/.config(). The builder once again always owns its own context. - Collapsed CRAM to a single context-based authenticateWithCram(executor, context, secret); dropped the identity-explicit overload that no longer had a production caller. - Renamed the imperative-auth connection factories onto the from:-issuance axis: createConnectionSendingFrom(context) (issues from: on connect for the onboarding flow to reuse) and createConnectionDeferringFrom(...) (no from: on connect, for complete's retried, self-issued PKAM). - Documented the from:-first behaviour on the AtCommandExecutors builder doc.
|
@akafredperry Take 3 ... I think it's now in line with your intent |
…round of changes. (Was handled directly within the AtCommandExecutor when that was a thing)
|
Thanks for the reviews Fred, great to get expert feedback and guidance |
- What I did
For a new connection, the first command sent should be
from:- this is so that the client can communicate successfully via proxies / gateways etc.- How I did it
A new
AtCommandExecutorContextbundles atSign/keys/config plus the single-use challenge (getAndSet(null)). The builder owns it and closes it over the onReady consumers; the AtCommandExecutor never sees it.CLI imperative flows (onboard / complete)
onboard: issuesfrom:on connect, then the scan sanity check; then CRAM which reuses the challenge and doesn't issuefrom, then PKAM issuesfrombecause there is no saved challenge (it's been used).complete: authenticates in a pending-retry loop, so it issues its ownfrom:per attempt — reusing a connect-time challenge could in theory go stale before the delayed PKAM. 1from:.createConnectionSendingFromandcreateConnectionDeferringFrom.Tests
AuthenticationCommandsTest—sendFromissuesfrom:and retains thechallenge; PKAM reuses it (one
from:on the wire) and falls back to its ownfrom:when none is retained; the challenge is consumed at most once.EnrollCommandsTest— onboard's CRAM reuses the connection'sfrom:challenge(one
from:, not two).(no behaviour change). The pre-existing functional packs which drive real onboarding against
real atServer prove it all hangs together.
- How to verify it
Tests pass