use anyhow::bail;
use axum::body::Bytes;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Redirect};
use axum_extra::extract::CookieJar;
use axum_extra::extract::cookie::{Cookie, SameSite};
use cookie::time::Duration;
use std::sync::Arc;
use base64::{Engine as B64Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use hyper::HeaderMap;
use ordinary_config::AuthConfig;
fn get_token_from_headers(headers: &HeaderMap) -> anyhow::Result<Bytes> {
if let Some(val) = headers.get("authorization")
&& let Ok(str_val) = val.to_str()
&& let Some(b64_token) = str_val.strip_prefix("Bearer ")
&& let Ok(token) = b64.decode(b64_token)
{
Ok(Bytes::copy_from_slice(&token))
} else {
bail!("no valid token")
}
}
pub async fn access(
State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
headers: HeaderMap,
) -> impl IntoResponse {
let span = tracing::info_span!("token", flv = %"fetch");
span.in_scope(|| {
let Ok(token) = get_token_from_headers(&headers) else {
return StatusCode::UNAUTHORIZED.into_response();
};
match state.auth.access_get(&token) {
Ok(token) => (StatusCode::OK, token).into_response(),
Err(err) => {
tracing::warn!(%err);
StatusCode::UNAUTHORIZED.into_response()
}
}
})
}
fn access_token_cookie(
jar: CookieJar,
token: &Bytes,
auth_config: &AuthConfig,
secure: bool,
) -> CookieJar {
let name = if secure {
tracing::info!("generated host access cookie");
"__Host-ORDINARY-ACCESS-TOKEN"
} else {
tracing::warn!("generated insecure access cookie");
"ORDINARY-ACCESS-TOKEN"
};
jar.add(
Cookie::build((name, b64.encode(token)))
.secure(secure)
.http_only(true)
.path("/")
.same_site(SameSite::Strict)
.max_age(Duration::seconds(i64::from(
auth_config.access_token.lifetime,
))),
)
}
pub async fn access_cookies(
State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
jar: CookieJar,
headers: HeaderMap,
) -> impl IntoResponse {
let span = tracing::info_span!("token", flv = %"cookie");
span.in_scope(|| {
let Ok(token) = get_token_from_headers(&headers) else {
return StatusCode::UNAUTHORIZED.into_response();
};
match state.auth.access_get(&token) {
Ok(token) => {
let cookie = if state.auth.config.cookies_enabled {
Some(access_token_cookie(
jar,
&token,
&state.auth.config,
state.secure_cookies,
))
} else {
None
};
(StatusCode::OK, cookie, token).into_response()
}
Err(err) => {
tracing::warn!(%err);
StatusCode::UNAUTHORIZED.into_response()
}
}
})
}
pub async fn access_redirect(
State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
jar: CookieJar,
) -> Result<(CookieJar, Redirect), StatusCode> {
let span = tracing::info_span!("token", flv = %"redirect");
let cookie_name = if state.secure_cookies {
"__Secure-ORDINARY-REFRESH-TOKEN"
} else {
"ORDINARY-REFRESH-TOKEN"
};
span.in_scope(|| {
if let Some(refresh_token) = jar.get(cookie_name) {
if let Ok(refresh_token) = b64.decode(refresh_token.value()) {
if let Ok(access_token) = state
.auth
.access_get(&Bytes::copy_from_slice(&refresh_token[..]))
{
return Ok((
access_token_cookie(
jar,
&access_token,
&state.auth.config,
state.secure_cookies,
),
Redirect::to("/"),
));
}
} else {
tracing::error!("failed to decode refresh cookie");
}
} else {
tracing::error!("no refresh cookie");
}
Err(StatusCode::UNAUTHORIZED)
})
}