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

NIP-98 HTTP Auth #469

Merged
merged 6 commits into from
Jun 17, 2023
Merged

NIP-98 HTTP Auth #469

merged 6 commits into from
Jun 17, 2023

Conversation

v0l
Copy link
Member

@v0l v0l commented Apr 24, 2023

Adding NIP-98 for HTTP Auth

This is currently used by the Snort backend api for managing NIP-5 addresses and the subscriptions system.

Nosdav is also using this spec @melvincarvalho

https://github.com/v0l/nips/blob/nip98/98.md

@melvincarvalho

This comment was marked as spam.

@staab
Copy link
Member

staab commented Apr 24, 2023

How is the event delivered to the API? Is it published to a relay? I would be concerned about replay attacks, this doesn't seem secure. I don't understand why a direct request wouldn't work, is this for delegating authentication?

@v0l
Copy link
Member Author

v0l commented Apr 24, 2023

How is the event delivered to the API? Is it published to a relay?

Authorization header is a request header in HTTP

I would be concerned about replay attacks

Idempotency can be managed by the API and is not a concern for this spec, timestamp checks should be good enough in most cases.

@staab
Copy link
Member

staab commented Apr 24, 2023

Ah, ok, I missed that. LGTM I guess then. This is a criticism of zaps as well, but it's strange to use the standard event structure but then encode it and throw it in some other place like a query string or authentication header rather than just using a regular HMAC or signing the url you're requesting directly or whatnot. Putting it all in a valid event makes me worry that a confused client will publish this event, which contains sensitive information, to relays, which is possible and definitely not what you want to do.

@v0l
Copy link
Member Author

v0l commented Apr 24, 2023

The main reason for using nostr events is that we already have infrastructure to create signed payloads via NIP-7.

Suggesting something like regular HMAC auth would require access to private keys or changes to NIP-7, we have a JSON object which is similar to JWT here and i see an opportunity to re-use the nostr identity in external systems over regular HTTP requests.

I would even go so far as to say that this should be used instead of NIP-42! Its much cleaner to auth to relays this way.

@fiatjaf
Copy link
Member

fiatjaf commented Apr 26, 2023

Is this the spec we want for all "Nostr login" use cases?

@fiatjaf
Copy link
Member

fiatjaf commented Apr 26, 2023

@Dolu89 @lukechilds can you take a look at this?

@nohea
Copy link

nohea commented Apr 27, 2023

It's interesting reading this proposal, coming from some slight use of JWTs with shared secrets on servers.
JWTs typically have an auth provider authenticating a user and also validating "claims" (authorization roles).
The JWT is passed in the Authorization Bearer _ HTTP header along with the actual resource request.
The server can then validate identity and claims via symmetrical shared secret, or alternatively via jwk_url for asymmetric auth.

Clearly for Nostr we want to auth users asymmetrically. It is technically possible for Nostr clients to generate their own JWT for id. To validate permissions to a resource, an organization would have to run their own auth service, use Nostr to validate id, but add claims to the JWT.

Wrapping the auth+httprequest in a Nostr event adds extra overhead. Clearly it works, but figuring out auth via the existing JWT methods will allow for more services to use Nostr auth without customization.

98.md Outdated Show resolved Hide resolved
@blakejakopovic
Copy link
Contributor

Is this the spec we want for all "Nostr login" use cases?

@fiatjaf This suits my needs for web app login and server sessions. We can close #373

@arthurfranca
Copy link
Contributor

Nice NIP but I wonder why here there is no challenge need but NIP-42 required it? (NIP-42 became a lot less attractive after the challenge requirement)

