Securing json-rpc end point
Hi, thanks for great library. I do have a query the what would be best way to secure JSON-RPC end points. Because if I use session or identity I need to pass request or session or identity in function and that seems to be not allowed ( or I might be missing something) .
What I should be doing in that case ? Is it allowed then how and if now what are my options here ?
Hi @kunjee17 , sorry for the delay in responding. That is a great question. It would definitely be possible to pass in pieces of the HTTP request to the handlers. I guess my initial thought was that the RPC framework should be agnostic about where/how it is running, but we are already providing pragmatic things like the Data extractors.
Let me think about the best way to handle this, or let me know if you have any thoughts. I don't think it will be as simple as cloning the actix_web::HttpRequest and making it available through the FromRequest extractor.
Currently, you could pass in an auth token directly to the RPC endpoint, and checking it inside the handler, but that might not be very ergonomic. Alternatively, you could create an actix-web guard or middleware of some kind to check identity before passing the request to the RPC web service (though that would not work if you need different behavior for each method/endpoint).
@kunjee17 I've made a first attempt at this here: #23. I don't think this will be too obtrusive to the non-HTTP use case. You would be responsible for copying identity/auth data from the request to the new RPC LocalData object. If you think this would work, I'll need to add support for Hyper, clean up the docs, and improve some of the ergonomics before publishing.
@kardeiz Thanks for reply. I was trying to integrate actix-session / actix-identity (it is wrapper around actix-session) with json rpc. While doing so I needed a access to method with session parameter.
fn index(session: Session) -> Result<&'static str, Error> {
// access the session state
if let Some(count) = session.get::<i32>("counter")? {
println!("SESSION value: {}", count);
// modify the session state
session.insert("counter", count + 1)?;
} else {
session.insert("counter", 1)?;
}
Ok("Welcome!")
}
Similar case is with identity.
#[get("/")]
async fn index(user: Option<Identity>) -> impl Responder {
if let Some(user) = user {
format!("Welcome! {}", user.id().unwrap())
} else {
"Welcome Anonymous!".to_owned()
}
}
#[post("/login")]
async fn login(request: HttpRequest) -> impl Responder {
// Some kind of authentication should happen here
// e.g. password-based, biometric, etc.
// [...]
// attach a verified user identity to the active session
Identity::login(&request.extensions(), "User1".into()).unwrap();
HttpResponse::Ok()
}
#[post("/logout")]
async fn logout(user: Identity) -> impl Responder {
user.logout();
HttpResponse::Ok()
}
Now if some how I can pass those in RPC method then it would be great. I guess LocalData should work if I can pass those in LocalData. (Example or little help to move in direction to do that would be great)
Another option as you would mention would be pass token ( jwt or any other bearer) as part of params. I thought of that as fall back option if nothing works out.
Fable remoting using F# is doing that
type IBookStoreApi = {
// login to acquire an auth token
login : LoginInfo -> Async<Result<SecurityToken, LoginError>>
// "public" function: no auth needed
searchBooksByTitle : string -> Async<list<Book>>
// secure function, requires a token
booksOnWishlist : SecurityToken -> Async<Result<list<Book>, AuthenticationError>>,
// secure function, requires a token and a book id
removeBookFromWishlist : SecureRequest<BookId> -> Async<Result<BookRemovalResult, AuthenticationError>>
// etc . . .
}
They also provide implicit authentication. also allowed access to request context. You can find more details here . Remoting api is heavily used as part of SAFE and SAFEr stack.
You can find whole doc here.
So, in gist
- Access to Http Req ( I guess it is possible but I can't find doc) so I can pull token from it in rpc method
- Allowing session or identity or any other extra to pass in to method ( Local Data might work) without modifying params
- If nothing works out. Example of how we can use jwt token as part of params just help future users :)
Hope I would be helpful. And thanks for replying back.
@kunjee17 Thanks for the additional information. I think #23 is now pretty close to how this will work.
Getting data from the HttpRequest in a future exposed some pretty large differences in how actix-web and hyper work (actix-web uses mostly local/non-Send futures), so I have split out the code for each into their own files.
If you grab the code from the PR branch you should be able to use it like:
let rpc = Server::new()
.with_extract_from_http_request_fn(|req|
<Identity as FromRequest>::extract(req)
.map_err(|_| Error::internal("No ID"))
)
.with_method(/* ... */)
And then extract it by adding a parameter to your handler: HttpRequestLocalData<Identity>.
Let me know if you have any further questions or concerns
@kardeiz I guess extract method works as it should be. PR #23 looks good to me. So, I guess it is good to go from my end. Thanks for neat solution instead of passing token in params. This will provide neat way to pass user data from session or identity to function. I do have a question though.
How can I access Http Req in handler
Means if I am having login method with params (username and password) and if I am doing validation I need to attach session to incoming request.
#[post("/login")]
async fn login(request: HttpRequest) -> impl Responder {
// Some kind of authentication should happen here
// e.g. password-based, biometric, etc.
// [...]
// attach a verified user identity to the active session
Identity::login(&request.extensions(), "User1".into()).unwrap();
HttpResponse::Ok()
}
if you see line Identity::login(&request.extensions(), "User1".into()).unwrap(); . It is adding request as parameter. How can I do that?