use rustlavel::prelude::*;
use rustlavel::oauth::verify::{self, Claims, INTROSPECTION_TTL};
use {{crate_name}}_shared::{Caller, problem};
#[derive(Clone)]
pub enum Verifier {
Introspect { url: String, client_id: String, secret: String },
Signed { public_key: Vec<u8> },
}
impl Verifier {
pub fn from_env() -> Verifier {
match rustlavel::env::env_or("TOKEN_VERIFICATION", "introspect").as_str() {
"signed" => Verifier::Signed {
public_key: rustlavel::auth::base64::decode(
&rustlavel::env::env_or("TOKEN_PUBLIC_KEY", ""),
)
.unwrap_or_default(),
},
_ => Verifier::Introspect {
url: rustlavel::env::env_or("AUTH_URL", "http://127.0.0.1:9001"),
client_id: rustlavel::env::env_or("INTROSPECT_CLIENT_ID", ""),
secret: rustlavel::env::env_or("INTROSPECT_CLIENT_SECRET", ""),
},
}
}
pub async fn claims(&self, req: &Request, token: &str) -> Option<Claims> {
let now = rustlavel::auth::unix_now() as i64;
match self {
Verifier::Signed { public_key } => verify::verify_signed(token, public_key, now).ok(),
Verifier::Introspect { url, client_id, secret } => {
let key = format!("introspect:{}", rustlavel::auth::hashing::sha256_hex(token.as_bytes()));
if let Some(cache) = req.state::<CacheStore>()
&& let Ok(Some(cached)) = cache.get(&key).await
&& let Ok(claims) = verify::read_introspection(&cached, now)
{
return Some(claims);
}
let credentials = rustlavel::auth::base64::encode(
format!("{client_id}:{secret}").as_bytes(),
);
let response = match Client::new()
.post(format!("{url}/oauth/introspect"))
.header("authorization", format!("Basic {credentials}"))
.header("content-type", "application/x-www-form-urlencoded")
.body(format!("token={}", rustlavel::url::encode(token)))
.send()
.await
{
Ok(response) => response,
Err(error) => {
error!("token check: cannot reach {url}/oauth/introspect: {error}");
return None;
}
};
if !response.is_success() {
error!(
"token check: {url} refused this service's introspection credentials \
(HTTP {}). Check INTROSPECT_CLIENT_ID and INTROSPECT_CLIENT_SECRET.",
response.status
);
return None;
}
let body = match response.json() {
Ok(body) => body,
Err(error) => {
error!("token check: {url} sent something unreadable: {error}");
return None;
}
};
let claims = match verify::read_introspection(&body, now) {
Ok(claims) => claims,
Err(_) => {
debug!("token check: the token was refused");
return None;
}
};
if let Some(cache) = req.state::<CacheStore>() {
let _ = cache.put(&key, body, INTROSPECTION_TTL).await;
}
Some(claims)
}
}
}
}
pub struct RequireCaller;
impl Middleware for RequireCaller {
fn handle(&self, mut request: Request, next: Next) -> BoxFuture<Response> {
Box::pin(async move {
if request.path() == "/health" {
return next.run(request).await;
}
let presented = request
.header("authorization")
.and_then(|value| value.strip_prefix("Bearer ").map(str::to_string));
let Some(token) = presented else {
return Response::new(Status::UNAUTHORIZED).with_json(problem("Unauthenticated."));
};
let verifier = match request.state::<Verifier>() {
Some(verifier) => verifier.clone(),
None => {
error!("no Verifier in state; every request will be refused");
return Response::new(Status::INTERNAL_ERROR);
}
};
let Some(claims) = verifier.claims(&request, &token).await else {
return Response::new(Status::UNAUTHORIZED).with_json(problem("Unauthenticated."));
};
request.extend(Caller {
subject: claims.subject,
client: claims.client,
scopes: claims.scopes.iter().map(str::to_string).collect(),
});
next.run(request).await
})
}
}