Skip to content

NIP-47 client-created secret#1818

Open
rolznz wants to merge 2 commits intonostr-protocol:masterfrom
rolznz:feat/nip47-client-created-secret
Open

NIP-47 client-created secret#1818
rolznz wants to merge 2 commits intonostr-protocol:masterfrom
rolznz:feat/nip47-client-created-secret

Conversation

@rolznz
Copy link
Copy Markdown
Contributor

@rolznz rolznz commented Mar 3, 2025

This PR introduces two "1-click" connection flows for setting up initial NWC connections. Rather than having to copy-paste a connection string, the user is presented with an authorization page which they can approve or decline. The secret is generated locally and never leaves the client.

  1. HTTP flow - for publicly accessible lightning wallets. Implemented in Alby Hub (my.albyhub.com) and CoinOS (coinos.io)
  2. Nostr flow - for mobile-based / self-hosted lightning wallets, very similar to NWA but without a new event type added. Implemented in Alby Go and Alby Hub. Benefits over NWC Deep Links are that it works cross-device, mobile to web, and the client-generated secret never leaves the client.

Both flows are also implemented in Alby JS SDK and Bitcoin Connect.

This can be tested in some apps:

Also implemented by:

  • CoinOS
  • PayWithFlash

Copy link
Copy Markdown

@asoltys asoltys left a comment

Choose a reason for hiding this comment

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

lgtm!

@nostr-wine
Copy link
Copy Markdown
Contributor

nostr-wine commented Mar 5, 2025

Thanks for working on this!

One of the things we liked about NWA was the ability for our service to use one single pubkey (instead of one per user) and instead rely on the secret in the connection string to identify and link a user's wallet. It looks like this proposal would require using a unique pubkey per user as the pubkey has become the only identifier of the new connection. Is that correct?

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Mar 5, 2025

Thanks for working on this!

One of the things we liked about NWA was the ability for our service to use one single pubkey (instead of one per user) and instead rely on the secret in the connection string to identify and link a user's wallet. It looks like this proposal would require using a unique pubkey per user as the pubkey has become the only identifier of the new connection. Is that correct?

No, the wallet service can use a single wallet service key for all its connections if it wants.

The only change here is that the client's secret is created client-side (and the corresponding pubkey is given to the wallet service) rather than the wallet service generating the client's key and requiring the user to copy the secret from the service into the client. In the Nostr flow, the client subscribes to an info event that has a p tag set to the client's pubkey, which allows it to discover the wallet service key (the pubkey of the published info event).

Note: Using a single wallet service key for all connections is simpler but unfortunately it leaks metadata (anyone who knows the wallet service pubkey can filter events by that key to see all activity through that wallet service). For this reason in Alby Hub we now generate unique wallet service keys per connection.

@nostr-wine
Copy link
Copy Markdown
Contributor

nostr-wine commented Mar 5, 2025

No, the wallet service can use a single wallet service key for all its connections if it wants.

Just to be clear - I'm talking about our NWC service (not a "wallet" but a "client" or "service"). How could we identify which wallet is associated with which user if our supplied pubkey isn't unique?

When we wanted to onboard a user with NWA, we would generate a unique secret to include in the URI and associate this secret with the user on our backend. When we received the 33194 event sent from the new wallet pubkey it included the secret so we could identify which of our users created this connection. The wallet service was still creating a unique pubkey per connection but nostr.wine was not.

Note: Using a single wallet service key for all connections is simpler but unfortunately it leaks metadata (anyone who knows the wallet service pubkey can filter events by that key to see all activity through that wallet service).

Not if your service uses a properly authenticated relay with access control. inbox.nostr.wine supports this by default. Events can be written by anyone tagging our NWC service pubkey but only our key can request and read those events. This setup becomes untenable if we need to manage authentications and queries for one pubkey per user.

@nostr-wine
Copy link
Copy Markdown
Contributor

nostr-wine commented Mar 5, 2025

Another subtle but important difference from NWA:

When we (a client or other non-wallet service) pass a relay parameter in the URI, is the wallet service expected to listen on this relay for requests in perpetuity?

With NWA the 33194 event broadcasted to the relay included in the client generated URI included an optional relay field for the wallet service to effectively override which relay would be used to listen for requests. Our service would then connect to the designated wallet relay to verify the presence of a 13194 info event for the corresponding wallet pubkey.

Are your wallet services prepared to connect to any number of 3rd party relays? Do they support NIP-42 AUTH?

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Mar 5, 2025

Just to be clear - I'm talking about our NWC service (not a "wallet" but a "client" or "service"). How could we identify which wallet is associated with which user if our supplied pubkey isn't unique? When we wanted to onboard a user with NWA, we would generate a unique secret to include in the URI and associate this secret with the user on our backend. When we received the 33194 event sent from the new wallet pubkey it included the secret so we could identify which of our users created this connection. The wallet service was still creating a unique pubkey per connection but nostr.wine was not.

Now nostr.wine as the "client" must generate the unique key instead of the wallet service. You just need to associate the key you generate with a particular user. (And only the corresponding public key of this unique key is given to the wallet service. You as the client will keep the secret and use it for NWC communication). Does that make sense?

