-
Notifications
You must be signed in to change notification settings - Fork 20
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
User authentication should be done in middleware #39
Comments
Hi @WarmBeer in the new Axum implementation I'm using an extractor: because some endpoints require authentication and others don't. And this is how it looks like in the upload torrent endpoint: pub async fn upload_torrent_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
multipart: Multipart,
) -> Response {
let user_id = match app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await {
Ok(user_id) => user_id,
Err(error) => return error.into_response(),
};
let torrent_request = match get_torrent_request_from_payload(multipart).await {
Ok(torrent_request) => torrent_request,
Err(error) => return error.into_response(),
};
let info_hash = torrent_request.torrent.info_hash().clone();
match app_data.torrent_service.add_torrent(torrent_request, user_id).await {
Ok(torrent_id) => new_torrent_response(torrent_id, &info_hash).into_response(),
Err(error) => error.into_response(),
}
} We could improve it. We could have a new Axum extractor that could be ExtractLoggedInUser(logged_in_user): ExtractLoggedInUser,
// or
ExtractAuthenticatedUser(authenticated_user): ExtractAuthenticatedUser, pub async fn upload_torrent_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
user_id: Option<ExtractAuthenticatedUser>
multipart: Multipart,
) That way, we could remove this from the handler:
In case the endpoint requires the user to be authenticated, we could use it without the optional: pub async fn upload_torrent_handler(
State(app_data): State<Arc<AppData>>,
ExtractAuthenticatedUser(user_id): ExtractAuthenticatedUser
multipart: Multipart,
) |
WIP. |
The upload torrent handler now uses the extractor to get the user id.
The upload torrent handler now uses the extractor to get the user id.
Hi @mario-nt just a clarification, regarding authentication there are three types of endpoints:
The "extractor that could be " would be used for the type-3 endpoitns. |
I've been discussing with @mario-nt how to pub async fn upload_torrent_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
multipart: Multipart,
) -> Response {
let user_id = match app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await {
Ok(user_id) => user_id,
Err(error) => return error.into_response(),
};
let add_torrent_form = match build_add_torrent_request_from_payload(multipart).await {
Ok(torrent_request) => torrent_request,
Err(error) => return error.into_response(),
};
match app_data.torrent_service.add_torrent(add_torrent_form, user_id).await {
Ok(response) => new_torrent_response(&response).into_response(),
Err(error) => error.into_response(),
}
} We want to move this code: let user_id = match app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await {
Ok(user_id) => user_id,
Err(error) => return error.into_response(),
}; to the middleware. We found these links: How do you access state in a custom Extractor?: |
Relates to: torrust/torrust-index-gui#424 Hi @mario-nt. There are two functions for authorization:
get_user_id_from_bearer_token let user_id = match app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await {
Ok(user_id) => user_id,
Err(error) => return error.into_response(),
}; Used in these handlers (torrent context):
get_optional_logged_in_user let opt_user_id = match get_optional_logged_in_user(maybe_bearer_token, app_data.clone()).await {
Ok(opt_user_id) => opt_user_id,
Err(error) => return error.into_response(),
}; Used in these handlers (torrent context):
Corner case: valid user for non-existing userSometimes you can get an unauthorized response. See torrust/torrust-index-gui#424. That's because the token is valid but the user does not exist anymore. Since we are not removing users from the application, that should happen only for development when you reset the database or manually remove it. Anyway, I think we should consider this corner case, in case we can remove users in the future. These are the functions we used in the handlers: pub async fn get_optional_logged_in_user(
maybe_bearer_token: Option<BearerToken>,
app_data: Arc<AppData>,
) -> Result<Option<UserId>, ServiceError> {
match maybe_bearer_token {
Some(bearer_token) => match app_data.auth.get_user_id_from_bearer_token(&Some(bearer_token)).await {
Ok(user_id) => Ok(Some(user_id)),
Err(error) => Err(error),
},
None => Ok(None),
}
}
// ...
pub async fn get_user_id_from_bearer_token(&self, maybe_token: &Option<BearerToken>) -> Result<UserId, ServiceError> {
let claims = self.get_claims_from_bearer_token(maybe_token).await?;
Ok(claims.user.user_id)
} For the endpoints that require a logged-in user we are not checking if the user exists in the handler. For example: pub async fn upload_torrent_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
multipart: Multipart,
) -> Response {
let user_id = match app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await {
Ok(user_id) => user_id,
Err(error) => return error.into_response(),
};
let add_torrent_form = match build_add_torrent_request_from_payload(multipart).await {
Ok(torrent_request) => torrent_request,
Err(error) => return error.into_response(),
};
match app_data.torrent_service.add_torrent(add_torrent_form, user_id).await {
Ok(response) => new_torrent_response(&response).into_response(),
Err(error) => error.into_response(),
}
} We check whether the user exists or not in the For the endpoints that do not require a logged-in user we are checking if the user exists in the middleware. For example: pub async fn download_torrent_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
Path(info_hash): Path<InfoHashParam>,
) -> Response {
let Ok(info_hash) = InfoHash::from_str(&info_hash.lowercase()) else {
return errors::Request::InvalidInfoHashParam.into_response();
};
debug!("Downloading torrent: {:?}", info_hash.to_hex_string());
if let Some(redirect_response) = redirect_to_download_url_using_canonical_info_hash_if_needed(&app_data, &info_hash).await {
debug!("Redirecting to URL with canonical info-hash");
redirect_response
} else {
let opt_user_id = match get_optional_logged_in_user(maybe_bearer_token, app_data.clone()).await {
Ok(opt_user_id) => opt_user_id,
Err(error) => return error.into_response(),
};
let torrent = match app_data.torrent_service.get_torrent(&info_hash, opt_user_id).await {
Ok(torrent) => torrent,
Err(error) => return error.into_response(),
};
let Ok(bytes) = parse_torrent::encode_torrent(&torrent) else {
return ServiceError::InternalServerError.into_response();
};
torrent_file_response(
bytes,
&format!("{}.torrent", torrent.info.name),
&torrent.canonical_info_hash_hex(),
)
}
} Notice that I think we should normalize the app behavior in these cases. I think we should:
@mario-nt your implementation is fine so far because you are doing that for the case when a logged-in user is required. If you implement the other extractor to extract the optional logged-in user you have to reject the request if a token is provided but the user does not exist. I think the extractor would be very similar but you can return |
User authentication is currently done manually in each endpoint that requires authentication. Ideally, we would have an authentication middleware instead. The authentication middleware could pass
Option<User>
to the endpoint handler.The text was updated successfully, but these errors were encountered: