jaredhanson / passport

Simple, unobtrusive authentication for Node.js.
https://www.passportjs.org?utm_source=github&utm_medium=referral&utm_campaign=passport&utm_content=about
MIT License
22.65k stars 1.23k forks source link

Challenge / Response Support #488

Open apowers313 opened 8 years ago

apowers313 commented 8 years ago

I'm writing a passport strategy for W3C WebAuthn, which does away with passwords in favor of using a cryptographic challenge / response.

The basic algorithm is that the server is expected to send a random string to the client / browser and the browser will use a user's previously registered private key sign the random string and send the signature back to the server -- thus authenticating the user by being in possession of the user's private key.

The problem is that passport assumes a single message from the client to the server for authentication (containing a password), not a two-step challenge / response where a client has to request information from a server before being able to authenticate. There needs to be a separate API call for getting a challenge (most likely in a JSON format) without effecting the state of passport.

Options for fixing this seem to be:

  1. Require a user manually create a new route for getting challenges, write all the code for managing challenges, and then just use passport.authenticate() for the response. Downside is that it requires every user to re-write the same code for managing challenges and breaks the beautiful simplicity of passport.
  2. Overload the passport.authorize() method for getting challenges. Probably confusing to passport users because it is semantically incorrect, but maybe it would work?
  3. Create a new passport.getChallenge() method for getting challenges. Assumes that there are going to be other frameworks that require challenge / response (which doesn't seem unlikely) and adds a new method the core framework, which I assume is something that shouldn't be taken lightly.

Any thoughts / preferences of which way to go?

apowers313 commented 8 years ago

Thinking about this a bit more, there seems to be a 4th option:

  1. Add a new strategy augmentation in the middleware alongside .success(), .fail(), .redirect(), etc., maybe called .info(), that returns the arguments to the client without modification and doesn't indicate either success or failure. This might actually be the most reasonable approach.
jaredhanson commented 8 years ago

I've read over the W3C spec, and from my understanding the authentication is by virtue of verifying a public/private key pair. The examples the W3C gives are fairly unclear as to what the challenge is used for. They go so far as to just embed a challenge into the JavaScript executing on the client. Is there any other guidance as to how the challenge is used?

A couple questions:

Require a user manually create a new route for getting challenges, write all the code for managing challenges, and then just use passport.authenticate() for the response. Downside is that it requires every user to re-write the same code for managing challenges and breaks the beautiful simplicity of passport.

Why isn't this option valid? If the challenge is unrelated to actual authentication, it makes sense to scope that functionality out of this module. In my opinion, that preserves the simplicity of passport.

The W3C spec has the following text:

This attestation statement is delivered to the WebAuthn Relying Party by the WebAuthn Relying Party’s script running on the client, using methods outside the scope of this specification. It contains all the information that the WebAuthn Relying Party’s server requires to validate the statement, as well as to decode and validate the bindings of both the client and authenticator data.

The spec seems to focus only on the client-side platform, and leaves authentication by the server undefined. Such behavior is what Passport should be focusing on. Are there any recommendations as to how to deliver WebAuthn attestations to a server for authentication?

apowers313 commented 8 years ago

The examples the W3C gives are fairly unclear as to what the challenge is used for.

Getting into the weeds of the spec, makeCredential = registration; and getAssertion = authentication. Focusing on getAssertion, since that's what matters from a passport perspective, Section 4.1.2 says that the challenge gets passed down from the browser to an authenticator\ through athenticatorGetAssertion and in Section 4.2.3 the authenticator signs a hash of clientData which includes the challenge. A signature of the hash proves that 1) an authenticator is in possession of a private key; and 2) that this is an approval of this specific instance (since the authenticator is signing a new random challenge each time).

Keep in mind that the first Review Draft of the W3C spec is just coming out on Tuesday, May 31st. I'm sure there is lots of clarification that will happen between now and then final version of the spec. The blog post from Microsoft might provide some clarity as well.

\ authenticators are typically hardware like a YubiKey or an Android phone. For a first version of a software-only authenticator, check out the webauthn-soft-authn that I've been working on (although it's not nearly complete).

Why isn't this option valid? If the challenge is unrelated to actual authentication [...]

Well, the challenge is related to authentication. In theory, you could have different routes for getting a challenge and returning the results of getAttestation and then the strategy could install different middleware for each. In practice it would be good to have a single route for WebAuthn authentication, where the first call to that route would return a challenge and the second call to that route would return a signed challenge.

That could be managed through something like a custom callback, but that would require the user to write code like this every time:

