use std::collections::BTreeMap;
use soaprs_core::{BoxFuture, SoapError, SoapResult};
use crate::{Authentication, Authenticator, AuthorizationName, Credential};
pub trait AuthenticationStrategy<P>: Authenticator<Credential, P>
where
P: Send,
{
fn strategy(&self) -> &AuthorizationName;
}
pub struct AuthenticationRegistry<P> {
strategies: BTreeMap<AuthorizationName, Box<dyn AuthenticationStrategy<P>>>,
}
impl<P> AuthenticationRegistry<P>
where
P: Send,
{
pub fn new() -> Self {
Self {
strategies: BTreeMap::new(),
}
}
pub fn register<S>(&mut self, strategy: S) -> SoapResult<()>
where
S: AuthenticationStrategy<P> + 'static,
{
let name = strategy.strategy().clone();
if self.strategies.contains_key(&name) {
return Err(SoapError::conflict(format!(
"authentication strategy `{name}` is already registered"
)));
}
self.strategies.insert(name, Box::new(strategy));
Ok(())
}
pub fn len(&self) -> usize {
self.strategies.len()
}
pub fn is_empty(&self) -> bool {
self.strategies.is_empty()
}
}
impl<P> Default for AuthenticationRegistry<P>
where
P: Send,
{
fn default() -> Self {
Self::new()
}
}
impl<P> Authenticator<Credential, P> for AuthenticationRegistry<P>
where
P: Send,
{
fn authenticate(&self, credential: Credential) -> BoxFuture<'_, SoapResult<Authentication<P>>> {
Box::pin(async move {
let requested = credential.strategy().clone();
let Some(strategy) = self.strategies.get(&requested) else {
return Err(SoapError::unsupported(format!(
"authentication strategy `{requested}` is not registered"
)));
};
let authentication = strategy.authenticate(credential).await?;
if authentication.strategy() != &requested {
return Err(SoapError::infrastructure(
"authentication strategy returned a mismatched strategy identity",
));
}
Ok(authentication)
})
}
}
#[cfg(test)]
mod tests {
use std::{
future::Future,
task::{Context, Poll, Waker},
};
use soaprs_core::{BoxFuture, SoapError, SoapErrorKind, SoapResult};
use super::{AuthenticationRegistry, AuthenticationStrategy};
use crate::{Authentication, Authenticator, AuthorizationName, Credential, StandardPrincipal};
struct FixedStrategy {
name: AuthorizationName,
}
impl Authenticator<Credential, StandardPrincipal> for FixedStrategy {
fn authenticate(
&self,
credential: Credential,
) -> BoxFuture<'_, SoapResult<Authentication<StandardPrincipal>>> {
Box::pin(async move {
if credential.secret().expose_secret() != "valid" {
return Err(SoapError::unauthorized());
}
Authentication::new("jwt", StandardPrincipal::new("user-1")?)
})
}
}
impl AuthenticationStrategy<StandardPrincipal> for FixedStrategy {
fn strategy(&self) -> &AuthorizationName {
&self.name
}
}
#[test]
fn registry_rejects_duplicates_and_routes_without_type_erasure() {
let Some(name) = AuthorizationName::new("jwt").ok() else {
panic!("valid strategy name");
};
let mut registry = AuthenticationRegistry::new();
assert!(
registry
.register(FixedStrategy { name: name.clone() })
.is_ok()
);
assert_eq!(registry.len(), 1);
assert!(registry.register(FixedStrategy { name }).is_err());
let valid = Credential::bearer("jwt", "valid")
.unwrap_or_else(|error| panic!("valid credential: {error}"));
let result = block_on(registry.authenticate(valid));
assert!(result.is_ok());
let unknown = Credential::bearer("unknown", "valid")
.unwrap_or_else(|error| panic!("valid credential: {error}"));
let result = block_on(registry.authenticate(unknown));
assert_eq!(
result.as_ref().map_err(|error| error.kind()),
Err(SoapErrorKind::Unsupported)
);
}
fn block_on<F>(future: F) -> F::Output
where
F: Future,
{
let mut context = Context::from_waker(Waker::noop());
let mut future = std::pin::pin!(future);
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(output) => return output,
Poll::Pending => std::thread::yield_now(),
}
}
}
}