1use crate::auth::{
2 error::AuthError, state::Identity, state::OAuth2State, state::OAuthToken, ErasedOAuthFlow,
3 OAuthProvider, UserMapper,
4};
5use crate::flow::{Flow, FlowContext, FlowResult};
6use async_trait::async_trait;
7
8pub struct OAuth2Flow<P: OAuthProvider, M: UserMapper = ()> {
10 provider: P,
11 mapper: Option<M>,
12 scopes: Vec<String>,
13 use_pkce: bool,
14}
15
16#[async_trait]
17impl<P: OAuthProvider + 'static, M: UserMapper + 'static> Flow for OAuth2Flow<P, M> {
18 fn id(&self) -> &str {
19 self.provider.provider_id()
20 }
21
22 async fn execute(&self, ctx: FlowContext) -> Result<FlowResult, AuthError> {
23 if let Some(_code) = ctx.params.get("code") {
24 let _received_state = ctx.params.get("state").ok_or(AuthError::CsrfMismatch)?;
25
26 Err(AuthError::Token(
30 "Direct Flow execution not updated for encrypted state".to_string(),
31 ))
32 } else {
33 let scopes_str = ctx.params.get("scopes").map(|s| s.as_str()).unwrap_or("");
35 let scopes_vec: Vec<&str> = if scopes_str.is_empty() {
36 Vec::new()
37 } else {
38 scopes_str.split(',').collect()
39 };
40
41 let pkce_challenge = ctx.params.get("pkce_challenge").map(|s| s.as_str());
42 let (url, _state) = self.initiate_login(&scopes_vec, pkce_challenge);
43 Ok(FlowResult::Redirect(url))
44 }
45 }
46}
47
48#[async_trait]
49impl<P: OAuthProvider + 'static, M: UserMapper + 'static> ErasedOAuthFlow for OAuth2Flow<P, M> {
50 fn provider_id(&self) -> String {
51 self.provider.provider_id().to_string()
52 }
53
54 fn initiate_login(
55 &self,
56 scopes: &[&str],
57 pkce_challenge: Option<&str>,
58 ) -> (String, OAuth2State) {
59 let effective_scopes = if !scopes.is_empty() {
60 scopes
61 } else {
62 &self
63 .scopes
64 .iter()
65 .map(|s| s.as_str())
66 .collect::<Vec<&str>>()
67 };
68
69 self.initiate_login(effective_scopes, pkce_challenge)
70 }
71
72 async fn finalize_login(
73 &self,
74 code: &str,
75 received_state: &str,
76 expected_state: &OAuth2State,
77 ) -> Result<(Identity, OAuthToken), AuthError> {
78 let (identity, token, _) = self
79 .finalize_login(code, received_state, expected_state)
80 .await?;
81 Ok((identity, token))
82 }
83}
84
85impl<P: OAuthProvider> OAuth2Flow<P, ()> {
86 pub fn new(provider: P) -> Self {
88 Self {
89 provider,
90 mapper: None,
91 scopes: Vec::new(),
92 use_pkce: true,
93 }
94 }
95}
96
97impl<P: OAuthProvider, M: UserMapper> OAuth2Flow<P, M> {
98 pub fn with_mapper(provider: P, mapper: M) -> Self {
100 Self {
101 provider,
102 mapper: Some(mapper),
103 scopes: Vec::new(),
104 use_pkce: true,
105 }
106 }
107
108 pub fn with_scopes(mut self, scopes: Vec<impl Into<String>>) -> Self {
110 self.scopes = scopes.into_iter().map(|s| s.into()).collect();
111 self
112 }
113
114 pub fn with_pkce(mut self, use_pkce: bool) -> Self {
116 self.use_pkce = use_pkce;
117 self
118 }
119
120 #[tracing::instrument(skip(self), fields(provider_id = %self.provider.provider_id()))]
122 pub fn initiate_login(
123 &self,
124 scopes: &[&str],
125 pkce_challenge: Option<&str>,
126 ) -> (String, OAuth2State) {
127 let state = uuid::Uuid::new_v4().to_string();
128 let nonce = Some(uuid::Uuid::new_v4().to_string());
129
130 let effective_scopes = if !scopes.is_empty() {
131 scopes
132 } else {
133 &self
134 .scopes
135 .iter()
136 .map(|s| s.as_str())
137 .collect::<Vec<&str>>()
138 };
139
140 tracing::debug!(scopes = ?effective_scopes, "generating authorization URL");
141
142 let url = self.provider.get_authorization_url(
143 &state,
144 effective_scopes,
145 pkce_challenge,
146 nonce.as_deref(),
147 );
148
149 let auth_state = OAuth2State {
150 state: state.clone(),
151 nonce,
152 code_verifier: None, success_url: None,
154 provider_id: self.provider.provider_id().to_string(),
155 expires_at: chrono::Utc::now().timestamp() + 600,
156 };
157
158 tracing::info!("authorization login initiated successfully");
159 (url, auth_state)
160 }
161
162 #[tracing::instrument(skip(self, code, expected_state), fields(provider_id = %self.provider.provider_id()))]
165 pub async fn finalize_login(
166 &self,
167 code: &str,
168 received_state: &str,
169 expected_state: &OAuth2State,
170 ) -> Result<(Identity, OAuthToken, Option<M::LocalUser>), AuthError> {
171 if received_state != expected_state.state {
172 tracing::error!("CSRF mismatch: received state does not match expected state");
173 return Err(AuthError::CsrfMismatch);
174 }
175
176 tracing::debug!("exchanging code for identity");
177 let (identity, token) = self
178 .provider
179 .exchange_code_for_identity(
180 code,
181 expected_state.code_verifier.as_deref(),
182 expected_state.nonce.as_deref(),
183 )
184 .await
185 .map_err(|e| {
186 tracing::error!(error = %e, "failed to exchange code for identity");
187 e
188 })?;
189
190 tracing::info!(user_id = %identity.external_id, "successfully retrieved identity from provider");
191
192 let local_user = if let Some(mapper) = &self.mapper {
195 tracing::debug!("mapping user identity");
196 Some(mapper.map_user(&identity).await.map_err(|e| {
197 tracing::error!(error = %e, "failed to map user");
198 e
199 })?)
200 } else {
201 None
202 };
203
204 Ok((identity, token, local_user))
205 }
206
207 pub async fn refresh_access_token(&self, refresh_token: &str) -> Result<OAuthToken, AuthError> {
209 self.provider.refresh_token(refresh_token).await
210 }
211
212 pub async fn revoke_token(&self, token: &str) -> Result<(), AuthError> {
214 self.provider.revoke_token(token).await
215 }
216}