use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use foundation_auth::{
extract_bearer_token as fa_extract_bearer_token, has_scope, AuthContext, AuthToken,
ConfidentialText,
};
use foundation_auth::{JwtVerifier, JwtVerifierConfig};
use foundation_errstacks::ErrorTrace;
use foundation_netio::shared::http::{
Extensions, SimpleHeader, SimpleHeaders, SimpleIncomingRequest, SimpleMethod,
SimpleOutgoingResponse, SimpleUrl,
};
use crate::shared::context::Ctx;
use crate::shared::error::{Code, ConnectError, ConnectResult};
use crate::shared::error_writer::{status_from_code, ErrorWriter};
use crate::shared::interceptor::{
Interceptor, StreamingClientFunc, StreamingHandlerFunc, UnaryFunc,
};
use crate::shared::protocol::canonicalize_content_type;
use crate::shared::transport::BoxFuture;
#[derive(Clone, Debug)]
pub struct AuthInfo {
pub context: AuthContext,
pub artifacts: Extensions,
}
impl AuthInfo {
#[must_use]
pub fn new(context: AuthContext) -> Self {
Self {
context,
artifacts: Extensions::new(),
}
}
#[must_use]
pub fn with_artifact<T: Send + Sync + 'static>(mut self, artifact: T) -> Self {
self.artifacts.insert(artifact);
self
}
}
#[must_use]
pub fn get_auth_info(ctx: &Ctx) -> Option<&AuthInfo> {
ctx.request.extensions.get::<AuthInfo>()
}
#[must_use]
pub fn get_auth_artifact<T: 'static>(ctx: &Ctx) -> Option<&T> {
get_auth_info(ctx)?.artifacts.get::<T>()
}
pub trait AuthFunc: Send + Sync + 'static {
fn authenticate<'a>(
&'a self,
headers: &'a SimpleHeaders,
url: &'a SimpleUrl,
) -> BoxFuture<'a, ConnectResult<Option<AuthInfo>>>;
}
impl<F, Fut> AuthFunc for F
where
F: Fn(&SimpleHeaders, &SimpleUrl) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ConnectResult<Option<AuthInfo>>> + Send + 'static,
{
fn authenticate<'a>(
&'a self,
headers: &'a SimpleHeaders,
url: &'a SimpleUrl,
) -> BoxFuture<'a, ConnectResult<Option<AuthInfo>>> {
Box::pin(self(headers, url))
}
}
#[must_use]
pub fn infer_protocol(request: &SimpleIncomingRequest) -> Option<&'static str> {
let ct = request
.headers
.get(&SimpleHeader::CONTENT_TYPE)
.and_then(|v| v.first())
.map(String::as_str)
.unwrap_or("");
let canonical = canonicalize_content_type(ct);
if request.method == SimpleMethod::POST {
if canonical.starts_with("application/grpc-web") {
return Some("grpc-web");
}
if canonical.starts_with("application/grpc") {
return Some("grpc");
}
if canonical.starts_with("application/connect+") || canonical.starts_with("application/") {
return Some("connect");
}
}
if request.method == SimpleMethod::GET {
if request
.request_url
.queries
.as_ref()
.and_then(|q| q.get("connect"))
.map_or(false, |v| v == "v1")
{
return Some("connect");
}
}
None
}
#[must_use]
pub fn infer_procedure(url: &SimpleUrl) -> Option<String> {
let path = &url.url;
let path_only = if let Some(pos) = path.find("://") {
let after_scheme = &path[pos + 3..];
match after_scheme.find('/') {
Some(p) => &after_scheme[p..],
None => "/",
}
} else {
path.as_str()
};
let segments: Vec<&str> = path_only
.trim_start_matches('/')
.split('/')
.filter(|s| !s.is_empty())
.collect();
if segments.len() >= 2 {
Some(format!(
"/{}/{}",
segments[segments.len() - 2],
segments[segments.len() - 1]
))
} else {
None
}
}
#[must_use]
pub fn bearer_token(request: &SimpleIncomingRequest) -> Option<String> {
let auth = request.headers.get(&SimpleHeader::AUTHORIZATION)?;
let header = auth.first()?;
fa_extract_bearer_token(Some(header.as_str()))
}
fn bearer_token_from_headers(headers: &SimpleHeaders) -> Option<String> {
let auth = headers.get(&SimpleHeader::AUTHORIZATION)?;
let header = auth.first()?;
fa_extract_bearer_token(Some(header.as_str()))
}
pub struct JwtAuthenticator {
verifier: JwtVerifier,
}
impl JwtAuthenticator {
pub fn new(config: JwtVerifierConfig) -> Result<Self, foundation_auth::JwtError> {
let verifier = JwtVerifier::from_config(config)?;
Ok(Self { verifier })
}
}
impl AuthFunc for JwtAuthenticator {
fn authenticate<'a>(
&'a self,
headers: &'a SimpleHeaders,
url: &'a SimpleUrl,
) -> BoxFuture<'a, ConnectResult<Option<AuthInfo>>> {
let token = bearer_token_from_headers(headers);
let path = url.url.clone();
Box::pin(async move {
let token = match token {
Some(t) => t,
None => return Ok(None),
};
match self.verifier.verify(&token) {
Ok(claims) => {
let mut ctx = AuthContext::new(path, None, None);
ctx.sub = Some(claims.sub.clone());
let auth_token = AuthToken::Jwt {
token: ConfidentialText::new(token),
expires_at: claims.exp.timestamp() as f64,
issuer: Some(claims.iss.clone()),
audience: Some(claims.aud.clone()),
};
ctx.token = Some(auth_token);
let mut info = AuthInfo::new(ctx);
info.artifacts.insert(claims);
Ok(Some(info))
}
Err(e) => Err(ConnectError::new(
Code::Unauthenticated,
format!("invalid token: {e}"),
)
.into()),
}
})
}
}
#[derive(Default)]
pub struct CompositeAuthenticator {
authenticators: Vec<Arc<dyn AuthFunc>>,
}
impl CompositeAuthenticator {
#[must_use]
pub fn new() -> Self {
Self {
authenticators: Vec::new(),
}
}
#[must_use]
pub fn with(mut self, auth: impl AuthFunc) -> Self {
self.authenticators.push(Arc::new(auth));
self
}
#[must_use]
pub fn with_arc(mut self, auth: Arc<dyn AuthFunc>) -> Self {
self.authenticators.push(auth);
self
}
}
impl AuthFunc for CompositeAuthenticator {
fn authenticate<'a>(
&'a self,
headers: &'a SimpleHeaders,
url: &'a SimpleUrl,
) -> BoxFuture<'a, ConnectResult<Option<AuthInfo>>> {
Box::pin(async move {
let mut last_err = None;
for auth in &self.authenticators {
match auth.authenticate(headers, url).await {
Ok(Some(info)) => return Ok(Some(info)),
Ok(None) => continue,
Err(e) => last_err = Some(e),
}
}
match last_err {
Some(e) => Err(e),
None => Ok(None),
}
})
}
}
pub struct PerProcedureAuth {
default_auth: Option<Arc<dyn AuthFunc>>,
procedure_auth: HashMap<String, Arc<dyn AuthFunc>>,
unauthenticated_procedures: HashSet<String>,
}
impl PerProcedureAuth {
#[must_use]
pub fn new() -> Self {
Self {
default_auth: None,
procedure_auth: HashMap::new(),
unauthenticated_procedures: HashSet::new(),
}
}
#[must_use]
pub fn with_default(mut self, auth: impl AuthFunc) -> Self {
self.default_auth = Some(Arc::new(auth));
self
}
#[must_use]
pub fn with_procedure(
mut self,
procedure: impl Into<String>,
auth: impl AuthFunc,
) -> Self {
self.procedure_auth.insert(procedure.into(), Arc::new(auth));
self
}
#[must_use]
pub fn without_auth(mut self, procedure: impl Into<String>) -> Self {
self.unauthenticated_procedures.insert(procedure.into());
self
}
}
impl Default for PerProcedureAuth {
fn default() -> Self {
Self::new()
}
}
impl AuthFunc for PerProcedureAuth {
fn authenticate<'a>(
&'a self,
headers: &'a SimpleHeaders,
url: &'a SimpleUrl,
) -> BoxFuture<'a, ConnectResult<Option<AuthInfo>>> {
Box::pin(async move {
let procedure = match infer_procedure(url) {
Some(p) => p,
None => return Ok(None),
};
if self.unauthenticated_procedures.contains(&procedure) {
return Ok(None);
}
if let Some(auth) = self.procedure_auth.get(&procedure) {
return auth.authenticate(headers, url).await;
}
if let Some(ref auth) = self.default_auth {
return auth.authenticate(headers, url).await;
}
Ok(None)
})
}
}
pub struct AuthzInterceptor {
required_scopes: Vec<String>,
}
impl AuthzInterceptor {
#[must_use]
pub fn new(required_scopes: Vec<String>) -> Self {
Self { required_scopes }
}
}
impl Interceptor for AuthzInterceptor {
fn wrap_unary(&self, next: UnaryFunc) -> UnaryFunc {
let scopes = self.required_scopes.clone();
Arc::new(move |ctx, call| {
let scopes = scopes.clone();
let next = next.clone();
Box::pin(async move {
if !scopes.is_empty() {
let info = get_auth_info(&ctx).ok_or_else(|| {
ErrorTrace::from(ConnectError::new(
Code::Unauthenticated,
"not authenticated",
))
})?;
let scope_refs: Vec<&str> = scopes.iter().map(String::as_str).collect();
if !has_scope(&info.context, &scope_refs) {
return Err(ConnectError::new(
Code::PermissionDenied,
"insufficient scope",
)
.into());
}
}
next(ctx, call).await
})
})
}
fn wrap_streaming_client(&self, next: StreamingClientFunc) -> StreamingClientFunc {
let scopes = self.required_scopes.clone();
Arc::new(move |ctx, spec| {
let scopes = scopes.clone();
let next = next.clone();
Box::pin(async move {
if !scopes.is_empty() {
let info = get_auth_info(&ctx).ok_or_else(|| {
ErrorTrace::from(ConnectError::new(
Code::Unauthenticated,
"not authenticated",
))
})?;
let scope_refs: Vec<&str> = scopes.iter().map(String::as_str).collect();
if !has_scope(&info.context, &scope_refs) {
return Err(ConnectError::new(
Code::PermissionDenied,
"insufficient scope",
)
.into());
}
}
next(ctx, spec).await
})
})
}
fn wrap_streaming_handler(&self, next: StreamingHandlerFunc) -> StreamingHandlerFunc {
let scopes = self.required_scopes.clone();
Arc::new(move |ctx, call| {
let scopes = scopes.clone();
let next = next.clone();
Box::pin(async move {
if !scopes.is_empty() {
let info = get_auth_info(&ctx).ok_or_else(|| {
ErrorTrace::from(ConnectError::new(
Code::Unauthenticated,
"not authenticated",
))
})?;
let scope_refs: Vec<&str> = scopes.iter().map(String::as_str).collect();
if !has_scope(&info.context, &scope_refs) {
return Err(ConnectError::new(
Code::PermissionDenied,
"insufficient scope",
)
.into());
}
}
next(ctx, call).await
})
})
}
}
pub async fn authenticate_request(
auth: &dyn AuthFunc,
request: &mut SimpleIncomingRequest,
error_writer: &ErrorWriter,
) -> Result<Option<AuthInfo>, SimpleOutgoingResponse> {
let result = {
let headers: &SimpleHeaders = &request.headers;
let url: &SimpleUrl = &request.request_url;
auth.authenticate(headers, url).await
};
match result {
Ok(Some(info)) => {
let exts = request.extensions.get_or_insert_with(Extensions::new);
exts.insert(info.clone());
Ok(Some(info))
}
Ok(None) => Ok(None),
Err(e) => {
let status = status_from_code(e.current_context().code());
let mut resp = SimpleOutgoingResponse::builder()
.with_status(status)
.build()
.expect("valid response");
let _ = error_writer.write(&mut resp, request, &e);
Err(resp)
}
}
}