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
Resolve #6068 digestauth challenge redux #6138
Resolve #6068 digestauth challenge redux #6138
Conversation
Took a quick pass through—excellent work, this is looking very nice 👍 |
A couple other notes:
|
9e7985c
to
c666a7f
Compare
Performance analysis, I ran this script against efadfc9 (origin/series/0.22) and c666a7f (this branch): -BasicAuthentication should respond to a request with correct credentials: 15ms
+BasicAuthentication should respond to a request with correct credentials: 19ms
-BasicAuthentication should respond to a request with unknown username with 401: 4ms
+BasicAuthentication should respond to a request with unknown username with 401: 6ms
BasicAuthentication should respond to a request with wrong password with 401: 1ms
-BasicAuthentication should respond to a request without authentication with 401: 20ms
+BasicAuthentication should respond to a request without authentication with 401: 24ms
-DigestAuthentication should avoid many concurrent replay attacks: 16ms
+DigestAuthentication should avoid many concurrent replay attacks: 31ms
-DigestAuthentication should respond to a request with correct credentials: 29ms
+DigestAuthentication should respond to a request with correct credentials: 40ms
-DigestAuthentication should respond to a request without authentication with 401: 8ms
+DigestAuthentication should respond to a request without authentication with 401: 49ms
-DigestAuthentication should respond to invalid requests with 401: 17ms
+DigestAuthentication should respond to invalid requests with 401: 7ms
-DigestAuthentication should respond to many concurrent requests while cleaning up nonces: 85ms
+DigestAuthentication should respond to many concurrent requests while cleaning up nonces: 140ms (the script just runs I'm somewhat confused by the directive of "don't worry about performance", but I'm heartened by my largely amateurish attempt not having too negative of a performance impact. I'm available and interested in idioms or suggestions to improve performance, if necessary. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You've been in good hands with others here, so I only skimmed it. Looking good, though. I imagine we'll do another round of releases this upcoming week, and I'd love to get this in.
On the subject of "don't worry about performance", I haven't followed the conversation closely enough to see where it came up. I'd say we do strive for performance, but correctness comes first, and we don't generally trade legibility or purity for performance without a profiled hotspot. Benchmarking and optimizing is fun, but when we're all mostly writing services that listen on a socket and then make a remote database query and then enrich that with a client call to another web service, an extra flatMap here or there is a drop in the ocean.
server/src/main/scala/org/http4s/server/middleware/authentication/DigestAuth.scala
Outdated
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/NonceF.scala
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ross beat my review by a few minutes, so apologies for the duplicated comments 😆
I made the comment about performance in #6138 (comment) (emphasis added):
Don't worry about performance testing
What I meant was, we wouldn't block your PR due to lack of benchmarks :) for sure, we don't want you to tank performance! Also, meaningful benchmarking is not easy: you have to account for JVM warm-up and JIT-optimizations, etc.
server/src/main/scala/org/http4s/server/middleware/authentication/NonceF.scala
Outdated
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/NonceKeeperF.scala
Outdated
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/NonceKeeperF.scala
Outdated
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/DigestAuth.scala
Outdated
Show resolved
Hide resolved
Thanks for the reviews, I've got my work cut out for me! In taking the next step in verifying my work here, I ran across In an attempt to resolve this, I've pushed up I'm offering this here as it's convenient to only break bincompat once, since we're already changing everything up, but this definitely falls under scope creep. |
That makes more sense 😅 I figured enough runs of the auth suite would be sufficient ballpark, which is why I included them, since this is (from my vantage point) a significant enough change to warrant them, though Ross' point about an extra I've updated the PR with feedback, and will update again shortly once I've finished this test for the |
server/src/main/scala/org/http4s/server/middleware/authentication/DigestAuth.scala
Outdated
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/DigestAuth.scala
Outdated
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/DigestAuth.scala
Outdated
Show resolved
Hide resolved
Resolves http4s#6068 NonceKeeper is package-private, so there's no way to construct one, meaning there's no way to call `challenge`. Mirror the same parameters from `apply`, and maintain the previous `challenge` method for bincompat.
…ion/DigestAuth.scala Co-authored-by: Arman Bilge <armanbilge@gmail.com>
In order to achieve the goal of F.delay(new NonceKeeper) but maintaining the original intent of `apply`, just revert everything in this PR, make the original (unusable) `challenge` `private`, and create a new public `challenge` with the necessary parameters, intended for human use, without impacting the existing flow.
…ion/DigestAuth.scala Co-authored-by: Arman Bilge <armanbilge@gmail.com>
Internals unchanged, in preparation for a pure rewrite of DigestAuth. Includes a private bincompat method
Internals unchanged, in preparation for a pure rewrite of DigestAuth. Includes a private bincompat method
ee3a040
to
3122229
Compare
160be26
to
aa6ab18
Compare
Alright, this is good to go, barring one final thought: Without knowing how to precompute the I added a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is shaping up well. I gave it a more thorough review. Lots of comments, but hopefully none of them stir the pot too much.
) | ||
type AuthenticationStore[F[_], A] = String => F[Option[(A, String)]] | ||
|
||
sealed trait AuthStore[F[_], A] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be sealed? I see we match on them in checkAuthParams
. Would it be better if those cases were a method on this, to permit more pluggable implementations? Or are plaintext and MD5 the two that are specified?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on how I understand the spec, there are only two sensible implementations:
Plain-text passwords:
md5($un:$realm:$pw):$nonce:$nc:$cnonce:$qop:md5($method:$uri)
Pre-hashed passwords:
md5($un:$realm:$pw) -> ha1 in the db
|
retrieve from db
v
$ha1:$nonce:$nc:$cnonce:$qop:md5($method:$uri)
The way I see it, by pushing more into subclasses of AuthStore
, we'd need to expose all these parameters for dubious benefit. ha1
is the only secret that needs to be exposed to user code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewing the spec again for another comment, this is actually called out explicitly by the spec:
Note that the HTTP server does not actually need to know the user's
cleartext password. As long as H(A1) is available to the server, the
validity of an Authorization header may be verified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An optional header allows the server to specify the algorithm used to
create the checksum or digest. By default the MD5 algorithm is used
and that is the only algorithm described in this document.
It looks like it can be md5
, md5-sess
, or extensions that probably nobody understands. I think it would be unpleasant to register and understand more for little gain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The need to add support for this stuff may motivate new contributors 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Additionally, adding the required helper methods, similar to Md5HashedAuthStore.precomputeHash
)
server/src/main/scala/org/http4s/server/middleware/authentication/DigestAuth.scala
Outdated
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/DigestUtil.scala
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/Nonce.scala
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/NonceKeeperF.scala
Outdated
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/NonceKeeperF.scala
Outdated
Show resolved
Hide resolved
server/src/main/scala/org/http4s/server/middleware/authentication/Nonce.scala
Show resolved
Hide resolved
private def getRandomData[F[_]](bits: Int)(implicit F: Sync[F]): F[String] = | ||
F.delay(new BigInteger(bits, random).toString(16)) | ||
|
||
def gen[F[_]: Sync](bits: Int): F[NonceF[F]] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to need a Blocker
and a ContextShift
in the long run. This can merge before #6165, but it would be best to get that done before we have a release and need to do a MiMa exemption.
The nice thing is, in Cats-Effect 3, this becomes more transparent in the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sort of interesting -- NonceKeeperF[F].newNonce()
already requires a permit for the singleton semaphore, so I think acquiring an instance of Blocker
at that time makes sense. I can keep moving it further up if desired, I really don't have a good intuition around where that instance needs to come from.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling new BigInteger(bits, random)
will block on some platforms as it gathers entropy. We want to shift that call specifically to another thread, so we're not blocking the compute pool on the IO to /dev/random
. There's also a mutex to worry about on SecureRandom
, but that should be fine since you're putting this instance behind a semaphore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does Blocking[F].use
actually acquire a new thread/threadpool? Should that parameter be bubbled all the way out to the caller's code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cats-Effect 2's Blocker
does require a thread pool. One Blocker
should suffice for most apps, but any app that does blocking needs one. It's part of the runtime in Cats-Effect 3, and available from the Sync
constraint, so this will melt away in 0.23.
Engineering tradeoff: if we don't thread it through, this could suck real bad for a small number of users. If we do thread it through, we're going to all that work for, like, Windows users, and other people who do Weird Shit. http4s-0.22 is basically EOL. Personally, I'm only fixing bugs on it. And this blocking is not new, and nobody has complained. So if you'd prefer, we could put a fat disclaimer on it and know it will be better in CE3. But this is "safer".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I overlooked the Blocker.use
. Yeah, that creates a new cached thread pool on every invocation. What we want is one cached blocker for the entire app that gets threaded through. This is where the API is going to get uglier in CE2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swapped it out to a single allocated Blocker
on init, backed by the global
threadpool, as this is going away anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't want to run blocking operations on the global threadpool. That pool has special magic if you additionally use a special standard library blocking
incantation, which we aren't. Blocking should typically be done on a dedicated, cached thread pool. I think it would be better to remove it entirely if we're not going to thread it through.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright -- threaded it through, as this is a new integration, and the exposure of it will better match the functionality in CE3 for users through the upgrade process.
In preparation for http4s#6165, thread through the required instances so we can just swap out `SecureRandom` when that stuff becomes available.
74638e5
to
f84f759
Compare
Seems like an unrelated failure, I spent a few minutes trying to understand what's going on but I'm completely out of my depth. While the failure message is
I actually found that - override val scalaCheckInitialSeed = "ul4X_OcjscjNgHDzfcislTYeeRBho4GD7b-pOuxHoPJ=";
+ override def scalaCheckInitialSeed = "ul4X_OcjscjNgHDzfcislTYeeRBho4GD7b-pOuxHoPJ="; |
Yeah, that test flake looks like another #4262. Don't worry about that here. |
We're already behind a Semaphore, and this Blocker instance is just to satisfy http4s#6165. Use the Blocker constructor available on both JVM and JS
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your perseverance on this. I think it's in a good place.
@armanbilge, are you good with this as is? You had opinions earlier. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the scope of this PR got away from me 😆 really fantastic work by @blast-hardcheese. Both CE2 and the RFC are a bit out of my comfort zone.
I glanced through and have one question, but not a blocker.
private[authentication] class Nonce(val created: Date, var nc: Int, val data: String) | ||
|
||
private[authentication] object Nonce { | ||
val random = new SecureRandom() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this becomes an effect, it can no longer be a global. So where will it be initialized?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The usage is deprecated. I don't think we need to make it an effect. Though we could maybe add a deprecation notice to this type, to assure it's only called through deprecated code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, is this not going to use the stuff from #6165?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was planning on welding it together after this. @blast-hardcheese has suffered enough. But I could merge it into this just to see what it looks like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, sounds good to me 🚀
Resolves #6068
Attempting to address PR feedback in #6077
Refactoring
NonceKeeper
synchronized
callsvar
toRef
F[_]
AuthStore
trait with a more secure scheme that doesn't require the user storing the user's passwords in plaintextSome remaining tasks:
F[_]
further: remove remainingvar
,while
/tailrec
/return
Opening this draft early on to communicate intent and direction, as well to get feedback early