When we (a client or other non-wallet service) pass a relay parameter in the URI, is the wallet service expected to listen on this relay for requests in perpetuity? With NWA the 33194 event broadcasted to the relay included in the client generated URI included an optional relay field for the wallet service to effectively override which relay would be used to listen for requests. Our service would then connect to the designated wallet relay to verify the presence of a 13194 info event for the corresponding wallet pubkey. Are your wallet services prepared to connect to any number of 3rd party relays? Do they support NIP-42 AUTH?

This is still possible in this proposal. The wallet service can provide its own preferred relay in the p tag (see https://github.com/nostr-protocol/nips/pull/1818/files#diff-d1ed4c38da272aa7f99e29edb6739e8fb209c723585601a84f0fd511d1c679e0R215). It doesn't explicitly say the client MUST use that relay, but I think there are some things to consider if we force the wallet service to obey the client app on which relay(s) to use. I would love feedback / thoughts on this.

Unfortunately from Alby Hub's side, we currently only support 1 relay at a time, which the user can configure via an environment variable. We do not support NIP-42 AUTH.

@nostr-wine
Copy link
Copy Markdown
Contributor

nostr-wine commented Mar 5, 2025

Now nostr.wine as the "client" must generate the unique key instead of the wallet service. You just need to associate the key you generate with a particular user. (And only the corresponding public key of this unique key is given to the wallet service. You as the client will keep the secret and use it for NWC communication). Does that make sense?

Yes, I understand your proposal but this doesn't support one of the main NWA features we used which was the ability to use a single pubkey on the client side.

This is still possible in this proposal. The wallet service can provide its own preferred relay in the p tag (see https://github.com/nostr-protocol/nips/pull/1818/files#diff-d1ed4c38da272aa7f99e29edb6739e8fb209c723585601a84f0fd511d1c679e0R215). It doesn't explicitly say the client MUST use that relay, but I think there are some things to consider if we force the wallet service to obey the client app on which relay(s) to use. I would love feedback / thoughts on this.

Unfortunately from Alby Hub's side, we currently only support 1 relay at a time, which the user can configure via an environment variable. We do not support NIP-42 AUTH.

Will the wallet service always send the 13194 to the relay in the client provided URI? If not, how would the client know where to look to find the 13194? How does that work with a 1 relay at a time restriction? I don't think you should force the wallet service to obey the client relay request but you must send the 13194 to the relay in the URI otherwise the client does not know which relay to use.

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Mar 5, 2025

Yes, I understand your proposal but this doesn't support one of the main NWA features we used which was the ability to use a single pubkey on the client side.

Ah, I see. But I don't think this works because the wallet service uses the pubkey of standard NIP-47 requests to know which app connection it is. Did you try creating two connections with the same pubkey for the same wallet service?

Will the wallet service always send the 13194 to the relay in the client provided URI? If not, how would the client know where to look to find the 13194? How does that work with a 1 relay at a time restriction? I don't think you should force the wallet service to obey the client relay request but you must send the 13194 to the relay in the URI otherwise the client does not know which relay to use.

For Alby Hub because it's self hosted and not publicly accessible, we use Alby Go as the user-facing NWA interface which communicates with Alby Hub over nostr to initiate the connection. Therefore, I think Alby Go can listen to the hub's info event and then re-broadcast it to the client-provided relay without Alby Hub having to be connected to it. (Note: we haven't done this yet)

@nostr-wine
Copy link
Copy Markdown
Contributor

nostr-wine commented Mar 5, 2025

Ah, I see. But I don't think this works because the wallet service uses the pubkey of standard NIP-47 requests to know which app connection it is. Did you try creating two connections with the same pubkey for the same wallet service?

I'm confused trying to understand what you mean by this. The wallet service knows who we are talking to because we are p-tagging the unique wallet connection pubkey (the one that sent the 13194 info event) with the request. NWA worked perfectly with the setup I'm describing.

All we need to do is add an optional secret to the client generated URI and then have the 13194 event include that secret if it was passed. This would allow us to have a similar flow to NWA without the extra event.

For Alby Hub because it's self hosted and not publicly accessible, we use Alby Go as the user-facing NWA interface which communicates with Alby Hub over nostr to initiate the connection. Therefore, I think Alby Go can listen to the hub's info event and then re-broadcast it to the client-provided relay without Alby Hub having to be connected to it. (Note: we haven't done this yet)

If your wallet service can't drop off the 13194 on the relay included in the client provided URI, then this proposal doesn't work at all currently. @asoltys does coinos support this correctly?

I think there is some disconnect because everyone always thinks of NWC as nostr clients on one side and wallet services on the other. There is a third participant and that is subscription services like nostr.wine. We have different requirements and goals. We aren't trying to manage a user's wallet, we are just trying to send them payment invoices when they are due. Reducing friction for services like us to implement NWC is important if we ever want it to be used beyond zaps.

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Mar 5, 2025

I think there is some disconnect because everyone always thinks of NWC as nostr clients on one side and wallet services on the other. There is a third participant and that is subscription services like nostr.wine. We have different requirements and goals. We aren't trying to manage a user's wallet, we are just trying to send them payment invoices when they are due. Reducing friction for services like us to implement NWC is important if we ever want it to be used beyond zaps.

I don't think this is a third participant, nostr.wine is just another client. A client is any type of app or service that wants some access to the user's wallet, even if it's receive-only access. A NWC client is not necessarily a typical nostr client. It can be anything - a PoS machine, a recurring payment service, a wallet interface, a game, ...

@nostr-wine
Copy link
Copy Markdown
Contributor

nostr-wine commented Mar 5, 2025

Have you read through the old NWA PR and the comments? I would highly recommend you do so because it would help us get on the same page.

There is actually a LOT of confusion about what a "client" is in that thread. I feel like Ben did a really good job of explaining why all the fields were necessary and the objectives of it. We also talked about "client" key management and why some services may not want to create one pubkey per connection.

We likely won't implement a NWC solution that requires us to store one private key per connection. It's not worth the added complexity on the relay REQ side alone. There are also security gains from not having to store and access potentially thousands of private keys (also mentioned in the NWA discussion).

@asoltys
Copy link
Copy Markdown

asoltys commented Mar 5, 2025

If your wallet service can't drop off the 13194 on the relay included in the client provided URI, then this proposal doesn't work at all currently. @asoltys does coinos support this correctly?

We don't allow the client to provide a relay. As the wallet provider we always use our own relay and inform the client of it in a postmessage that gets sent back when the user authorizes the connection.

I agree using an AUTH'd relay would be a good idea to not leak events/metadata for the connection. Our relay is public at this time. I'm not very familiar with relay authentication in general yet so open to suggestions on how we could improve there. I'm running strfry with near stock configuration for relay.coinos.io

Copy link
Copy Markdown

@ekzyis ekzyis left a comment

Choose a reason for hiding this comment

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

Looks good! It's just unfortunate that as-is, a 1-click flow from desktop web to a mobile NWC wallet without a server (like cashu.me) doesn't seem to be possible but I don't know how you want to contact the mobile NWC wallet without communicating some relay to it beforehand.

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Mar 6, 2025

Looks good! It's just unfortunate that as-is, a 1-click flow from desktop web to a mobile NWC wallet without a server (like cashu.me) doesn't seem to be possible but I don't know how you want to contact the mobile NWC wallet without communicating some relay to it beforehand.

@ekzyis in this current proposal the flow is that a QR code would be shown on the desktop web that the user would have to scan using cashu.me, and then cashu.me would include its preferred relay in the broadcasted info event to the client's relay. I believe this is completely possible, where do you see the issue?

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Mar 6, 2025

I agree using an AUTH'd relay would be a good idea to not leak events/metadata for the connection. Our relay is public at this time. I'm not very familiar with relay authentication in general yet so open to suggestions on how we could improve there. I'm running strfry with near stock configuration for relay.coinos.io

We also do not run an AUTH'd relay. I think if AUTH is added it's important to not require that wallet connections be tied to user identity (I guess it can be done by using the standard NIP-47 client and wallet service keys to sign these messages, but isn't in then kind of redundant to force them to sign additional messages?).

Our current solution is to use unique keys both for the client and wallet service per connection, and some minor constraints on our relay to ensure events cannot be queried without providing a filter with one of these keys (either querying by author, or a specific e or p tag). This way we prevent metadata leakage.

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Mar 6, 2025

We likely won't implement a NWC solution that requires us to store one private key per connection. It's not worth the added complexity on the relay REQ side alone. There are also security gains from not having to store and access potentially thousands of private keys (also mentioned in the NWA discussion).

Yes, I read the comments. I understand right now you can subscribe to a single pubkey and listen to events from all different wallet service keys. This is indeed simpler for your usecase. We run multiple services which subscribe to many keys on relays so have experience with what you mention above. One of our demo apps which does scheduled subscription payments I believe is very similar to the model nostr.wine could use to charge users for relay usage on a monthly basis: https://zapplanner.albylabs.com/

However, I would like to explain the other side of what your suggested change would mean:

  1. add the randomly generated NWA secret property and rely on this to identify users and change the spec to optimize for this particular usecase. We increase the complexity of the spec by adding a third "secret" (outside of the client and wallet keys) which I think adds a lot of confusion and increases complexity for wallet service and NWC library developers.
  2. require all wallet services to use unique wallet service keys per connection in order for this specific usecase to work. (even though I would recommend wallet services to do this - now this would be a strict requirement)
  3. on a database level require wallet services the client pubkey to be non-unique, but require the wallet service key to be unique. This seems a very strange architecture to me.

@nostr-wine please let me know if I miss something here.

@nostr-wine
Copy link
Copy Markdown
Contributor

nostr-wine commented Mar 6, 2025

Yes, I read the comments.

One of the comments is about why a secret (outside of client generated pubkey) is necessary for the client initiated nostr flow regardless of whether or not the client uses a unique key per connection.

Since the wallet service never shares their pubkey with the client app out of band, what happens if multiple 13194 events are posted within a few ms tagging the same client pubkey? Should the client app always assume the first received event is the legitimate one? What if they have the same timestamp? An attacker could monitor for these events (on your unauthenticated NWC relays) and immediately create duplicate 13194s. There is no way for the client app to know with certainty which event is from the wallet service without a shared secret. The p tag is public. Pubkeys are NOT secret and we should not call them secrets.

I understand right now you can subscribe to a single pubkey and listen to events from all different wallet service keys.

Not right now - this only worked with the original NWA proposal and Mutiny. Without a functional nostr 1 click flow we are not using NWC at all currently.

We run multiple services which subscribe to many keys on relays so have experience with what you mention above.

This is a lot easier to do on unprotected relays. We would like NWC to work on authenticated relays too. There are already several functional relays that provide this service for nostr DMs (you can learn more about the architecture here: https://docs.nostr.wine/inbox/readme - we even had added 33194 events for NWA). If the service needs to authenticate for every single pubkey it is monitoring and then open individual REQs for each it would be impractical with just a few thousand wallet connections.

2. require all wallet services to use unique wallet service keys per connection in order for this specific usecase to work. (even though I would recommend wallet services to do this - now this would be a strict requirement)

Actually it does not need to be unique per connection, just per wallet user. We would never (as a service) want to create more than one active connection at a time with the same wallet user anyway.

With that being said, we can't imagine any wallet service would actually want to reuse keys. The privacy issues are a lot more relevant on the wallet side. You wouldn't want a random outside observer to be able to track a single user's wallet interactions. On the flip side, I don't see any issue If everyone can see that nostr.wine is sending NWC events to a bunch of random pubkeys.

@ekzyis
Copy link
Copy Markdown

ekzyis commented Mar 6, 2025

Looks good! It's just unfortunate that as-is, a 1-click flow from desktop web to a mobile NWC wallet without a server (like cashu.me) doesn't seem to be possible but I don't know how you want to contact the mobile NWC wallet without communicating some relay to it beforehand.

@ekzyis in this current proposal the flow is that a QR code would be shown on the desktop web that the user would have to scan using cashu.me, and then cashu.me would include its preferred relay in the broadcasted info event to the client's relay. I believe this is completely possible, where do you see the issue?

Oh, you are right, I didn't consider the QR code. I guess it can count as a 1-click flow. I was thinking of "1-click" more in terms of "the client opens the wallet" (no context switch), but with the QR code flow, the user has to manually open their wallet.

But it's okay, just wanted to mention and as mentioned, I wouldn't know how to improve this anyway.

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Mar 6, 2025

Since the wallet service never shares their pubkey with the client app out of band, what happens if multiple 13194 events are posted within a few ms tagging the same client pubkey? Should the client app always assume the first received event is the legitimate one? What if they have the same timestamp? An attacker could monitor for these events (on your unauthenticated NWC relays) and immediately create duplicate 13194s. There is no way for the client app to know with certainty which event is from the wallet service without a shared secret. The p tag is public.

I think it makes more sense that we add some basic recommendations for relay protection for wallet service relay runners to prevent against allowing all their notes to be crawled (e.g. how Alby's relay does it). If you wish to use a random public relay sure you are at a very small risk of this attack, but there's almost no incentive to do this attack...

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Mar 6, 2025

Oh, you are right, I didn't consider the QR code. I guess it can count as a 1-click flow. I was thinking of "1-click" more in terms of "the client opens the wallet" (no context switch), but with the QR code flow, the user has to manually open their wallet. But it's okay, just wanted to mention and as mentioned, I wouldn't know how to improve this anyway.

@ekzyis you could have a native lightning wallet app on your desktop which registers the nostr+walletauth scheme. Then everything can be done on a single device and truly with one click. There's just no NWC-enabled lightning wallets I know of that do this yet.

EDIT: it can also technically be done with browser extensions (maybe the Alby Extension will be able to handle these links at sometime in the future)

@nostr-wine
Copy link
Copy Markdown
Contributor

I think it makes more sense that we add some basic recommendations for relay protection for wallet service relay runners to prevent against allowing all their notes to be crawled (e.g. how Alby's relay does it). If you wish to use a random public relay sure you are at a very small risk of this attack, but there's almost no incentive to do this attack...

We think this is much more complicated, open ended, and unlikely to be properly enforced. Not all relay implementations support these type of restrictions. Including a secret is simple and only relevant for those updating their implementations to support this new "1-click" flow.

We need to move on to other things so we'll step aside from here. If others are happy with it as is don't let our opinions get in the way!


- `pubkey` Required. The corresponding pubkey of the secret generated by the **client**.

The **wallet service** MAY ignore all the below optional parameters.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it'd be good to reference an optional NIP-68 client ID here for some basic phishing protection.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In the long term I think it's a good idea but I am unsure if it will be adopted in the short-mid term due to much more additional work on all sides (wallet service having to fetch the event and calculate WoT, client must handle a redirect uri, developer required to publish this event).

Should it be a NIP-19 nevent so that it also contains the relay? Do you have suggestions on the field name?


The client MAY generate its own secret and co-ordinate only public keys with the wallet service so that secrets are never exposed.

### HTTP Confirmation Page
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm really glad we're talking about standardizing this HTTP-based flow. The experience you guys have had with alby for a while is a much better UX than the copy-paste flow IMO and works especially nicely for custodial wallets.

My main worry here is that from a security standpoint, this gets very close to OAuth, but without the guarantees that OAuth has around phishing prevention, csrf, token rotation, etc. OAuth is really battle-tested and while it can be painful, trying to re-implement OAuth is a very common security foot-gun. When building out UMA Auth we spent quite a while in security review with 3P security experts and they insisted on simply using OAuth for the connection handshake when there's a wallet available over HTTP.

I do quite like that this method never has the secret leave the client application. I'd initially tried to hold that property with UMA Auth as well, but it was much trickier to get key rotation correct that way. I still think it might be doable, but doing without user interaction is awkward. Eventually I resigned myself to the idea of the wallet being the source of truth on key state, etc.

For reference:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These are all valid points.

I think the simplicity of NWC is a big reason it has gained adoption and we should not take this lightly. I think the implications of OAuth also push hosted wallets further from self-hosted ones, which currently in this proposal I have tried to keep as aligned as possible.

@asoltys I would be interested to hear your perspective.

- `isolated` Optional. The makes an isolated app connection / sub-wallet with its own balance and only access to its own transaction list. e.g. `&isolated=true`.
- `metadata` Optional. Url encoded, JSON-serialized metadata that describes the app connection.

The **user** MUST be presented with a confirmation page to be able to review and approve or decline the connection.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any thoughts on how this happens? For UMA auth, we use the user's UMA (or lightning address) fetch a configuration document similar to the OIDC discovery doc. From there, we just read the authorization_endpoint field to decide what url to open. It's really similar to how oauth works in this regard and allows the user to just type a lightning address to open the right endpoint (or detect that this method won't work).

See https://docs.uma.me/uma-auth/uma-auth-client/client-manual-implementation#oauth-20-exchange

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Our current solution is to have a list of wallets to pick from (e.g. Bitcoin Connect has a list of wallets it supports and knows what flows to use, and in the http case what url to use).

I think there are pros and cons of both approaches though.

@vitorpamplona
Copy link
Copy Markdown
Collaborator

Are we doing this? Can we clear all debates over this and move to either implement/merge or close it?

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Jul 31, 2025

@vitorpamplona currently:

  • Alby Hub + CoinOS have implemented the web flow
  • Alby Hub/Go + Flash wallet have implemented the Nostr flow
  • It's implemented in Bitcoin Connect and Alby JS SDK.

We hope to see more adoption but I think it's a good start.

@MegalithicBTC
Copy link
Copy Markdown

Thanks for all of this.

I've reviewed this with a goal of implementing this for https://rizful.com/

I understand how this works and how we could modify Rizful to incorporate this.

But: The "stakes are high" here -- a flaw in this flow, which led to the loss of user funds, could tarnish the reputation of NWC going forward.

My reservation is the same as @jklein24 .... Oauth is complicated, and I don't personally understand it very well.... I think before implementation, we need an Oauth security expert to review this.

Specifically I look at this sample url, used to make a new connection....

https://example.com/connections/new?pubkey=b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4

... And my grug-brain says "This is too simple. Oauth is like 10x more complicated than this. Oauth must be so much more complicated for a reason."

I know Block has a lot of security experts, I wonder if they would lend us one to take a look at this?

@MegalithicBTC
Copy link
Copy Markdown

I have a vague idea of an attack on this which is not technical but relies on user deception.

Currently, if I want to connect Rizful and Primal, I need to proactively copy the NWC code from Rizful, and paste it into Primal. I've got to do a lot of active thinking, and I'm pretty unlikely to have a "fake Primal App" on my phone (or web browser), into which I could be tricked into pasting my NWC string.

Looking at this proposal, here, we see:

https://github.com/rolznz/nips/blob/89fe83d5ebb7755a021c68b0aa643f79706f01aa/47.md?plain=1#L153

icon: Optional. The URL of an icon of the client app to display on the confirmation page."

So this means that the CLIENT is specifying a icon and a name which the NWC service might display on their "are you sure you want to connect?" page. The problem I think comes up in this flow:

  1. user visiting a malicious nostr website
  2. malicious website has a button "primal wants to send you 100 sats! click here!"
  3. user clicks button
  4. user is redirected to a URL on rizful.com, where he sees the primal icon and the primal name, and the text "connect rizful to primal"

So to combat this, rizful.com could refuse to display the icon and name parameters, and instead show...

"An unknown app is trying to connect to Rizful, do you want to allow it?

... BUT -- when I think of all time times that I have connected apps on the internet -- like, connecting Google to X, or two banks together, or anything like that.... a central part of the security is that they are GUARANTEEING to me that I am connecting to the app or service that i think I am connecting to....

Now, of course, Nostr doesn't use centralized identity... but shouldn't we somehow verify in theClient-Created Secret flow that the request to create a new NWC connection is coming from the same entity that the user thinks it is coming from?

This could be accomplished I think with DNS records? This is like an SSL/TLS thing, where we just need to verify that the request from the client is coming from the same domain that it is claiming to come from? (But... these requests will be coming from apps on people's phones mostly, so this won't work? )

Or is some sort of nostr-native way better?

Or am I being paranoid ....and this is not needed at all in everyone's opinion?

@jklein24
Copy link
Copy Markdown
Contributor

I have a vague idea of an attack on this which is not technical but relies on user deception.

Currently, if I want to connect Rizful and Primal, I need to proactively copy the NWC code from Rizful, and paste it into Primal. I've got to do a lot of active thinking, and I'm pretty unlikely to have a "fake Primal App" on my phone (or web browser), into which I could be tricked into pasting my NWC string.

Looking at this proposal, here, we see:

https://github.com/rolznz/nips/blob/89fe83d5ebb7755a021c68b0aa643f79706f01aa/47.md?plain=1#L153

icon: Optional. The URL of an icon of the client app to display on the confirmation page."

So this means that the CLIENT is specifying a icon and a name which the NWC service might display on their "are you sure you want to connect?" page. The problem I think comes up in this flow:

  1. user visiting a malicious nostr website
  2. malicious website has a button "primal wants to send you 100 sats! click here!"
  3. user clicks button
  4. user is redirected to a URL on rizful.com, where he sees the primal icon and the primal name, and the text "connect rizful to primal"

So to combat this, rizful.com could refuse to display the icon and name parameters, and instead show...

"An unknown app is trying to connect to Rizful, do you want to allow it?

... BUT -- when I think of all time times that I have connected apps on the internet -- like, connecting Google to X, or two banks together, or anything like that.... a central part of the security is that they are GUARANTEEING to me that I am connecting to the app or service that i think I am connecting to....

Now, of course, Nostr doesn't use centralized identity... but shouldn't we somehow verify in theClient-Created Secret flow that the request to create a new NWC connection is coming from the same entity that the user thinks it is coming from?

This could be accomplished I think with DNS records? This is like an SSL/TLS thing, where we just need to verify that the request from the client is coming from the same domain that it is claiming to come from? (But... these requests will be coming from apps on people's phones mostly, so this won't work? )

Or is some sort of nostr-native way better?

Or am I being paranoid ....and this is not needed at all in everyone's opinion?

FWIW, this type of phishing attack is exactly the type of thing we were worried about in designing the UMA Auth flow, which is why we just went with a more standard OAuth scheme with dynamic nostr-driven client app registration. See the docs here

@MegalithicBTC
Copy link
Copy Markdown

MegalithicBTC commented Aug 20, 2025

Right, it's even more effective in the context of phishing, because then it's like "I got an email from Primal and they want to send me 1000 sats! Sure I'll connect Rizful to Primal!"

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Aug 21, 2025

Yeah, @jklein24 's NIP-68 proposal is of course much more secure than just a name and icon. I just think it adds a lot of extra complexity that could be done later when we have more apps and users actually using this connection flow. (wallet service having to fetch the event and calculate WoT, client must handle a redirect uri, developer required to publish this event, user must have an adequate WOT of mutual followers who follow this app developer's nostr account to know that that app is trustworthy...).

@MegalithicBTC
Copy link
Copy Markdown

@rolznz great points. We would like to add the @jklein24 proposed workflow (or something like it, if someone comes up with something more simple... ) at the point that there is a client that supports it. We're nervous about the https version because I think if there is an exploit, the NWC service is likely to take the blame.

ALSO... I know this is not NOSTR-native, but I still think a custom DNS record could be a simple solution... isn't this exactly the kind of thing that custom DNS records are used for? (I'm not sure) .... I wonder if usage of a custom DNS record could be added to your https spec.....


#### Example HTTP confirmation URL

`https://example.com/connections/new?pubkey=b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4`
Copy link
Copy Markdown

@ekzyis ekzyis Sep 28, 2025

Choose a reason for hiding this comment

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

Wouldn't it make sense to standardize this endpoint and use a Well-known URI, similar to LNURL?

Afaict, the endpoint for Coinos was hardcoded to /apps/new in Alby and I am not sure how I should have known that without looking into Alby's or Coinos' implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That sounds good to me.

@ekzyis
Copy link
Copy Markdown

ekzyis commented Sep 28, 2025

@MegalithicBTC

Or am I being paranoid ....and this is not needed at all in everyone's opinion?

No you are not, I share your concerns

@rolznz

I just think it adds a lot of extra complexity that could be done later when we have more apps and users actually using this connection flow

I think "could be done later" is a flawed assumption. It will be MUCH harder to fix this connection flow later if a severe vulnerability was found "when we have more apps and users actually using this connection flow."


I will implement this as-is on SN to create Coinos wallets without leaving SN but it should not be seen as an endorsement of the current spec. I just want to play around with it first and see if/how I can break it.

@MegalithicBTC
Copy link
Copy Markdown

MegalithicBTC commented Sep 29, 2025

I think the safest way for a nostr client application to get an NWC code (and lightning address) from a NWC wallet is to require the user to take affirmative action to enter a code into into the nostr client application, and then nostr client application then uses that code to get the secrets.

With this in mind, we have created this demo client application which holds a user's hand from Signup --> Get NWC Code & Lightning address. It uses a "token exchange" security model.

https://github.com/MegalithicBTC/rizful-integration-demo

This demo flow handles:

  1. Sign up for Rizful
  2. Get 2FA-like code from Rizful
  3. User enters 2FA code in client app
  4. Client app automatically gets NWC string and Lightning address from Rizful

Our Rizful implementation requires that a user sign up for Rizful with a email/password ... you can see our rationale in the ReadMe - I would not be surprised if many/most nostr developers didn't like this and would rather that user's funds be secured only by their nsec.

... But the main point is that a user must take affirmative action to enter the 2FA into their client application, which I think might be better security practice than any "automatic" flow.

@MegalithicBTC
Copy link
Copy Markdown

Obviously there is a server-side implementation to this flow. It's not in this public repo, but maybe it's obvious how it works (I'm not sure.) One of the key security features is that the 2FA-like code is time-limited, in our case we are limiting it to three minutes, after which it becomes invalid.

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Sep 29, 2025

@MegalithicBTC it's not 100% automatic as the user should be presented with a confirmation screen to view the app's permissions and either accept or deny the connection.

Here's an example:
image

(This is the HTTP flow, but it's also the same with the NWA flow)

I guess the 2FA code is a bit better because it can only be exchanged for a NWC secret once. But still I think a big part of this proposal is to avoid the need to copy-paste.

@MegalithicBTC
Copy link
Copy Markdown

Right.

In my mind "the need to copy-paste" is an important security feature, similar to how 2FA codes work... if the user pastes the code into an app or website, then the user is saying "I have identified this app/website as one that I want to give the ability to make NWC payments on my behalf."

Then if, two weeks later, the user approaches Rizful and says "my wallet was drained, what happened??" We can always say -- "When you pasted the code we gave you, did you paste it into a app or website that you trust?"

But you're definitely correct that it adds friction.

@MegalithicBTC
Copy link
Copy Markdown

FYI -- This onboarding flow ( https://github.com/MegalithicBTC/rizful-integration-demo ) is now integrated in jumble.social -- you can see what it looks like from the perspective of a user here: https://docs.megalithic.me/using-rizful/use-jumble -- see "The Easy Way".

@ekzyis
Copy link
Copy Markdown

ekzyis commented Oct 2, 2025

I think the safest way for a nostr client application to get an NWC code (and lightning address) from a NWC wallet is to require the user to take affirmative action to enter a code into into the nostr client application, and then nostr client application then uses that code to get the secrets.

[...]

But you're definitely correct that it adds friction.

I agree with @rolznz here:

But still I think a big part of this proposal is to avoid the need to copy-paste.

Not directly copying the secret but a code with expiration to share them makes it more secure, but it seems to solve more for better security than for better UX. It's still better UX, but not enough.

I think there's a way to keep the original flow and still have similar security guarantees. Afaict, we only need to make sure the wallet can verify that the request actually comes from a service so it can provide security guarantees to the user when approving or denying the request (see below for terminology).

After talking to @rolznz yesterday, I came to the following idea, maybe you can tell me what is wrong with it.

It's inspired by SSO where you first need to register with OAuth providers to get a client_id and client_secret before you can add a 'Login with Github' or 'Login with Apple' button, see here.

For cases like Zappy Bird where there is no backend that can keep secrets (single-page apps), a client_id + redirect URL is used, see here.

Terminology

name description examples
service wants to be associated with payments; shows up as part of the approval request in the wallet Stacker News, nostr.wine
wallet wants to provide API for payments and give users security guarantees about connection Rizful, Coinos, Alby
client wants to show a button to connect a wallet (Connect Coinos, Connect Rizful etc.) to make payments on behalf of the user Stacker News Frontend, Zappy Bird, any zap-enabled nostr client like Damus or Primal
user user of service Stacker News User, Zappy Bird Player, nostr.wine subscriber, any user of a zap-enabled nostr client

Steps

  1. Registration

1.1 service requests to register with wallet

Registration here means that the service requests a client_id+client_secret that the wallet will associate with the service and the service will associate with the wallet.

1.2. wallet generates unique client_id+client_secret

The wallet MUST verify authenticity of request. This can be done via any secure, authenticated channel and must most likely be done manually by a human. Examples for secure, authenticated channels are Signal, nostr DMs etc.

A wallet can also provide a simple contact form for this because it is the wallet's responsibility to authenticate requests.

1.3. wallet approves request by replying with client_id+client_secret

service can now implement a "Connect wallet" button

  1. Connect flow

2.1. user presses on "Connect wallet”

2.2. client creates a shared secret with wallet via ECDH

Not sure yet which keys are used here, could be nostr, but maybe doesn't have to be

2.3. client encrypts connect request to wallet with shared secret but does not send it yet

2.4. client sends encrypted connect request to server

2.5. server authenticates client

Here the service can make sure it doesn’t sign blindly but only if it’s actually a user of the service. This can use any authentication scheme like JWTs.

2.6. server signs request to connect wallet using client_id, client_secret

2.7. server responds with signature

2.8. client signs request using shared secret

This step makes sure that client can trust server to not send the encrypted request with its own signature to wallet to get the credentials because the request does not have the client's signature yet, assuming wallet also verifies the signature of the client in 2.9

2.10. client sends the encrypted connect request with server+client signature to wallet

2.11. wallet verifies both signatures

2.12. wallet shows 'Connect wallet to service?’

In this step, the wallet can now be sure the request comes from the service because the service signed the request.

2.13. user approves or denies request in wallet

If approved, wallet responds with the requested credentials to client.

Additional context

  • Maybe the connect request doesn't need encryption, only signatures from client+server. It's not the request that is sensitive, but the response.

  • Requests can be any transport method (Nostr, HTTP etc.) or a combination of any.

  • Since Zappy Bird does not run a backend, but has a domain, it would need to register a redirect URL with every wallet as mentioned above and here:

In any case, with both the Implicit Flow as well as the Authorization Code Flow with no secret, the server must require registration of the redirect URL in order to maintain the security of the flow.

Security Considerations

The only way the authorization code grant with no client secret can be secure is by using the “state” parameter and restricting the redirect URL to trusted clients. Since the secret is not used, there is no way to verify the identity of the client other than by using a registered redirect URL. This is why you need to pre-register your redirect URL with the OAuth 2.0 service.

(In theory, Stacker News could also implement this without client_secret and provide a redirect URL instead. edit: oh, actually, no, the server could see the response that way.)

I think this actually goes back to what @MegalithicBTC mentioned here at August 20:

This could be accomplished I think with DNS records? This is like an SSL/TLS thing, where we just need to verify that the request from the client is coming from the same domain that it is claiming to come from? (But... these requests will be coming from apps on people's phones mostly, so this won't work? )

I think registering a redirect URL could accomplishy exactly that! The wallet could use DNS records like TXT to verify the authenticity of the registration request.

Sorry for the wallet of text. I hope this made sense. Let me know what you think! Is this too complicated? Is this not as secure as I think it is?

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Oct 22, 2025

@ekzyis if I understand correctly you are basically suggesting something similar to OAuth, right? one downside I see of your version is it relies on a server for the app itself. Most NWC apps today are client-side. I also don't think expecting apps to register with every wallet is scalable. Jeremy's NIP-68 would mean developers only need to register their app once (by posting it to nostr).

```javascript
window.dispatchEvent(new CustomEvent("nwc:success", {
detail: {
relayUrl,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

relayUrl is not a good name considering there can be multiple relays

@ekzyis
Copy link
Copy Markdown

ekzyis commented Oct 22, 2025

relies on a server for the app itself

No, you can register a redirect URL with the wallet, see this in my comment above:

For cases like Zappy Bird where there is no backend that can keep secrets (single-page apps), a client_id + redirect URL is used, see here.

[...]

Since Zappy Bird does not run a backend, but has a domain, it would need to register a redirect URL with every wallet as mentioned above and here:


I also don't think expecting apps to register with every wallet is scalable

Yes, it's not great for apps, but it's great for wallets to be able to show their users something they can actually trust.

Also, how many wallets are there that support this? So far, it's only Coinos right? Don't you already have to add a new button in your app for each new wallet? In the process of adding the button, you could reach out to the wallet provider to register, no?

I agree with you that this is the main downside though. But I think it's worth it for users and wallet providers.

I haven't had the time yet to implement a proof of concept of this, but I'm planning to.

Jeremy's #1383 would mean developers only need to register their app once

Thanks, I didn't know about it, will check it out!

@rolznz
Copy link
Copy Markdown
Contributor Author

rolznz commented Oct 22, 2025

Also, how many wallets are there that support this? So far, it's only Coinos right? Don't you already have to add a new button in your app for each new wallet? In the process of adding the button, you could reach out to the wallet provider to register, no?

Yeah, coinos and Alby Cloud currently.

If your app manually maintains a list of wallets, yes. But I don't think that's a great solution either. 🤔

Our solution so far for this is that apps would use a package like Bitcoin connect, which maintains a registry of wallets. (and there could be similar libraries built for different platforms/environments).


The **user** opens the URL in the **wallet service** by scanning a QR code, handling a deeplink or pasting in a URI, and MUST be presented with a confirmation page.

If the **user** approves the request, a new connection should be created for the **client**'s public key (specified in the base path of the authorization URI). Once the connection is created, the NIP-47 info event MUST be broadcasted to the relays specified in the authorization URI, and the info event MUST include a `p` tag containing the public key of the **client** this info is for, so that the **client** can discover the public key of the **wallet service**. The `p` tag MAY contain the recommended relay url of the **wallet service**. If provided, the recommended relay url MUST be used.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

for multiple relay support, is it possible to support multiple recommended relay URLs here?

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.

8 participants