5 releases (breaking)
0.5.0 | May 19, 2025 |
---|---|
0.4.0 | May 19, 2025 |
0.3.0 | May 7, 2025 |
0.2.0 | May 2, 2025 |
0.1.0 | Apr 21, 2025 |
#312 in Authentication
568 downloads per month
35KB
799 lines
omnium
A set of extensions for building web applications on axum.
Unstable: This crate is not ready for use. The author is building out these extensions to iterate on a proof of concept, and the surface may change frequently.
api
The api::responses
module provides a set of response conventions for axum handlers, implementing axum's IntoResponse
trait for typical use cases.
A handler returns JsonResult
or TypedJsonResult<T>
, where JsonResult
can be used for type-erased responses and TypedJsonResult<T>
can be used for consistently-typed responses.
The Ok(...)
arm on these types can be used regardless of status code, to return custom responses:
// ...
use omnium::api::{JsonResult, JsonResponse};
async fn handler() -> JsonResult {
let result = try_do_or_err().await;
match result {
Ok => JsonResponse::of_status(StatusCode::ACCEPTED).into()
Err => JsonResponse::of_status(StatusCode::CONFLICT).into()
}
}
Response conventions are provided through the JsonResponse<T>
struct, which implements Into<JsonResult>
and Into<TypedJsonResult<T>>
, as well as axum's IntoResponse
.
A handler can return a JSON response for any serializable body, with a default OK
status:
async fn handler() -> JsonResult {
JsonResponse::of_json(body).into()
}
Another status code can be set on the response:
async fn handler() -> JsonResult {
JsonResponse::of_json(body).with_status(StatusCode::IM_A_TEAPOT).into()
}
A handler can return a simple JsonStatusBody
status response, implicitly deriving the response body as appropriate for the status:
async fn handler() -> JsonResult {
JsonResponse::of_status(StatusCode::OK).into()
}
An additional detail message can be added to the JsonStatusBody
:
async fn handler() -> JsonResult {
JsonResponse::of_status(StatusCode::OK).with_detail("Additional detail").into()
}
The default JsonResult
erases the response payload type. If you are returning a consistent type on response, you can return a TypedJsonResult<T>
:
async fn handler() -> TypedJsonResult<JsonStatusBody> {
JsonResponse::of_status(StatusCode::OK).with_detail("Additional detail").into()
}
The Err
arm is available on both JsonResult
and TypedJsonResult<T>
to handle status responses. For example, when using TypedJsonResult<T>
for the happy path, you may choose to return status responses to communicate other results to the caller, whether for an error, informational result, or other case:
async fn handler() -> TypedJsonResult<JsonStatusBody> {
Err(JsonResponse::of_status(StatusCode::OK).with_detail("Additional detail"))
}
Finally, the Err
arm is used to automatically handle internal server errors. A handler can return Err(Into<anyhow::Error>)
, which will be rendered as an INTERNAL_SERVER_ERROR
response.
async fn handler() -> JsonResult {
let success = try_do_or_err().await?;
// ...
}
With this convention, unhandled errors have built-in IntoResponse
rendering and other errors must be rendered explicitly by a handler. When the handler returns an internal error in this way, the error details are not visible to the caller.
security
The security
module provides JWT-based authentication middleware, with utilities for a cookie-based credential exchange or the authorization
header for browser-based or programmatic authentication.
Create a service secret:
let service_secret = create_service_secret();
Configure authentication middleware:
#[derive(Clone)]
struct AppUser {}
struct AppState {
pub service_secret: ServiceSecret,
}
impl SessionManager<AppUser> for Arc<AppState> {
async fn get_service_secret(&self) -> anyhow::Result<&ServiceSecret> {
// Return secret from application secret manager:
Ok(&self.service_secret)
}
async fn get_user(&self, _user_id: String) -> anyhow::Result<Option<AppUser>> {
// Return user from application database:
Ok(Some(AppUser {}))
}
fn extract_credential(&self, request: &Request, _cookies: &CookieJar) -> Option<Credential> {
// Extract credential from request:
Credential::from_authorization_header(&request)
}
}
Attach authentication middleware:
fn app(state: Arc<AppState>) -> Router {
Router::new()
.route("/api/user", get(|| async { "Hello, user!" }))
.layer(from_fn_with_state(
state.clone(),
authenticate::<AppUser, Arc<AppState>>,
))
.with_state(state)
}
Create the user session:
create_session(
"some-user-id",
&EncodingKey::from_secret(state.service_secret.value.as_bytes()),
Duration::from_secs(60),
);
With Credential::from_authorization_header
, a client may pass the session as the authorization
header. With Credential::from_cookie
, a client may pass the session as the __Host-omn-sess
cookie. For a simple, user-facing web application, you can set the __Host-omn-sess
cookie when the user signs in, in order to authenticate requests to the service running on the same origin. If the application shares a session across multiple services on different origins, it can expose the session for use by the client in the authorization
header for programmatic, cross-origin requests. You can plug in your own handling for extracting credentials from requests with a custom extract_credential
handler.
Requests from an unauthenticated user will reject with a 401 response.
For an authenticated user, the user object from user_lookup
can be retrieved from request state to avoid redundant lookup in handlers:
pub async fn handler(
Extension(caller): Extension<AppUser>,
) {
println!("Caller is: {}", caller);
}
Because a variety of data may need to be passed and verified between an application and its users, encode_claims
and decode_claims
may also be used for other purposes other than authentication. Note that claims are signed but not encrypted.
In addition to claims-based utilities, this crate wraps aes_gcm
to provide utilties encrypt_string_aes256_gcm
and decrypt_string_aes256_gcm
. A secret created by create_service_secret
can also be used with these encryption utilities.
Dependencies
~14–26MB
~455K SLoC