app.get('/auth/webauthn', function(req, res, next) {
  passport.authenticate('webauthn', function(err, user, info) {
  // check whether this is a request for a challenge or a signed challenge
  // if it's a request for a challenge, generate the challenge and return res.json()
  // if it's a signed challenge lookup the public key for the user and verify the signature
  })(req, res, next);
});

True, every user could cut and paste that same route code in there every time... but that seems to defeat the purpose of using passport. Personally, I would prefer something that looks like:

app.get('/auth/webauthn', passport.authenticate('webauthn'))

The spec seems to focus only on the client-side platform [...]

Yes, and that's by design since W3C is only concerned with browsers. The original browser specs were drafted by FIDO and I suspect that FIDO will provide some specs / guidelines as to what servers are supposed to do. Eventually, a WebAuthn server might look something like this.

apowers313 commented 8 years ago

Btw, here is an example passport WebAuthn strategy and demo server using Jingo. The relevant parts of the Jingo demo are routes/auth.js and public/js/rp-app.js.

Apologies in advance for the rough state of the code.

markg85 commented 5 years ago

Hi,

Is there an update on this?

I'm asking because:

  1. WebAuthn really gained some interesting traction in recent months! With lots of the android devices now being capable and it working really sweet in Chrome on android!
  2. The Krypton authenticator (https://krypt.co/) makes this work in a very cool way where your phone is used to unlock the site you request on your desktop or wherever you've paired the authenticator.
  3. Chrome and Firefox support this!

I've seen the WebAuthn strategy plugin https://github.com/apowers313/passport-webauthn from @apowers313 but as it's 3 years old + this thread i'm a bit hesitant to try it out. Also, the strategy isn't in the list of passportjs packages http://www.passportjs.org/packages/ which makes me even more hesitant.

I'm very eager to start using this on some pet project sites, but currently using WebAuthn as a method of login/registration is quite a lot of code. I can write it all, but i definitely prefer to use a library that takes that away for me.

And here i seem to have both relevant parties involved, hence the question :)

apowers313 commented 5 years ago

@markg85 I never published it because it requires PR #489 to be merged. I agree that it seems like the time is right to come back to this.

jaredhanson commented 2 years ago

OK, I'm digging this back up :)

I've implemented a WebAuthn strategy: passport-fido2-webauthn. It works without changing or adding to Passport internals. Here's my thoughts on the approach.

Of the options outlined here, I took option 1:

Require a user manually create a new route for getting challenges, write all the code for managing challenges, and then just use passport.authenticate() for the response. Downside is that it requires every user to re-write the same code for managing challenges and breaks the beautiful simplicity of passport.

As can be seen in the example app, this ends up looking something like this:

router.post('/login/public-key/challenge', function(req, res, next) {
  store.challenge(req, function(err, challenge) {
    if (err) { return next(err); }
    res.json({ challenge: base64url.encode(challenge) });
  });
});

Where store is a SessionChallengeStore exported by the strategy package. The store generates the challenge in a way that the strategy knows how to verify, and the application is expected to serialize it according to its own needs when communicating with the client-side JS.

Authenticating the response (including the challenge), happens normally:

router.post('/login/public-key', passport.authenticate('webauthn'), ...);

As noted, the downside here is that applications have to individually create a /challenge route, and its not just "drop-in" reusable. That said, things are designed in such a way to minimize the amount of effort.

One way to avoid that, would be to introduce a strategy.challenge() function (similar to strategy.raw() as proposed in #489). My hesitation with that is as follows:

Right now the strategy action functions (success(), fail(), redirect()) all operate on "native" HTTP primitives or pass control back the the application (error(), pass()). By "native" I mean things that are defined as standards directly in the HTTP protocol. This primarily involves status codes (401, 3xx, etc) and headers typically associated with HTTP authentication schemes such as Digest, which define specifics of how to encode challenges (including nonces).

With WebAuthn, however, the protocol by which challenges are conveyed isn't specified, but left up to the application. I'm wary of bleeding what are application-level concerns into a middleware framework. That being said, I am seeing other authentication protocols that need application-level nonces. Various Web3-related projects need similar capabilities, such as Sign in with Ethereum.

Given that this is an emerging pattern, I wonder if it would be an opportune time to define a "cryptographic challenge" protocol intended to be reused by applications. This could serialize things as JSON for browser-based consumption, or even just set a nonce in a response HTTP header. @apowers313: I'm curious if you've heard similar requests or know of any ongoing work to define such a protocol or even just conventions that have emerged among development frameworks? I'd be happy to contribute here. If/when such a protocol was formalized, I'd be more inclined to support it directly in Passport, so that it can be reused across strategies that need such cryptographic primitives.

I'd love to get feedback on this topic now, since there's a lot of momentum with WebAuthn and I want to make sure Passport has first-class support. Thanks!