1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use futures::future::BoxFuture;
5use serde::Serialize;
6use thiserror::Error;
7
8use crate::config::UpstreamConfig;
9use crate::upstream::http_body_cap::BodyCappedHttpClient;
10
11pub type UpstreamOAuthHttpClient = rmcp::transport::AuthClient<BodyCappedHttpClient>;
12
13#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
14pub struct BeginAuthorization {
15 pub authorization_url: String,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct UpstreamOAuthCredentialStatus {
20 pub access_token_expires_at: i64,
21 pub refresh_token_present: bool,
22}
23
24#[derive(Debug, Error, Clone, PartialEq, Eq)]
25#[error("{kind}: {message}")]
26pub struct UpstreamOAuthError {
27 kind: String,
28 message: String,
29}
30
31impl UpstreamOAuthError {
32 #[must_use]
33 pub fn new(kind: impl Into<String>, message: impl Into<String>) -> Self {
34 Self {
35 kind: kind.into(),
36 message: message.into(),
37 }
38 }
39
40 #[must_use]
41 pub fn internal(message: impl Into<String>) -> Self {
42 Self::new("internal_error", message)
43 }
44
45 #[must_use]
46 pub fn kind(&self) -> &str {
47 &self.kind
48 }
49
50 #[must_use]
51 pub fn message(&self) -> &str {
52 &self.message
53 }
54}
55
56pub trait UpstreamOAuthProvider: Send + Sync {
57 fn authenticated_http_client<'a>(
58 &'a self,
59 upstream: &'a UpstreamConfig,
60 subject: &'a str,
61 http_client: BodyCappedHttpClient,
62 ) -> BoxFuture<'a, Result<UpstreamOAuthHttpClient, UpstreamOAuthError>>;
63
64 fn evict_subject(&self, _upstream: &str, _subject: &str) {}
65
66 fn evict_upstream(&self, _upstream: &str) {}
67}
68
69pub trait UpstreamOAuthManager: Send + Sync {
70 fn begin_authorization<'a>(
71 &'a self,
72 subject: &'a str,
73 ) -> BoxFuture<'a, Result<BeginAuthorization, UpstreamOAuthError>>;
74
75 fn complete_authorization<'a>(
76 &'a self,
77 subject: &'a str,
78 code: &'a str,
79 state: &'a str,
80 issuer: Option<&'a str>,
81 ) -> BoxFuture<'a, Result<(), UpstreamOAuthError>>;
82
83 fn credential_status<'a>(
84 &'a self,
85 subject: &'a str,
86 ) -> BoxFuture<'a, Result<Option<UpstreamOAuthCredentialStatus>, UpstreamOAuthError>>;
87
88 fn clear_credentials<'a>(
89 &'a self,
90 subject: &'a str,
91 ) -> BoxFuture<'a, Result<(), UpstreamOAuthError>>;
92
93 fn access_token<'a>(
94 &'a self,
95 subject: &'a str,
96 ) -> BoxFuture<'a, Result<String, UpstreamOAuthError>>;
97}
98
99#[derive(Clone)]
100pub struct UpstreamOAuthRuntime {
101 provider: Arc<dyn UpstreamOAuthProvider>,
102 managers: Arc<BTreeMap<String, Arc<dyn UpstreamOAuthManager>>>,
103}
104
105impl UpstreamOAuthRuntime {
106 #[must_use]
107 pub fn new(
108 provider: Arc<dyn UpstreamOAuthProvider>,
109 managers: BTreeMap<String, Arc<dyn UpstreamOAuthManager>>,
110 ) -> Self {
111 Self {
112 provider,
113 managers: Arc::new(managers),
114 }
115 }
116
117 #[must_use]
118 pub fn provider(&self) -> Arc<dyn UpstreamOAuthProvider> {
119 Arc::clone(&self.provider)
120 }
121
122 #[must_use]
123 pub fn manager(&self, upstream: &str) -> Option<Arc<dyn UpstreamOAuthManager>> {
124 self.managers.get(upstream).cloned()
125 }
126
127 pub fn evict_subject(&self, upstream: &str, subject: &str) {
128 self.provider.evict_subject(upstream, subject);
129 }
130
131 pub fn evict_upstream(&self, upstream: &str) {
132 self.provider.evict_upstream(upstream);
133 }
134}
135
136#[cfg(test)]
137#[path = "oauth_tests.rs"]
138mod tests;