Skip to content

feat(auth): send from: on connect and reuse its challenge#410

Merged
gkc merged 8 commits into
trunkfrom
gkc-from-first
Jul 22, 2026
Merged

feat(auth): send from: on connect and reuse its challenge#410
gkc merged 8 commits into
trunkfrom
gkc-from-first

Conversation

@gkc

@gkc gkc commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

- 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 AtCommandExecutorContext bundles 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: issues from: on connect, then the scan sanity check; then CRAM which reuses the challenge and doesn't issue from, then PKAM issues from because there is no saved challenge (it's been used).
    • complete: authenticates in a pending-retry loop, so it issues its own from: per attempt — reusing a connect-time challenge could in theory go stale before the delayed PKAM. 1 from:.
    • Two factories for clarrity: createConnectionSendingFrom andcreateConnectionDeferringFrom.
  • Tests

    • Added unit coverage for the challenge lifecycle:
      • AuthenticationCommandsTestsendFrom issues from: and retains the
        challenge; PKAM reuses it (one from: on the wire) and falls back to its own
        from: when none is retained; the challenge is consumed at most once.
      • EnrollCommandsTest — onboard's CRAM reuses the connection's from: challenge
        (one from:, not two).
    • Adapted the existing CRAM and onboard unit tests to the context-based auth API
      (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

@gkc
gkc requested review from akafredperry and cconstab July 11, 2026 13:13
@akafredperry

akafredperry commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

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.
AtKeys already supports a transient map for caching keys, using that would be the least amount of changes.
BUT
If we feel that is an abuse of the design we could add an explicit authChallenge field to AtKeys (perhaps still yuck).
OR
Create a new context class that "gathers together" all the current (and future) contexty stuff (e.g. AtSign, AtKeys, connection config and now authChallenge).

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

@gkc

gkc commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks Fred I will noodle on that today

@gkc

gkc commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@akafredperry i made an attempt along the lines you suggested here and in chat ... ptal

@gkc
gkc removed the request for review from cconstab July 12, 2026 18:04

@akafredperry akafredperry Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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()...
}

@akafredperry

Copy link
Copy Markdown
Collaborator

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.

@gkc

gkc commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

OK .... maybe I should scrap this branch and start again

gkc added 3 commits July 21, 2026 12:03
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:).
@gkc
gkc force-pushed the gkc-from-first branch from 9755dfa to 6c37158 Compare July 21, 2026 13:45
gkc added 2 commits July 21, 2026 14:57
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.
@gkc

gkc commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@akafredperry Take 3 ... I think it's now in line with your intent

@gkc
gkc requested a review from akafredperry July 22, 2026 08:47

@akafredperry akafredperry left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

perfect (imho)

@gkc
gkc merged commit 91dc955 into trunk Jul 22, 2026
3 checks passed
@gkc

gkc commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews Fred, great to get expert feedback and guidance

@gkc
gkc deleted the gkc-from-first branch July 22, 2026 23:28
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