On NIP-42 I suggested an authorization on connection with something like new WebSocket("wss://...?authorization=stringified_22242_event") (query string cause websocket doesn't play well with headers) but it got rejected because of #141 comment from @cameri .

@quentintaranpino
Copy link

I just implemented it in nostrcheck-api-ts it seems to me a very elegant solution to authenticate a request to a public API.

Thanks @v0l 🙂

@blakejakopovic
Copy link
Contributor

Have we considered including notes for common HTTP request failure responses? Effectively returning 401 Unauthorised or 402 Payment Required would be reasonable to use - maybe others could be considered too.

For either of those responses, we could perhaps define a WWW-Authenticate header with a Nostr specific auth-scheme to indicate this method of auth is supported.

As an example:

WWW-Authenticate: <auth-scheme>
WWW-Authenticate: Nostr-NIP-98

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate
https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes

@fiatjaf
Copy link
Member

fiatjaf commented May 8, 2023

I wonder the same, @arthurfranca.

@melvincarvalho

This comment was marked as spam.

@fiatjaf
Copy link
Member

fiatjaf commented May 10, 2023

@melvincarvalho do you understand why it is absolutely necessary for NIP-42 and not for this? I'm asking honestly because I forgot. For NIP-42 I think it wasn't just providing "extra security", it was absolutely necessary.

@blakejakopovic
Copy link
Contributor

@fiatjaf as an example of prior work that includes a challenge, the lightning lud-04 spec includes a k value. Effectively making it an interactive protocol.

I just watched the Fedimint schnorr signature tech talk, and they mentioned it's possible to do this non-interactively where you effectively use a zero-knowledge proof (using a provably random number as I understand it) instead of a challenge/interactive protocol. I'm waiting for the slides to be posted, as I don't have a full description of how it functions.

For protocol based NIPs, if may be useful to include a diagram to make it clear. Something like this perhaps.


ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication

@mikedilger
Copy link
Contributor

mikedilger commented May 11, 2023

The challenge is to avoid replay attacks. In NIP-42 an impostor can send an event created by someone else and the relay will believe the impostor is that someone else. In this NIP-98, an imposter can do the same thing (within the suggested 60 seconds).

Under normal usage impostors won't have access to the authenticating event. But in case they do through some other security weakness, it should only be usable once. This is for security-in-depth.

If an interactive protocol is intolerable, then we should be looking at zk-SNARK (although I'm not sure it provides the no-replay freshness guarantee we are looking for)

@Egge21M
Copy link
Contributor

Egge21M commented May 11, 2023

Under normal usage impostors won't have access to the authenticating event. But in case they do through some other security weakness, it should only be usable once. This is for security-in-depth.

I think I disagree here. Most authentication services have this problem. If you leak your JWT your have a problem until it expires. 60 seconds is much shorter than most token expiries.

And if most people consider this a problem, servers could store the id of the event, similar to how oath keeps track of used tokens, and forbid reuse. It would mean that the client will have to rebuild events for every subsequent request though, which sounds undesirable

@fiatjaf
Copy link
Member

fiatjaf commented May 11, 2023

The challenge is to avoid replay attacks. In NIP-42 an impostor can send an event created by someone else and the relay will believe the impostor is that someone else.

How? If the user Bob sends his event directly to the relay Ronald, and the event has a commitment to the relay URL, what can Ronald do with that? It can't use that to auth to other relays.

@blakejakopovic
Copy link
Contributor

blakejakopovic commented May 11, 2023 via email

@mikedilger
Copy link
Contributor

How? If the user Bob sends his event directly to the relay Ronald, and the event has a commitment to the relay URL, what can Ronald do with that? It can't use that to auth to other relays.

Ronald being the relay can't do anything interesting. But any 3rd party getting ahold of it could trick Ronald.

@fiatjaf
Copy link
Member

fiatjaf commented May 12, 2023

Should we change NIP-42 to remove the challenge? @mikedilger That would make it much simpler.

@arthurfranca
Copy link
Contributor

  1. The method already includes several robust security measures, such as checking the kind, created_at, url, and method [...] help ensure the authenticity and integrity of the request.

Same as NIP-42.

  1. The use of a timestamp within the created_at tag [...] within a reasonable time window

Same as NIP-42. (It is suggested 10 min but it could also be 60 seconds)

  1. The method is primarily designed for Nostr-specific HTTP services, which may have lower risk profiles [...]

Relays (NIP-42 use-case) have even lower risk cause not only the auth event is signed, but all other payloads are signed events that a malicious client wouldn't be able to create without a nsec

  1. Adding a challenge-response mechanism [...] negatively impact the user experience [...]

Totally agree! Also for NIP-42.


NIP-98 main difference is that on every request, a new auth event is created.
While on NIP-42 it is created just once, cause it is a persistent websocket connection.

To be clear, I'm in favor of not using a challenge for both NIPs. NIP-42 should be as easy as new WebSocket("wss://...?authorization=stringified_22242_event") -> close the socket if authentication fail -> or register pubkey to ws client instance

@mikedilger
Copy link
Contributor

mikedilger commented May 13, 2023

Should we change NIP-42 to remove the challenge? @mikedilger That would make it much simpler.

No. I'm not following your gist here. NIP-42 and NIP-98 need challenges.

The challenge in NIP-42 is needed. The attack is like this:

  1. Bob connects to Ronald.
  2. Ronald asks Bob for AUTH not requiring any challenge
  3. Bob authenticates. But Eve was evesdropping and copied the auth event from bob.
  4. Eve connects to Ronald.
  5. Ronald asks Eve for AUTH not requiring any challenge
  6. Eve presents the event that Bob generated. Ronald now thinks Eve is Bob.

Even with AUTH (but without wss) there is a session-hijacking attack where Eve can hijack the session and Ronald thinks he is continuing to talk with Bob, but it is now Eve. So NIP-42 should require WSS, not just WS.

@melvincarvalho

This comment was marked as spam.

@mikedilger
Copy link
Contributor

I agree that WSS prevents session hijacking.

I'm no fan of trusting in "short-lived". Eve can attack microseconds after. Anything the legitimate user could do, the attacker could do.

A nonce could work, but that means the server has to remember every nonce. If you think that is easier than the server issuing a challenge, then ok. But they have to be tracked forever, kind of like an ever growing blockchain. I should presume bitcoiners are fine with that craziness.

I hear you on idempotent operations. Replaying such is not a risk if they truly are fully idempotent.

IP binding isn't secure. Kevin Mitnick attacked Tsusomu Shimumora's lab using half-open TCP connections that spoofed the IP address... way back when I was learning computer security in 1994 or so.

@Egge21M
Copy link
Contributor

Egge21M commented May 13, 2023

A nonce could work, but that means the server has to remember every nonce. If you think that is easier than the server issuing a challenge, then ok. But they have to be tracked forever, kind of like an ever growing blockchain. I should presume bitcoiners are fine with that craziness.

It would only have to be kept until the timestamp goes out of the allowed timeframe

@arthurfranca
Copy link
Contributor

arthurfranca commented May 13, 2023

  1. Timestamping and Nonce: Adding a timestamp or a nonce (a number used once) in the authentication header could prevent replay attacks.

A nonce could work, but that means the server has to remember every nonce.

The nonce is the event id. Just need to keep it around for the allowed time window (60 seconds).

IP binding isn't secure [...] Kevin Mitnick [...] spoofed the IP address [...]

For using the same ip on an http connection I only imagine it possible if using the same local network (like same lan house) or hacking the user ISP, but I'm not Kevin Mitnick xD. Atleast it is an extra security layer.


Regarding a NIP-42 version without challenge (NIP-98 doesn't need it as it is not a persistent session - just keep track of event id), the relay could do the AUTH command dance (the current NIP-42 with challenge) when something important is at stake.
Like when you are already logged in but have to send the current password again when wanting to change the password. For nostr it would be after user tries to do NIP-109: Pubkey Deletion, for instance.
<- it doesnt even make sense cause attacker would need the nsec to create the event. so can't think of an use-case for challenge

So NIP-42 is overkill and has poor experience for the current use-cases (rate-limiting and [paid] relay login). For session creation, an authentication without challenge is good enough.

4. The `method` tag MUST be the same HTTP method used for the requested resource.

When the request contains a body (as in POST/PUT/PATCH methods) clients SHOULD include a SHA256 hash of the request body in a `payload` tag as hex (`["payload", "<sha256-hex>"]`), servers MAY check this to validate that the requested payload is authorized.

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
When calling an [unsafe](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP) http method (all but GET, HEAD and OPTIONS), server SHOULD keep track of the event id for the above time window duration to prevent reuse.

```
Authorization: Nostr eyJpZCI6ImZlOTY0ZTc1ODkwMzM2MGYyOGQ4NDI0ZDA5MmRhODQ5NGVkMjA3Y2JhODIzMTEwYmUzYTU3ZGZlNGI1Nzg3MzQiLCJwdWJrZXkiOiI2M2ZlNjMxOGRjNTg1ODNjZmUxNjgxMGY4NmRkMDllMThiZmQ3NmFhYmMyNGEwMDgxY2UyODU2ZjMzMDUwNGVkIiwiY29udGVudCI6IiIsImtpbmQiOjI3MjM1LCJjcmVhdGVkX2F0IjoxNjgyMzI3ODUyLCJ0YWdzIjpbWyJ1cmwiLCJodHRwczovL2FwaS5zbm9ydC5zb2NpYWwvYXBpL3YxL241c3AvbGlzdCJdLFsibWV0aG9kIiwiR0VUIl1dLCJzaWciOiI1ZWQ5ZDhlYzk1OGJjODU0Zjk5N2JkYzI0YWMzMzdkMDA1YWYzNzIzMjQ3NDdlZmU0YTAwZTI0ZjRjMzA0MzdmZjRkZDgzMDg2ODRiZWQ0NjdkOWQ2YmUzZTVhNTE3YmI0M2IxNzMyY2M3ZDMzOTQ5YTNhYWY4NjcwNWMyMjE4NCJ9
```

Copy link
Contributor

@mikedilger mikedilger May 18, 2023

Choose a reason for hiding this comment

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

Replay Attacks

While this NIP assures that the kind 27235 event guarantees that its author authorizes using the stated HTTP method at the stated HTTP URL within 60 seconds of the created_at timestamp, it does not guarantee to the relay server that the HTTP client supplying this event is the author of that event. That is to say, replay attacks within 60 seconds are possible if the event leaks.

Relays Servers should consider whether any data they return to an authenticated HTTP connection is confidential. If so, it is strongly recommended that they only accept authentication and reply with said confidential information if the connection is secured with TLS (e.g. via https).

Copy link
Member

Choose a reason for hiding this comment

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

There are no relays involved in this flow. It only signs an event to take advantage of NIP-07, but the result is sent over an HTTP header.

Copy link
Member

Choose a reason for hiding this comment

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

A 60 second expiring token scoped to a specific URL is actually significantly better security than OAuth tokens, even with refresh tokens.

This is perhaps the most secure HTTP Auth method the world has ever seen.

@melvincarvalho

This comment was marked as spam.

@alexgleason
Copy link
Member

This is interesting. I needed to create server-side events on behalf of users, and I solved a similar problem differently:

  1. Client opens a websocket connection to the server.
  2. Client uses a pubkey in the Authorization header when making an HTTP request.
  3. The server does not immediately serve a response. It stalls.
  4. Server pushes an event to sign to the client over the websocket.
  5. Client signs the event and pushes it back to the server.
  6. The server finally returns the HTTP response.

I have a very specific use-case (hacking Nostr support into a legacy system), but I thought you might think it's interesting.

"created_at": 1682327852,
"tags": [
[
"u",
Copy link
Member

Choose a reason for hiding this comment

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

Why "u" instead of "url"? I think single letter tags are meant to be indexed by relays with filters such as { "#u": [...] }

Since the intention is not to index, I don't think it should be a single letter.

Copy link
Member

Choose a reason for hiding this comment

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

I second this. I believe r would also be valid here.

98.md Outdated
Servers MUST perform the following checks in order to validate the event:
1. The `kind` MUST be `27235`.
2. The `created_at` MUST be within a reasonable time window (suggestion 60 seconds).
3. The `url` tag MUST be exactly the same as the absolute request URL (including query parameters).
Copy link
Member

Choose a reason for hiding this comment

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

Here you've called it "url" even though your example says "u", so I'm guessing you were trying to decide.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea i prefer url @melvincarvalho prefers u

Copy link
Member

Choose a reason for hiding this comment

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

@melvincarvalho is wrong. 😃 That's my only remaining feedback, otherwise LGTM.

This comment was marked as spam.

Copy link
Member

Choose a reason for hiding this comment

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

NIP-94 uses the "url" tag already. Having "u" and "url" coexist isn't ideal. https://github.com/nostr-protocol/nips/blob/master/94.md

Choose a reason for hiding this comment

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

I am using it on two endpoints, but I would have no problem changing it. I'll adapt to what you all decide 🙂

This comment was marked as spam.

Copy link
Member

Choose a reason for hiding this comment

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

I support this NIP. 👍

Copy link
Member

Choose a reason for hiding this comment

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

Hate to bring this back up, but NIP 12 defines r for url references. url is used in NIP 94. Adding u violates rule #4 for drafting NIPs. I think your implementation should lose vs the NIPs repo @melvincarvalho.

Copy link
Member

@fiatjaf fiatjaf May 23, 2023

Choose a reason for hiding this comment

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

@staab but the same tag can have different meanings in two different contexts.

@alexgleason
Copy link
Member

Another possible flow uses NIP-07's encrypt functionality. The server has a pubkey, which at some point the client obtains and stores. The Authorization header contains the user's pubkey in plaintext, along with a signed message to the server's pubkey.

I've been thinking about this problem a lot.

@blakejakopovic
Copy link
Contributor

I've drafted a protocol flow NIP for 401 Unauthorized and 402 Payment Required. I think it should become part of this NIP - as defining a protocol without clear lifecycle and having common uses defined, we will result in weird behaviours between implementations. https://gist.github.com/blakejakopovic/fe384b8fd97231ece267bf264eb466ef - feedback welcome.

I think the best way forward is to define optional additional security layers that can be used. One could be tracking event_id and limiting to single use. Another would be using certificate pinning inside your application, to prevent ssl stripping / MIM. Ultimately we need a non-interactive auth protocol, and optionally an interactive one if people so desire. You can always add additional security like in #469 (comment)

For an ephemeral event, the tag shouldn't matter as it's not going to be indexed anyway (u or url) - what we should do however is try define a standardised tag for urls going forward. However, really it's contextual anyway.

@melvincarvalho

This comment was marked as spam.

@blakejakopovic
Copy link
Contributor

blakejakopovic commented May 21, 2023 via email

@melvincarvalho

This comment was marked as spam.

Copy link
Member

@staab staab left a comment

Choose a reason for hiding this comment

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

ACK, but I'd prefer to not see u introduced. We only have 26 letters.

"created_at": 1682327852,
"tags": [
[
"u",
Copy link
Member

Choose a reason for hiding this comment

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

I second this. I believe r would also be valid here.

98.md Outdated
Servers MUST perform the following checks in order to validate the event:
1. The `kind` MUST be `27235`.
2. The `created_at` MUST be within a reasonable time window (suggestion 60 seconds).
3. The `url` tag MUST be exactly the same as the absolute request URL (including query parameters).
Copy link
Member

Choose a reason for hiding this comment

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

Hate to bring this back up, but NIP 12 defines r for url references. url is used in NIP 94. Adding u violates rule #4 for drafting NIPs. I think your implementation should lose vs the NIPs repo @melvincarvalho.


When the request contains a body (as in POST/PUT/PATCH methods) clients SHOULD include a SHA256 hash of the request body in a `payload` tag as hex (`["payload", "<sha256-hex>"]`), servers MAY check this to validate that the requested payload is authorized.

If one of the checks was to fail the server SHOULD respond with a 401 Unauthorized response code.
Copy link
Member

Choose a reason for hiding this comment

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

I believe this should be 403 if the Authorization header was actually sent.

This comment was marked as spam.

This comment was marked as spam.

Copy link
Member

Choose a reason for hiding this comment

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

I don't know what the constraints related to your project are, but if this is the correct usage of 401 that's fine, I'm not 100% clear on the nuances.

@rabble
Copy link
Collaborator

rabble commented Jun 12, 2023

Doesn't https://ucan.xyz/ basically do serverless JWT for authorization like we're looking at for this?

@v0l v0l merged commit c8c2ab6 into nostr-protocol:master Jun 17, 2023
@v0l v0l deleted the nip98 branch June 17, 2023 18:21
@alexgleason
Copy link
Member

I implemented NIP-98 auth in Ditto: https://gitlab.com/soapbox-pub/ditto/-/blob/develop/src/middleware/auth98.ts

So you can call Mastodon API endpoints like GET https://ditto.pub/api/v1/accounts/verify_credentials with NIP-98 auth and it'll recognize you and return your Nostr account in Mastodon API format.

One idea which occurred to me... is that NIP-98 + proof of work is an epic combination for deterring spam and DDOS attacks. Nothing stops me from adding a NIP-13 nonce tag to auth events and then validating POW on the server.

You could use this during registration especially.

However, POW with NIP-07 is a bit awkward.

@Dolu89
Copy link

Dolu89 commented Aug 4, 2023

I implemented NIP-98 auth in a web component https://github.com/Dolu89/nostr-one
You can put this button on your website and it will call your login API. It's a web component. It's usable with any front-end framework and also on pure HTML/JS pages.

Then you can use NIP-98 from nostr-tools that I implemented too, in your backend to validate the event. https://github.com/nbd-wtf/nostr-tools/blob/3368e8c00e728ff9b4981d58f8f7e546caa1d0f1/nip98.ts#L63C23-L63C36
Documentation is probably not perfect. Don't hesitate if you don't understand something

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.