1use crate::redirect::{AuthorizationCode, RedirectListener};
2use crate::token::TokenResponse;
3use crate::{OAuthConfig, Pkce, TokenSet, random_token};
4use origin_domain::{AppError, Clock, Result};
5use origin_http::{HttpClient, HttpRequest};
6use origin_platform::Opener;
7use std::sync::Arc;
8
9#[derive(Debug)]
14pub struct PendingAuthorization {
15 pub authorization_url: String,
16 pub(crate) state: String,
17 pub(crate) pkce: Pkce,
18 pub(crate) redirect_uri: String,
19}
20
21#[derive(Debug, Clone)]
23pub struct AuthorizationFlow {
24 config: OAuthConfig,
25 http: Arc<dyn HttpClient>,
26 clock: Arc<dyn Clock>,
27}
28
29impl AuthorizationFlow {
30 pub fn new(config: OAuthConfig, http: Arc<dyn HttpClient>, clock: Arc<dyn Clock>) -> Self {
31 Self {
32 config,
33 http,
34 clock,
35 }
36 }
37
38 pub fn begin(&self, redirect_uri: impl Into<String>) -> Result<PendingAuthorization> {
42 let redirect_uri = redirect_uri.into();
43 let state = random_token(32)?;
44 let pkce = Pkce::generate()?;
45
46 let scope = self.config.scope_parameter();
47 let mut parameters: Vec<(&str, &str)> = vec![
48 ("response_type", "code"),
49 ("client_id", &self.config.client_id),
50 ("redirect_uri", &redirect_uri),
51 ("state", &state),
52 ("code_challenge", pkce.challenge()),
53 ("code_challenge_method", "S256"),
54 ];
55 if !scope.is_empty() {
56 parameters.push(("scope", &scope));
57 }
58 for (key, value) in &self.config.extra_authorization_params {
59 parameters.push((key.as_str(), value.as_str()));
60 }
61
62 let authorization_url = HttpRequest::get(&self.config.authorization_endpoint)
63 .query(¶meters)
64 .url;
65
66 Ok(PendingAuthorization {
67 authorization_url,
68 state,
69 pkce,
70 redirect_uri,
71 })
72 }
73
74 pub async fn exchange(
76 &self,
77 pending: &PendingAuthorization,
78 code: &AuthorizationCode,
79 ) -> Result<TokenSet> {
80 let mut fields: Vec<(&str, &str)> = vec![
81 ("grant_type", "authorization_code"),
82 ("code", code.as_str()),
83 ("redirect_uri", &pending.redirect_uri),
84 ("client_id", &self.config.client_id),
85 ("code_verifier", pending.pkce.verifier()),
86 ];
87 if let Some(secret) = &self.config.client_secret {
88 fields.push(("client_secret", secret.expose()));
89 }
90
91 self.post_token_request(&fields).await
92 }
93
94 pub async fn refresh(&self, refresh_token: &str) -> Result<TokenSet> {
96 let mut fields: Vec<(&str, &str)> = vec![
97 ("grant_type", "refresh_token"),
98 ("refresh_token", refresh_token),
99 ("client_id", &self.config.client_id),
100 ];
101 if let Some(secret) = &self.config.client_secret {
102 fields.push(("client_secret", secret.expose()));
103 }
104
105 self.post_token_request(&fields).await
106 }
107
108 pub async fn authorize(
110 &self,
111 listener: &dyn RedirectListener,
112 opener: &dyn Opener,
113 ) -> Result<TokenSet> {
114 let pending = self.begin(listener.redirect_uri())?;
115
116 tracing::info!("opening browser for authorization");
117 opener.open_url(&pending.authorization_url).await?;
118
119 let code = listener.wait(&pending.state).await?;
120 let tokens = self.exchange(&pending, &code).await?;
121
122 tracing::info!(scopes = ?tokens.scopes, "authorization complete");
123 Ok(tokens)
124 }
125
126 async fn post_token_request(&self, fields: &[(&str, &str)]) -> Result<TokenSet> {
127 let request = HttpRequest::post(&self.config.token_endpoint)
128 .header("accept", "application/json")
129 .form(fields);
130
131 let now = self.clock.now();
132 let response = self.http.send(request).await?;
133
134 if !response.is_success() {
135 return Err(token_endpoint_error(&response, now));
136 }
137
138 Ok(response.json::<TokenResponse>()?.into_token_set(now))
139 }
140}
141
142fn token_endpoint_error(
145 response: &origin_http::HttpResponse,
146 now: time::OffsetDateTime,
147) -> AppError {
148 #[derive(serde::Deserialize)]
149 struct OAuthError {
150 error: Option<String>,
151 error_description: Option<String>,
152 }
153
154 if let Ok(error) = response.json::<OAuthError>() {
155 let code = error.error.clone();
156 let message = error
157 .error_description
158 .or(error.error)
159 .unwrap_or_else(|| format!("token endpoint returned http {}", response.status));
160
161 return match code.as_deref() {
162 Some("invalid_grant") => AppError::Authentication(message),
163 Some("invalid_client" | "unauthorized_client" | "unsupported_grant_type") => {
164 AppError::Configuration(message)
165 }
166 Some("access_denied" | "invalid_scope") => AppError::Permission(message),
167 _ => AppError::ExternalService(message),
168 };
169 }
170
171 match response.clone().error_for_status(now).unwrap_err() {
172 AppError::Authentication(_) | AppError::Validation(_) => {
173 AppError::ExternalService(format!("token endpoint returned http {}", response.status))
174 }
175 error => error,
176 }
177}