feat!: add dynamic authorization support#194
feat!: add dynamic authorization support#194EddieHouston wants to merge 2 commits intobitcoindevkit:masterfrom
Conversation
7e9324b to
db94491
Compare
|
concept ACK Thanks for this, adding an "authorization" header is the most common way to authenticate to a http endpoints so this should be generally useful. Appreciate the comprehensive examples and unit tests. The team is spread a bit thin so it may take some time to get it reviewed and tested. |
oleonardolima
left a comment
There was a problem hiding this comment.
cACK
I did an initial round of review. Overall, it looks good! I'll give it a try testing it.
|
@EddieHouston needs rebase |
@oleonardolima Was this still planned to release before the Electrum protocol v1.6 changes? |
I'm planning to have both under the same release, as the version negotiation is should work fine for new v1.6 and old servers. Appreciate it, if you could do a rebase. |
|
Also, you could use the opportunity to squash into a single commit, as it touches the same files. |
db94491 to
3781f4d
Compare
This comment was marked as outdated.
This comment was marked as outdated.
3781f4d to
ca27c4a
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
ca27c4a to
3590cea
Compare
3e58253 to
020f193
Compare
oleonardolima
left a comment
There was a problem hiding this comment.
tACK 020f193
It's looking good! I still think the API feels a bit odd, and we could just update the existing methods to have a new auth parameter, though it can be addressed in a follow-up (maybe in another release even).
I gave it a shot and tested it with non-authenticated (testnet) servers that support v1.6 by syncing/broadcasting w/ bdk_wallet, it's still working as expected.
I also tested it with authenticated (mainnet) servers, but didn't tested it by syncing/broadcasting w/ bdk_wallet, still need to test it in testnet/signet if possible.
@EddieHouston how hard would it be run this in regtest on CI? Since we'll probably move some stuff around for the next release, would be nice to be able to test it out there and catch any regressions. |
|
@EddieHouston can you also provide complete instructions on testing this with your servers? |
@luisschwab You can probably start from this: oleonardolima@4f6bc16 I think that having it as a full example is better than have it all in just docs, will update to the latest changes and probably push that commit, wdyt @EddieHouston |
|
@oleonardolima go ahead. While you're at it, can you squash the last 3 commits and fix formatting? |
In theory not difficult, just time. You could maybe mock up a test server that takes an API key or something. |
|
I cherry-picked my changes for the examples and squashed everything into a single commit. I changed the Adding a regtest auth infra for testing in CI can be done in a follow-up. |
oleonardolima
left a comment
There was a problem hiding this comment.
tACK 1c7ad73
I've tested using this version of the electrum-client for FullScan/Sync and broadcasting with a bdk_wallet on Testnet3. I did it for both unauthenticated and authenticated servers, and it's working as expected.
evanlinjin
left a comment
There was a problem hiding this comment.
Thanks for this work. This is only a partial review.
src/raw_client.rs
Outdated
| params.to_vec(), | ||
| ); | ||
|
|
||
| // Servers expect auth token only in the first request in batched requests |
There was a problem hiding this comment.
Unfortunately this library doesn't actually use JSON-RPC batch arrays - the "batch" here just means "write all in one go".
We need to add auth token to all requests.
There was a problem hiding this comment.
@EddieHouston Are there any issues on the server side if we choose to send the authorization within every request ?
There was a problem hiding this comment.
@EddieHouston Are there any issues on the server side if we choose to send the authorization within every request ?
yeah, it's a huge amount of extra unneeded data over the wire.
There was a problem hiding this comment.
@EddieHouston Are there any issues on the server side if we choose to send the authorization within every request ?
yeah, it's a huge amount of extra unneeded data over the wire.
Oh, got it. However, we should probably follow with above's suggestion to guarantee general compatibility.
There was a problem hiding this comment.
I don't see the problem, the original code adds the auth only on the first request in the batch, even though its using newline-delimited JSON-RPC, they are all flushed to the wire together and it has auth only on the first one as expected.
There was a problem hiding this comment.
I think we can keep as it's for now, if leads to any performance issues we can change it and release in a patch release, as it'd be internal and non-breaking.
src/types.rs
Outdated
| pub params: Vec<Param>, | ||
| /// Optional authorization token (e.g., JWT Bearer token) | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub authorization: Option<String>, |
There was a problem hiding this comment.
Is this intentionally made public? RawClient seems to overwrite this regardless (raw_client.rs:973).
Either make this pub(crate), or change logic to not override a caller-set value (however, does this feature even make sense?).
There was a problem hiding this comment.
If I got it right, RawClient overwrites it because Request is always being created with an authorization as None.
Yes, it probably can be pub(crate) and already assigned when creating a new Request, that would also fix the issue of only adding the authorization to the first one in batch scenarios.
There was a problem hiding this comment.
Actually I changed it to private in the latest changes, there's no need to be either pub or pub(crate), you should set it through the new .with_auth() method.
src/raw_client.rs
Outdated
| Self::new_with_auth(socket_addrs, timeout, None) | ||
| } | ||
|
|
||
| pub(crate) fn new_with_auth<A: ToSocketAddrs>( |
There was a problem hiding this comment.
All of the new .._with_auth methods seem to only populate the auto_provider field. Is this correct?
Instead of having all these duplicate methods - we can use a builder pattern by introducing a single method:
pub(crate) with_auth(mut self, auth_provider: AuthProvider) -> Self {
self.auth_provider = Some(auth_provider);
self
}There was a problem hiding this comment.
I think this goes back to my previous comment.
Yes, we should give it a try to builder pattern, it'll give us a better API.
cc @EddieHouston
There was a problem hiding this comment.
Alright, I did it in the latest commits. It now has a better API and follows the builder pattern.
- add new `AuthProvider` public type to handle an `authorization` token. - add new `Option<AuthProvider>` as `authorization_provider` in `Config`, it holds the dynamic authentication token callback provider. - add new `Option<AuthProvider>` as `auth_provider` in `RawClient`, it holds the dynamic authentication token callback provider. - add new `authorization` private field in `Request`. - add new `pub fn with_auth()` to `RawClient` in order to set the authorization provider into the client, it should be used following a builder patter, the client defaults to no authentication otherwise. - add new `.with_auth()` method to `Request`, it sets the `authorization` token, if any. - update `pub fn negotiate_protocol_version()` in `RawClient` to return the `RawClient`, this way it SHOULD no be used following a builder pattern. - update `.call()` and `.batch_call()` to use the newly added `.with_auth()`, instead of updating request's authorization field directly. BREAKING CHANGE: all changes above are API breaking changes.
- add two new examples: `jwt_auth.rs` and `jwt_dynamic_auth.rs` as references on how to build a client with authentication.
Description
This PR adds support for dynamic authorization in Electrum RPC requests.
This enables authentication with authorization-protected Electrum servers and API gateways.
Use cases:
Notes to the reviewers
The implementation uses a callback pattern rather than a static token to support dynamic scenarios like automatic token refresh. The
AuthProvideris called for each RPC request, allowing the token to be updated without reconnecting the client.Both
ConfigandRawClientstructs needed customDebugimplementations since function pointers don't implementDebug- this is handled by displaying<provider>when an auth provider is present, which prevents leaking sensitive token values in debug output.Thread safety is ensured through
Arcwrapping andSend + Syncbounds, making this safe for concurrent use across the client.Tests added:
14 new tests, all passing
Changelog notice
Added: Dynamic authorization support via
AuthProvidercallback inConfigBuilder. Enables authentication with authorization-protected Electrum servers and automatic token refresh patterns for Bearer tokens, API keys, and other authorization schemes.Checklists
All Submissions:
New Features:
Bugfixes: