Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nip46 auth with NDK #1636

Merged
merged 33 commits into from
Dec 14, 2024
Merged

Nip46 auth with NDK #1636

merged 33 commits into from
Dec 14, 2024

Conversation

riccardobl
Copy link
Member

@riccardobl riccardobl commented Nov 22, 2024

Description

Nip46 auth using NDK (reimplementation of #1311)

Depends on #1590 closes #864

This PR updates the behavior of the "Login with Nostr" button by adding an intermediary page that presents the following login options:

  • Login with extension: the current behavior using the browser extension eg. alby
  • Login by pasting a bunker:// token
  • Login by using a nip-05 address

Screenshots

image
image
image

Additional Context

The login by qr code/nostrconnect:// has been removed due to the thing we discussed.

I've been testing using https://nsec.app/

Checklist

Are your changes backwards compatible? Please answer below:

yes

Did you QA this? Could we deploy this straight to production? Please answer below:

yes , tested both bunker:// , nip-05 and extension to ensure there is no regression.

For frontend changes: Tested on mobile? Please answer below:

Yes

Did you introduce any new environment variables? If so, call them out explicitly here:

no

Copy link

socket-security bot commented Nov 22, 2024

No dependency changes detected. Learn more about Socket for GitHub ↗︎

👍 No dependency changes detected in pull request

@riccardobl riccardobl changed the title Nip46 v2 Nip46 auth with NDK Nov 22, 2024
@riccardobl riccardobl requested a review from Copilot November 22, 2024 15:51
Copilot

This comment was marked as off-topic.

@ekzyis ekzyis mentioned this pull request Dec 3, 2024
4 tasks
@ekzyis
Copy link
Member

ekzyis commented Dec 4, 2024

I went ahead and rebased this branch on #1590, I hope you don't mind.

However, I wonder if this is in draft because it depends on #1590 even though #1590 itself was ready for review? Or is this in draft because the changes between this PR and #1590 (git diff ndk..nip46V2) aren't ready for review yet?

Also, can you use the PR template which mentions how you tested this (which remote signer etc.) or if there was anything unclear during implementation? The template helps a lot with reviews.

<p>
Please confirm this action on your remote signer.
</p>
{!challengeUrl && (<pre>{challenge}</pre>)}
Copy link
Member Author

Choose a reason for hiding this comment

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

nip46 doesn't say this must always be an url, so i've added some extra code to show it as a text if it is not a valid url.
It could contain instruction like "open your signer and confirm"

console.log('nostr auth error', e)
setExtensionError({ message: `${text} failed`, details: e.message })
// authorize user
const auth = useCallback(async (nip46token) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Since NDK has this nice signer abstraction, i've made this single callback to handle both nip46 and nip07 logins.

</li>
<li>
<a href='https://app.nsecbunker.com/'>nsecBunker</a><br />
available as: SaaS or self-hosted
Copy link
Member Author

Choose a reason for hiding this comment

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

the original PR had more signers in this list, but i've found they have issues or don't seem to be very well maintained, so i've decided to keep only these two

@ekzyis
Copy link
Member

ekzyis commented Dec 8, 2024

what do you mean? There is only one signature per login

Sorry, I meant two confirmations in the signer required. I think the solution would be to use NIP-98 since permission to sign kind:27235 is already included in the basic permissions. So we can ask the signer to sign kind:27235 without another confirmation and send that signed event to the server instead of our custom event.

that would expose user secrets

Even though my idea about running the signer on the server is no longer relevant thanks to NIP-98, which user secrets would this expose since this is remote signing? Do you mean the secrets in the bunker URL?

This pr is only for login, as the feature request was only for that.
Crossposting needs more work that includes having a way to set which signer to use and store the secret to the vault.

Ah okay, makes sense. I forgot I was using firefox where I didn't have the nostr extension so I thought this broke crossposting but it didn't.

Are you trying to login twice with the same nip46 token?

Oh yes, that was the issue. Works with fresh tokens. Is it possible to handle this? Only showing a spinner with no info is bad UX (related to my question why you decided to use a modal).

@riccardobl
Copy link
Member Author

  1. URL parsing issue

should be fixed now, i've added the same workaround

  1. Confusing modal

i've replaced the modal with this

image

@riccardobl
Copy link
Member Author

Sorry, I meant two confirmations in the signer required.

you mean the two popups that comes up in nsec.app?
I think this is either an ux problem in nsec.app or NDK is not using nip46 to its full extent, because it is possible to ask for permissions in the connect method

Even though my idea about running the signer on the server is no longer relevant thanks to NIP-98, which user secrets would this expose since this is remote signing? Do you mean the secrets in the bunker URL?

yes the server could steal the bunker secret

NIP-98 seems another auth method that is unrelated to NIP-46

Oh yes, that was the issue. Works with fresh tokens. Is it possible to handle this? Only showing a spinner with no info is bad UX (related to my question why you decided to use a modal).

we can only add an arbitrary timeout, it is the signer (nsec.app) that decides to ignore these events instead of replying with an error

@riccardobl
Copy link
Member Author

NDK is not using nip46 to its full extent

i think it's ndk, see

  return new Promise((resolve, reject) => {
      const connectParams = [this.userPubkey ?? ""];
      if (this.secret) connectParams.push(this.secret);
      if (!this.bunkerPubkey) throw new Error("Bunker pubkey not set");
      this.rpc.sendRequest(
        this.bunkerPubkey,
        "connect",
        connectParams,
        24133,
        (response) => {
          if (response.result === "ack") {
            this.getPublicKey().then((pubkey) => {
              this._user = new NDKUser({ pubkey });
              resolve(this._user);
            });
          } else {
            reject(response.error);
          }
        }
      );
    });

it should include a third optional_requested_permissions connectParams

@riccardobl riccardobl requested a review from ekzyis December 8, 2024 20:11
@ekzyis
Copy link
Member

ekzyis commented Dec 8, 2024

you mean the two popups that comes up in nsec.app?

yes

I think this is either an ux problem in nsec.app or NDK is not using nip46 to its full extent, because it is possible to ask for permissions in the connect method

I think it's an issue with how we're using NIP-46 for our authentication. We don't need to ask for additional permissions, we're just not using the permissions we're given:

2024-12-08-211236_1920x1200_scrot

NIP-98 seems another auth method that is unrelated to NIP-46

NIP-46 is only remote signing, it's not actually an auth method. NIP-98 is the auth method. It's the same that we currently do (sign an event) but standardized so that we don't have to ask for permission to sign the event since that permission was already granted

we can only add an arbitrary timeout, it is the signer (nsec.app) that decides to ignore these events instead of replying with an error

We could show a message like "are you sure you used a fresh token?" after the timeout but not actually abort the login because the user might just be slow with pressing "confirm" in their signer

@riccardobl
Copy link
Member Author

riccardobl commented Dec 8, 2024

Ah, i get what you mean now. Is it defined anywhere that kind 27235 is supposed to be a default permissions or is it a choice of nsec.app?
We can change kind of the event, in the end we just need the signature, but isn't it an arbitrary choice of the signer?

EDIT: changed to use kind 27235

@ekzyis
Copy link
Member

ekzyis commented Dec 8, 2024

Is it defined anywhere that kind 27235 is supposed to be a default permissions or is it a choice of nsec.app?
We can change kind of the event, in the end we just need the signature, but isn't it an arbitrary choice of the signer?

I don't know, but it's at least a standard.

EDIT: changed to use kind 27235

Thanks, much better now!

@riccardobl
Copy link
Member Author

riccardobl commented Dec 8, 2024

NIP-46 is only remote signing, it's not actually an auth method. NIP-98 is the auth method. It's the same that we currently do (sign an event) but standardized so that we don't have to ask for permission to sign the event since that permission was already granted

NIP-98 requires its own flow see. It is basically nostr basic auth, i don't think it is worth (or even possible) to implement all that for us, so i've just added some generic metadata to make it "pass" just in case some signer is doing some extra validation

Copy link
Member

@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.

Some CSS suggestions:

diff --git a/components/nostr-auth.js b/components/nostr-auth.js
index 697853af..e056516b 100644
--- a/components/nostr-auth.js
+++ b/components/nostr-auth.js
@@ -138,13 +138,13 @@ export function NostrAuth ({ text, callbackUrl, multiAuth }) {
 
   return (
     <>
-      <h3 className='w-100 pb-2'>{status.title ? status.title : ((text || 'Login') + ' with Nostr')}</h3>
+      <h3 className='w-100 mb-0 pb-3 text-center'>{status.title ? status.title : ((text || 'Login') + ' with Nostr')}</h3>
       {status.error && <NostrError message={status.msg} />}
       {status.loading
         ? (
           <>
-            <Moon className='spin fill-grey' width='50' height='50' />
-            <div className='text-muted pt-4 pb-4 w-100'>{status.msg}</div>
+            <Moon className='spin fill-grey' width='24' height='24' />
+            <div className='text-muted py-3 w-100 text-center'>{status.msg}</div>
             {status.button && (
               <Button
                 className='w-100' variant='primary'
before

2024-12-08-213629_1920x1200_scrot

after

2024-12-08-213701_1920x1200_scrot

How does the NIP-05 login work and how can I test it? I see that NIP-46 mentions this now:

nip05 login is removed

Should we drop it and only mention bunker://?

@riccardobl
Copy link
Member Author

Added the timeout

image

@riccardobl
Copy link
Member Author

riccardobl commented Dec 8, 2024

Some CSS suggestions:

wouldn't this make it look a bit odd compared to the other login screens and the general sn layout that is generally aligned to left?

image

@riccardobl
Copy link
Member Author

How does the NIP-05 login work and how can I test it? I see that NIP-46 mentions this now:

you can use this from the same nsec.app page
image

Co-authored-by: ekzyis <[email protected]>
Copy link
Member

@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.

wouldn't this make it look a bit odd compared to the other login screens and the general sn layout that is generally aligned to left?

Mhh, I don't know. I just felt that the spinner in the center with everything else not in the center looks odd. But maybe it's just me or it's just too big for me.

So please ignore if you don't agree, @huumn will have the last word anyway.

Going to approve this now since my comment about the timeout implementation and the wording of the hint are mostly just nitpicks.

Comment on lines +45 to +46
const [suggestion, setSuggestion] = useState(null)
const suggestionTimeout = useRef(null)
Copy link
Member

Choose a reason for hiding this comment

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

Sorry for the micromanagement, but do you really need 1x useState, 1x useRef, 1x useEffect and two functions to show a message after some delay? Is this not enough:

diff --git a/components/nostr-auth.js b/components/nostr-auth.js
index 9cc85716..7ea093db 100644
--- a/components/nostr-auth.js
+++ b/components/nostr-auth.js
@@ -42,8 +42,7 @@ export function NostrAuth ({ text, callbackUrl, multiAuth }) {
     button: undefined
   })
 
-  const [suggestion, setSuggestion] = useState(null)
-  const suggestionTimeout = useRef(null)
+  const [showSuggestion, setShowSuggestion] = useState(false)
   const toaster = useToast()
 
   const challengeResolver = useCallback(async (challenge) => {
@@ -93,23 +92,6 @@ export function NostrAuth ({ text, callbackUrl, multiAuth }) {
     })
   }, [])
 
-  const clearSuggestionTimer = () => {
-    if (suggestionTimeout.current) clearTimeout(suggestionTimeout.current)
-  }
-
-  const setSuggestionWithTimer = (msg) => {
-    clearSuggestionTimer()
-    suggestionTimeout.current = setTimeout(() => {
-      setSuggestion(msg)
-    }, 10_000)
-  }
-
-  useEffect(() => {
-    return () => {
-      clearSuggestionTimer()
-    }
-  }, [])
-
   // authorize user
   const auth = useCallback(async (nip46token) => {
     setStatus({
@@ -132,9 +114,9 @@ export function NostrAuth ({ text, callbackUrl, multiAuth }) {
         signer.once('authUrl', challengeResolver)
       }
 
-      setSuggestionWithTimer('Having trouble? Try to use a fresh token or NIP-05 address')
+      const timeout = setTimeout(() => setShowSuggestion(true), 10_000)
       await signer.blockUntilReady()
-      clearSuggestionTimer()
+      clearTimeout(timeout)
 
       setStatus({
         msg: 'Signing in',
@@ -160,8 +142,6 @@ export function NostrAuth ({ text, callbackUrl, multiAuth }) {
       })
     } catch (e) {
       setError(e)
-    } finally {
-      clearSuggestionTimer()
     }
   }, [])
 
@@ -182,8 +162,10 @@ export function NostrAuth ({ text, callbackUrl, multiAuth }) {
                 {status.button.label}
               </Button>
             )}
-            {suggestion && (
-              <div className='text-muted text-center small pt-2'>{suggestion}</div>
+            {showSuggestion && (
+              <div className='text-muted text-center small pt-2'>
+                Having trouble? Make sure you used a fresh token or a valid NIP-05 address.
+              </div>
             )}
           </>
           )

Did you do it this way so you can clear the timeout when the page unmounts? From what I can tell, this doesn't seem to be required but if so, I think only useRef and useEffect is what you need. The suggestion message doesn't require state imo. it can just be hardcoded. So probably related to what you mentioned in the other comment with your preference of avoiding to hardcode stuff.

Copy link
Member Author

@riccardobl riccardobl Dec 9, 2024

Choose a reason for hiding this comment

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

The clearSuggestionTimer in the finally block is to avoid causing an useless state update if we are showing the error, the clearSuggestionTimer on unmount is to not hold the context (up to 10 seconds) from the garbage collector. Generally i find it is a good practice to make code that clean after itself.

@ekzyis ekzyis requested a review from huumn December 9, 2024 17:38
Copy link

gitguardian bot commented Dec 13, 2024

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
Once a secret has been leaked into a git repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@huumn huumn merged commit bdd2413 into stackernews:master Dec 14, 2024
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Login with Nostr NIP-46
3 participants