-
-
Notifications
You must be signed in to change notification settings - Fork 723
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
working auth filter rejection with authentication header and realm name #751
base: master
Are you sure you want to change the base?
Conversation
src/filters/auth.rs
Outdated
pub fn basic( | ||
realm: &'static str, | ||
) -> impl Filter<Extract = (Authorization<Basic>,), Error = Rejection> + Copy { | ||
header::header2().or_else(move |_| future::err(crate::reject::unauthorized(realm))) |
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 feel like it would be nicer to have the callback argument be Basic
instead of Authorization
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 don't follow what you mean here, can you please elaborate?
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.
Something like the following
pub fn basic(
realm: &'static str,
) -> impl Filter<Extract = (Basic,), Error = Rejection> + Copy {
header::header2().map(|auth: Authorization<Basic>| auth.0).or_else(move |_| future::err(crate::reject::unauthorized(realm)))
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, okay I'll look into that!
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 see how that helps now, much cleaner code afterwards :) should be okay now too
examples/basic_auth.rs
Outdated
#[tokio::main] | ||
async fn main() { | ||
let routes = warp::auth::basic("Realm name").map(|auth_header: Authorization<Basic>| { | ||
// TODO: some password lookups done in here |
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.
It might be nice for the example to actually include a method for password lookups
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.
well anything serious needs to pass in some form of database, file or at least a hashmap dragging in dependencies. easiest form is a silly if name == password
check, but not sure that makes sense.
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.
If the goal of an example is to illustrate how to use the filter, it would be good to provide something closer to a real world example instead of a todo comment. Using the dir
example as a template, the project provides an actual path that is populated with files. When that example is run, it actually serves content, which helps to illustrate how to use that filter.
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'll add a mutex guarded Hashmap then, everything else needs 3rd party libs. Okay like that?
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 may want to make it a Arc<tokio::sync::Mutex<HashMap<String, String>>>
since almost any implementation is going to need to do some kind of async
work. The tokio
dependency will already be there from using tokio::main
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 realised a lock-guard is not needed, since this is read-only. example should be fine now
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.
My suggestion of using the tokio mutex was mostly to have some async work to perform in the handler, the new example is quite a bit more illustrative though
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.
Sure I understand, but anything that needs async work (e.g. update of password db while server is running) is a way too big example. And showing use of a Mutex when lock-free is actually good enough is not a good example either ;)
however my example is broken, as it does not return a 401 on auth-error. but this is what my comment in the parent thread discusses, how to do this best 🤔
src/reject.rs
Outdated
let realm = e.downcast_ref::<String>().expect("realm must be string"); | ||
res.headers_mut().insert( | ||
"www-authenticate", | ||
HeaderValue::from_str(format!("Basic realm=\"{}\"", realm).as_str()).unwrap(), |
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 unauthorized response should be able to handle both Basic
and Bearer
replies at least. The full list of valid values here is 10 entries long, so some flexibility on the scheme
would be nice.
Also according to MDN the realm
could be omitted, though I am not sure it is wise to allow that
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.
Given that what I've coded so far feels kinda hacky, supporting multiple schemes calls for an even cleaner implementation. Meaning for example coming up with a new rejection type as mentioned in my description of the PR? I'll try to add Bearer
but I am not sure this is really readable code after without fundamental changes.
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.
Updating the error type to not be a unit_error
should simplify the realm and scheme extraction from the user's configuration.
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.
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.
thx for the feedback, I'll hack away :)
@@ -509,6 +543,11 @@ unit_error! { | |||
pub UnsupportedMediaType: "The request's content-type is not supported" | |||
} | |||
|
|||
unit_error! { |
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 probably not a unit_error
if the scheme
is going to be provided
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.
what other type of error is it then? I just tried make it return Unauthorized request, I am not a full pro of warps internals ;)
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.
MissingHeader would be an error you could model this off of.
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.
thx, will look into it
While getting rid of unit-error and looking at your attempt with the Authorizer trait, I realized my example is broken. Here's a quick attempt to work around the limitation: use headers::authorization::Basic;
use std::collections::HashMap;
use std::sync::Arc;
use warp::Filter;
async fn is_authenticated(
auth_header: Basic,
pwdb: Arc<HashMap<&str, &str>>,
) -> Result<String, warp::Rejection> {
let user = auth_header.username();
if pwdb.get(user) == Some(&auth_header.password()) {
Ok(user.to_string())
} else {
Err(warp::reject::unauthorized("Basic", "Realm")) // stupid hack making unauthorized public
}
}
#[tokio::main]
async fn main() {
let pwdb: HashMap<_, _> = vec![
("alice", "wonderland"),
("bob", "cat"),
("carl", "IePai4ph"),
]
.into_iter()
.collect();
let pwdb = Arc::new(pwdb); // no lock-guard needed, it's read-only
let pwdb = warp::any().map(move || pwdb.clone());
let routes = warp::auth::basic("Realm name")
.and(pwdb.clone())
.and_then(is_authenticated)
.map(|user: String| format!("Hello, {} you're authenticated", user));
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
} Another important thing, the In my application I hacked around all those limitations making a custom rejection handler and more boilerplate. Would be nice to also allow for custom-content-type too while we are at it. I like your Authorizer trait idea, seems more flexible than my simple |
I am not sure I follow this, the 401 reply is sent back with an empty body so the
I would be curious where you end up with this, I ultimately decided that this wasn't the right abstraction but it could have been my implementation. |
the problem arises when the authentication fails, then the warp reject code we are building here is active, correct? If that is the case you'll have a content-type of plain/text instead of potentially wanted json. Plus no control of output on auth-error, you might want to send something like Basically we need to be able to handle
cases 1, 3 and 4 are easily doable with crate only code, number 2 is where the user likely wants to customise. |
This would have to follow down the rejection path. It is a bit more boilerplate but I think allowing for rejections to be handled outside of the primary handler is a nice thing about this pattern. Here is a gist with the basic pattern: https://gist.github.com/FreeMasen/c0ec97e03bfe93d3d703ee31a605bcbf |
thanks for the help, that is basically what I built for my app - I think it is a lot of boilerplate necessary to "just" have authentication. If we can find a way to hide that for the users of warp it'd be cooler. I'll return trying to find a nice solution on monday, no computer accessible this weekend :) |
I made my code a bit more readable and played with your example code.
I think we need to figure out a better way for |
quickhack for diff --git a/src/reject.rs b/src/reject.rs
index 05e4a86..e2cbb4c 100644
--- a/src/reject.rs
+++ b/src/reject.rs
@@ -337,6 +337,11 @@ impl Rejection {
false
}
}
+
+ /// workaround for recover() handler
+ pub fn make_response(&self) -> hyper::Response<Body> {
+ self.into_response()
+ }
}
impl<T: Reject> From<T> for Rejection {
then your examples else branch is simply let res = if let Some(inner) = rej.find::<AuthRejections>() {
// ...
} else {
rej.make_response()
}; but this change goes way beyond this issues title I feel. |
My expectations is that the UnauthorizedChallenge wouldn't be a rejection but a reply that is already taken care of by the framework. If I instrument an endpoint with |
This is what I mean, as soon as you recover you have to deal with any rejection, the |
I think you are understanding how recover is supposed to be used. In the case that a route is defined using
I believe this is still the point you are trying to make that I may be misunderstanding. Are you saying that you want to reply to an unauthorized request with some kind of custom body during the Unauthorized Challenge? If so, can you provide a little more detail about the use case you see as being a stumbling block? I would expect the body of the Unauthorized Challenge response to be empty making the content type not really important |
the issue is for browsers you need to send the 401 + www-authenticate header in 2 cases
If the basic() filter returns a reply for case 1. that could be okay, but you need to then redundantly construct the reply for case 2. in your code since the basic() will not pass along the scheme+realm into the rejection handler (it's a reply now). Also, given that 404 and more is a rejection, as soon I want to custom handle the auth using rejections, you also have to custom handle 404, connectionupgrade and whatnot. When all you wanted to do is create and handle a certain type of rejections. We need a rejections "filter" to go before for the content-type issue, if you want to write a pure json only api, you should be able to serve only json without sometimes changing to text/plain. This can be done by copying what is being coded here and handling every rejection, so one could argue it's fine to write lots of boilerplate for this special usecase (even though it'd be a verbatim copy changing only the content-type). Something like nginx config For standard authentication without custom requirements beyond authentication/authorization I feel it is too much extra code to write and confusing. I wouldn't expect I have to handle 404 myself when I only want to handle auth myself and vice versa. |
most webservers send a human readable string informing them auth is necessary, otherwise the browser window stays empty when the user clicks "cancel" in his auth-popup. But yeah, when the content is empty, you can also argue it is not necessary to send a content-type at all ;) |
I would expect this should lead to a 403 and not another 401 as defined here, also for reference
I think this is a separate issue entirely. I could possibly see the inclusion of another Server level configuration or some kind of |
correct, I patched it to send empty body and no content-type now.
I agree. Leaves us with how to best treat recover for rejections lowering the coding-load of auth implementations. My hack from above is basically the rejections-filter doing the default warp action if not handled myself. what do you think? |
I just played with this, the cors filter also is implemented with rejections, difference is it's use of wrapsealed, however if your example code was extended with CORS this way, the handle_rejections also gets the CORS rejections to handle: let routes = warp::auth::basic("Realm name")
.and(pwdb.clone())
.and_then(is_authenticated)
.map(|user: String| format!("Hello, {} you're authenticated", user))
.with(cors)
.recover(handle_rejections); normally nobody notices that because they probably chain the |
…hanks to FreeMasen example is not yet handling missing auth-headers correctly
I feel like auth should probably be a |
I agree a |
Hello @FreeMasen I started a new branch for a with-filter called It'd be great if we can work together on making that work with an async callback? or discuss a different API for building an auth filter? Anyways please let me know what you think |
This is for issue #62 based on pr #716 but mine actually works. However I understand it is a crazy hack how I pass the realm name along to create the additional header. If some maintainer could guide me how to make that clean please? I'll fix it as needed.
For cleaner implementation: I'd assume we need a new rejection type. Because a failed authentication (i.e. user/pass wrong) will require the same headers returned as this authentication header missing filter does, only now needed by another filter: the one checking the credentials.