1use std::collections::BTreeMap;
4
5use soaprs_core::{BoxFuture, SoapError, SoapResult};
6
7use crate::{Authentication, Authenticator, AuthorizationName, Credential};
8
9pub trait AuthenticationStrategy<P>: Authenticator<Credential, P>
11where
12 P: Send,
13{
14 fn strategy(&self) -> &AuthorizationName;
16}
17
18pub struct AuthenticationRegistry<P> {
20 strategies: BTreeMap<AuthorizationName, Box<dyn AuthenticationStrategy<P>>>,
21}
22
23impl<P> AuthenticationRegistry<P>
24where
25 P: Send,
26{
27 pub fn new() -> Self {
29 Self {
30 strategies: BTreeMap::new(),
31 }
32 }
33
34 pub fn register<S>(&mut self, strategy: S) -> SoapResult<()>
36 where
37 S: AuthenticationStrategy<P> + 'static,
38 {
39 let name = strategy.strategy().clone();
40 if self.strategies.contains_key(&name) {
41 return Err(SoapError::conflict(format!(
42 "authentication strategy `{name}` is already registered"
43 )));
44 }
45 self.strategies.insert(name, Box::new(strategy));
46 Ok(())
47 }
48
49 pub fn len(&self) -> usize {
51 self.strategies.len()
52 }
53
54 pub fn is_empty(&self) -> bool {
56 self.strategies.is_empty()
57 }
58}
59
60impl<P> Default for AuthenticationRegistry<P>
61where
62 P: Send,
63{
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl<P> Authenticator<Credential, P> for AuthenticationRegistry<P>
70where
71 P: Send,
72{
73 fn authenticate(&self, credential: Credential) -> BoxFuture<'_, SoapResult<Authentication<P>>> {
74 Box::pin(async move {
75 let requested = credential.strategy().clone();
76 let Some(strategy) = self.strategies.get(&requested) else {
77 return Err(SoapError::unsupported(format!(
78 "authentication strategy `{requested}` is not registered"
79 )));
80 };
81 let authentication = strategy.authenticate(credential).await?;
82 if authentication.strategy() != &requested {
83 return Err(SoapError::infrastructure(
84 "authentication strategy returned a mismatched strategy identity",
85 ));
86 }
87 Ok(authentication)
88 })
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use std::{
95 future::Future,
96 task::{Context, Poll, Waker},
97 };
98
99 use soaprs_core::{BoxFuture, SoapError, SoapErrorKind, SoapResult};
100
101 use super::{AuthenticationRegistry, AuthenticationStrategy};
102 use crate::{Authentication, Authenticator, AuthorizationName, Credential, StandardPrincipal};
103
104 struct FixedStrategy {
105 name: AuthorizationName,
106 }
107
108 impl Authenticator<Credential, StandardPrincipal> for FixedStrategy {
109 fn authenticate(
110 &self,
111 credential: Credential,
112 ) -> BoxFuture<'_, SoapResult<Authentication<StandardPrincipal>>> {
113 Box::pin(async move {
114 if credential.secret().expose_secret() != "valid" {
115 return Err(SoapError::unauthorized());
116 }
117 Authentication::new("jwt", StandardPrincipal::new("user-1")?)
118 })
119 }
120 }
121
122 impl AuthenticationStrategy<StandardPrincipal> for FixedStrategy {
123 fn strategy(&self) -> &AuthorizationName {
124 &self.name
125 }
126 }
127
128 #[test]
129 fn registry_rejects_duplicates_and_routes_without_type_erasure() {
130 let Some(name) = AuthorizationName::new("jwt").ok() else {
131 panic!("valid strategy name");
132 };
133 let mut registry = AuthenticationRegistry::new();
134 assert!(
135 registry
136 .register(FixedStrategy { name: name.clone() })
137 .is_ok()
138 );
139 assert_eq!(registry.len(), 1);
140 assert!(registry.register(FixedStrategy { name }).is_err());
141
142 let valid = Credential::bearer("jwt", "valid")
143 .unwrap_or_else(|error| panic!("valid credential: {error}"));
144 let result = block_on(registry.authenticate(valid));
145 assert!(result.is_ok());
146
147 let unknown = Credential::bearer("unknown", "valid")
148 .unwrap_or_else(|error| panic!("valid credential: {error}"));
149 let result = block_on(registry.authenticate(unknown));
150 assert_eq!(
151 result.as_ref().map_err(|error| error.kind()),
152 Err(SoapErrorKind::Unsupported)
153 );
154 }
155
156 fn block_on<F>(future: F) -> F::Output
157 where
158 F: Future,
159 {
160 let mut context = Context::from_waker(Waker::noop());
161 let mut future = std::pin::pin!(future);
162 loop {
163 match future.as_mut().poll(&mut context) {
164 Poll::Ready(output) => return output,
165 Poll::Pending => std::thread::yield_now(),
166 }
167 }
168 }
169}