I have several routes that require the user to be logged. How redirect to the login route if the users hit a url and is not authorized?
If not authorised, return a responses with a 302 or 303 status plus a Location header with the page to redirect to.
Yeah, but where? This relate to https://github.com/actix/actix-web/issues/1488. I have a token with the user details:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenServer(pub Token);
impl FromRequest for TokenServer {
type Error = actix_web::Error;
type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &HttpRequest, pl: &mut dev::Payload) -> Self::Future {
if let Some(identity) = Identity::from_request(req, pl)
.into_inner()
.ok()
.map(|x| x.identity())
.unwrap()
{
let user: Token = serde_json::from_str(&identity).expect("Invalid token payload");
ok(TokenServer(user))
} else if is_ui_request(req) {
let company = get_req_company(req).unwrap();
let user = Token::test(company, "10");
ok(TokenServer(user))
} else {
err(ServerErr::CoreError {
source: CoreErr::Unauthorized,
}
.into())
}
}
}
But here, how the the redirect? If I wait until I get the error on the handlers:
fn error_page<B>(res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
let company = get_req_company(&res.request()).unwrap();
let page = match admin::error_page(company.into(), res.status()) {
Ok(page) => page,
Err(err) => HttpResponse::InternalServerError().body(format!("{}", err)),
};
Ok(ErrorHandlerResponse::Response(
res.into_response(page.into_body()),
))
}
.wrap(
ErrorHandlers::new()
.handler(http::StatusCode::NOT_FOUND, error_page)
.handler(http::StatusCode::INTERNAL_SERVER_ERROR, error_page),
)
I lost the flow. I imagine I could put another handler in ErrorHandlers, but I think this flow is weird. Is possible to use instead middlewares or something like that?
Probably in your route handler. You need to, somewhere, generate an HttpResponse with the correct status code and Location set and return it.
async fn route_handler(token: Option<TokenServer>) -> HttpRepsonse {
if token.is_none() {
HttpResponse::Found().header("Location", "/login").finish()
} else {
HttpResponse::Ok().finish()
}
}
I would do this in FE, not by the API BE.
A common pattern in some other web frameworks (e.g. Phoenix for Elixir) is to have all the routes that require authentication guarded by an a auth middleware. The middleware can try to extract a token, and produce a redirect response early (without reaching the handler) if the extraction fails. Is this possible with Actix middlewares?
Is possible to use route_handler "globally"? That will be nice, but if is a guard per route, less nice.
One can conceive of a variation to the method used in that example route handler that makes it a single line per route to return 302 "errors" with redirections.
In your example code, you return a ServerErr from the FromRequest impl. By using token: Result<TokenServer, ServerErr> extractor, taking advantage of ? operator, and implementing the ResponseError trait on this struct can handle all sorts of errors, producing HttpResponses; in this case producing a HttpResponse with the status and headers set correctly.
Alternatively, using some simple middleware on routes with common prefixes can extract tokens and return errors before hitting the handlers, enabling handlers to
@mamcx So how do you want to _login_ if ALL routes are guarded globally?
You may need to have several other open routes in your API also.
I think it should be per route and having a global guard (AKA middleware) is not good for this case.
Anyhow if you require a really global redirect for all routes (except /login), check this example.
Another good solution can be to have an extractor like what you have here, then have a middleware to add the _Location_ header to all responses with "401 Unauthorized".
I think this way will satisfy everybody :)
@mamcx So how do you want to _login_ if ALL routes are guarded globally?
You may need to have several other open routes in your API also.
I think it should be per route and having a global guard (AKA middleware) is not good for this case.Anyhow if you require a really global redirect for all routes (except /login), check this example.
This is what I want, but get stuck in how extract the identity from it:
fn call(&mut self, req: ServiceRequest) -> Self::Future {
// We only need to hook into the `start` for this middleware.
let is_logged_in = false; // Change this to see the change in outcome in the browser
let (r, mut pl) = req.into_parts(); <--- THIS MOVE
let identity = Identity::from_request(&r, &mut pl).into_inner();
if is_logged_in {
Either::Left(self.service.call(req))
and then have no way to check for this :(
Another good solution can be to have an extractor like what you have here, then have a middleware to add the Location header to all responses with "401 Unauthorized".
How this could be?
@mamcx Regarding your original question: "How auto-redirect to login when user is not authorized?"...
The answer can be using middlewares to add a header to all 401 responses.
You can read the documentation here:
https://docs.rs/actix-web/2.0.0/actix_web/middleware/errhandlers/struct.ErrorHandlers.html
In the example, replace 500 (internal server error) error code with 401 and instead of inserting content_type header, insert location header.
@ZizhengTai are you also fine with this?
If you have any other questions not directly related to this issue, maybe opening another issue or checking examples can help.
@omid The 401 middleware approach makes sense. Thank you.
I found a way to solve the issue in how pass the data to get the identity:
fn call(&mut self, req: ServiceRequest) -> Self::Future {
if not_auth(req.path()) {
return Either::Left(self.service.call(req));
};
let (r, mut pl) = req.into_parts(); <- unpack
let token = auto_login(&r, &mut pl); <-- EXTRACT identity
let req = ServiceRequest::from_parts(r, pl).ok().unwrap(); <-- repack
if token.is_some() {
Either::Left(self.service.call(req))
} else {
Either::Right(ok(req.into_response(
HttpResponse::Found()
.header(http::header::LOCATION, "/login")
.finish()
.into_body(),
)))
}
}
Most helpful comment
I found a way to solve the issue in how pass the data to get the identity: