Skip to main content

origin_auth/
flow.rs

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/// A flow that has been started but not yet completed.
10///
11/// Holds the two values that must survive until the code comes back: the `state` used
12/// to recognise our own redirect, and the PKCE verifier that proves we started it.
13#[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/// The OAuth 2.0 authorization code flow with PKCE.
22#[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    /// Build the URL to send the user to.
39    ///
40    /// `redirect_uri` comes from an already-listening [`RedirectListener`].
41    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(&parameters)
64            .url;
65
66        Ok(PendingAuthorization {
67            authorization_url,
68            state,
69            pkce,
70            redirect_uri,
71        })
72    }
73
74    /// Trade the authorization code for tokens.
75    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    /// Exchange a refresh token for a new access token.
95    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    /// Run the whole flow: open the browser, wait for the redirect, exchange the code.
109    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
142/// Only `invalid_grant` proves that the stored authorization is no longer usable.
143/// Provider outages and malformed error responses must not log the user out.
144fn 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}