• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

divviup / divviup-api / 13168098896

05 Feb 2025 10:55PM UTC coverage: 55.68% (-0.1%) from 55.794%
13168098896

push

github

web-flow
Bump oauth2 from 4.4.2 to 5.0.0 (#1532)

* Bump oauth2 from 4.4.2 to 5.0.0

Bumps [oauth2](https://github.com/ramosbugs/oauth2-rs) from 4.4.2 to 5.0.0.
- [Release notes](https://github.com/ramosbugs/oauth2-rs/releases)
- [Upgrade guide](https://github.com/ramosbugs/oauth2-rs/blob/main/UPGRADE.md)
- [Commits](https://github.com/ramosbugs/oauth2-rs/compare/4.4.2...5.0.0)

---
updated-dependencies:
- dependency-name: oauth2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix `oauth 0.5` breakage

Updates `divviup-api` to adapt to the breaking changes in `oauth 0.5`.
Much of this follows from advice in the [upgrade guide][1].

- move entire crate to `http 1.1.0`
    - use `http-compat-1` feature on `trillium-http`
- adopt builder pattern for constructing `oauth2::BasicClient`
    - add type alias `ConfiguredOauthClient` as a shorthand for
      `oauth2::BasicClient<...>`
- add wrapper around `trillium_client::Client` so we can implement
  `oauth2::AsyncHttpClient` on it
    - translate `oauth2::HttpRequest/Response` to/from Trillium
      equivalents

[1]: https://github.com/ramosbugs/oauth2-rs/blob/main/UPGRADE.md

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tim Geoghegan <timg@divviup.org>

6 of 27 new or added lines in 1 file covered. (22.22%)

3 existing lines in 1 file now uncovered.

3877 of 6963 relevant lines covered (55.68%)

85.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

24.81
/src/handler/oauth2.rs
1
use crate::{User, USER_SESSION_KEY};
2
use oauth2::{
3
    basic::{BasicClient, BasicErrorResponseType},
4
    AsyncHttpClient, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, EndpointNotSet,
5
    EndpointSet, HttpRequest, HttpResponse, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl,
6
    RequestTokenError, Scope, StandardErrorResponse, TokenResponse, TokenUrl,
7
};
8
use querystrong::QueryStrong;
9
use std::{future::Future, pin::Pin, sync::Arc};
10
use trillium::{conn_try, conn_unwrap, Conn, KnownHeaderName::Authorization, Status};
11
use trillium_client::{Client, ClientSerdeError};
12
use trillium_http::Headers;
13
use trillium_redirect::RedirectConnExt;
14
use trillium_sessions::SessionConnExt;
15
use url::Url;
16

17
/// Type alias for an oauth2::Client once we've finished configuring it in `OauthClient::new`.
18
/// Crate oauth's guide to upgrading to 0.5 recommends defining this kind of alias:
19
/// https://github.com/ramosbugs/oauth2-rs/blob/main/UPGRADE.md#add-typestate-generic-types-to-client
20
pub type ConfiguredOauthClient = BasicClient<
21
    EndpointSet,    // HasAuthURL
22
    EndpointNotSet, // HasDeviceAuthURL
23
    EndpointNotSet, // HasIntrospectionURL
24
    EndpointNotSet, // HasRevocationURL
25
    EndpointSet,    // HasTokenURL
26
>;
27

28
#[derive(Clone, Debug)]
29
pub struct Oauth2Config {
30
    pub authorize_url: Url,
31
    pub token_url: Url,
32
    pub client_id: String,
33
    pub client_secret: String,
34
    pub redirect_url: Url,
35
    pub base_url: Url,
36
    pub audience: String,
37
    pub http_client: Client,
38
}
39

40
pub async fn redirect(conn: Conn) -> Conn {
1✔
41
    let client: &OauthClient = conn.state().unwrap();
1✔
42

1✔
43
    let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
1✔
44

1✔
45
    let (auth_url, csrf_token) = client
1✔
46
        .oauth2_client()
1✔
47
        .authorize_url(CsrfToken::new_random)
1✔
48
        .add_scope(Scope::new(String::from("openid")))
1✔
49
        .add_scope(Scope::new(String::from("profile")))
1✔
50
        .add_scope(Scope::new(String::from("email")))
1✔
51
        .set_pkce_challenge(pkce_challenge)
1✔
52
        .url();
1✔
53

1✔
54
    conn.redirect(auth_url.to_string())
1✔
55
        .with_session("pkce_verifier", pkce_verifier)
1✔
56
        .with_session("csrf_token", csrf_token)
1✔
57
}
1✔
58

59
pub async fn callback(conn: Conn) -> Conn {
×
60
    let qs = QueryStrong::parse(conn.querystring()).unwrap_or_default();
×
61

62
    let Some(auth_code) = qs
×
63
        .get_str("code")
×
64
        .map(|c| AuthorizationCode::new(String::from(c)))
×
65
    else {
66
        return conn
×
67
            .with_body("expected code query param")
×
68
            .with_status(Status::Forbidden)
×
69
            .halt();
×
70
    };
71

72
    let Some(pkce_verifier) = conn.session().get("pkce_verifier") else {
×
73
        return conn
×
74
            .with_body("expected pkce verifier in session")
×
75
            .with_status(Status::Forbidden)
×
76
            .halt();
×
77
    };
78

79
    let session_csrf: Option<String> = conn.session().get("csrf_token");
×
80
    let qs_csrf = qs.get_str("state");
×
81

×
82
    if session_csrf.is_none() || qs_csrf != session_csrf.as_deref() {
×
83
        return conn
×
84
            .with_body("csrf mismatch or missing")
×
85
            .with_status(Status::Forbidden)
×
86
            .halt();
×
87
    }
×
88

89
    let client = conn_unwrap!(conn.state::<OauthClient>(), conn);
×
90
    let user = conn_try!(
×
91
        client
×
92
            .exchange_code_for_user(auth_code, pkce_verifier)
×
93
            .await,
×
94
        conn
×
95
    );
96

97
    conn.with_session(USER_SESSION_KEY, user)
×
98
}
×
99

100
#[derive(thiserror::Error, Debug)]
101
enum OauthError {
102
    #[error(transparent)]
103
    HttpError(#[from] trillium_client::Error),
104
    #[error(transparent)]
105
    InvalidStatusCode(#[from] oauth2::http::status::InvalidStatusCode),
106
    #[error(transparent)]
107
    HeaderConversionError(#[from] trillium_http::http_compat1::HeaderConversionError),
108
    #[error(transparent)]
109
    UrlError(#[from] url::ParseError),
110
    #[error("error response: {0}")]
111
    RequestTokenError(StandardErrorResponse<BasicErrorResponseType>),
112
    #[error(transparent)]
113
    Serde(#[from] serde_json::error::Error),
114
    #[error("Other error: {0}")]
115
    Other(String),
116
    #[error("expected a successful status, but found {0:?}")]
117
    UnexpectedStatus(Option<Status>),
118
    #[error(transparent)]
119
    HttpCrateError(#[from] oauth2::http::Error),
120
}
121

122
impl From<RequestTokenError<OauthError, StandardErrorResponse<BasicErrorResponseType>>>
123
    for OauthError
124
{
125
    fn from(
×
126
        value: RequestTokenError<OauthError, StandardErrorResponse<BasicErrorResponseType>>,
×
127
    ) -> Self {
×
128
        match value {
×
129
            RequestTokenError::ServerResponse(server_response) => {
×
130
                OauthError::RequestTokenError(server_response)
×
131
            }
132
            RequestTokenError::Request(e) => e,
×
133
            RequestTokenError::Parse(error, _path) => OauthError::Serde(error.into_inner()),
×
134
            RequestTokenError::Other(s) => OauthError::Other(s),
×
135
        }
136
    }
×
137
}
138

139
impl From<ClientSerdeError> for OauthError {
140
    fn from(value: ClientSerdeError) -> Self {
×
141
        match value {
×
142
            ClientSerdeError::HttpError(e) => OauthError::HttpError(e),
×
143
            ClientSerdeError::JsonError(e) => OauthError::Serde(e),
×
144
        }
145
    }
×
146
}
147

148
#[derive(Clone, Debug)]
149
pub struct OauthClient(Arc<OauthClientInner>);
150

151
#[derive(Debug)]
152
struct OauthClientInner {
153
    oauth_config: Oauth2Config,
154
    oauth2_client: ConfiguredOauthClient,
155
}
156

157
impl OauthClient {
158
    async fn exchange_code_for_user(
×
159
        &self,
×
160
        auth_code: AuthorizationCode,
×
161
        pkce_verifier: PkceCodeVerifier,
×
162
    ) -> Result<User, OauthError> {
×
163
        let http_client = self.http_client().clone();
×
164
        let exchange = self
×
165
            .oauth2_client()
×
166
            .exchange_code(auth_code)
×
167
            .set_pkce_verifier(pkce_verifier)
×
168
            .add_extra_param("audience", &self.0.oauth_config.audience)
×
NEW
169
            .request_async(&ClientWrapper(http_client))
×
170
            .await?;
×
171

172
        let mut client_conn = self
×
173
            .http_client()
×
174
            .get(self.0.oauth_config.base_url.join("/userinfo")?)
×
175
            .with_request_header(
×
176
                Authorization,
×
177
                format!("Bearer {}", exchange.access_token().secret()),
×
178
            )
×
179
            .await?;
×
180
        if !client_conn
×
181
            .status()
×
182
            .as_ref()
×
183
            .map(Status::is_success)
×
184
            .unwrap_or_default()
×
185
        {
186
            return Err(OauthError::UnexpectedStatus(client_conn.status()));
×
187
        }
×
188

×
189
        Ok(client_conn.response_json().await?)
×
190
    }
×
191

192
    pub fn new(config: &Oauth2Config) -> Self {
283✔
193
        let oauth2_client = BasicClient::new(ClientId::new(config.client_id.clone()))
283✔
194
            .set_client_secret(ClientSecret::new(config.client_secret.clone()))
283✔
195
            .set_auth_uri(AuthUrl::from_url(config.authorize_url.clone()))
283✔
196
            .set_token_uri(TokenUrl::from_url(config.token_url.clone()))
283✔
197
            .set_redirect_uri(RedirectUrl::from_url(config.redirect_url.clone()));
283✔
198

283✔
199
        Self(Arc::new(OauthClientInner {
283✔
200
            oauth_config: config.clone(),
283✔
201
            oauth2_client,
283✔
202
        }))
283✔
203
    }
283✔
204

205
    pub fn oauth2_client(&self) -> &ConfiguredOauthClient {
1✔
206
        &self.0.oauth2_client
1✔
207
    }
1✔
208

209
    pub fn http_client(&self) -> &Client {
×
210
        &self.0.oauth_config.http_client
×
211
    }
×
212
}
213

214
// Wraps a [`trillium_client::Client`] so we can implement [`oauth2::AsyncHttpClient`] on it, as
215
// otherwise the orphan rule would forbid this.
216
struct ClientWrapper(Client);
217

218
// Inspired by the impls `oauth2` provides for `reqwest::Client`
219
// https://github.com/ramosbugs/oauth2-rs/blob/23b952b23e6069525bc7e4c4f2c4924b8d28ce3a/src/reqwest.rs
220
impl<'c> AsyncHttpClient<'c> for ClientWrapper {
221
    type Error = OauthError;
222
    type Future = Pin<Box<dyn Future<Output = Result<HttpResponse, Self::Error>> + Send + 'c>>;
223

NEW
224
    fn call(&'c self, req: HttpRequest) -> Self::Future {
×
NEW
225
        Box::pin(async move {
×
226
            // Translate the oauth2::http::Request into a Trillium request
NEW
227
            let mut conn = self
×
NEW
228
                .0
×
NEW
229
                .build_conn(req.method(), req.uri().to_string().parse::<Url>()?)
×
NEW
230
                .with_body(req.body().clone())
×
NEW
231
                .with_request_headers(Headers::from(req.headers().clone()))
×
NEW
232
                .await?;
×
NEW
233
            let status_code: oauth2::http::StatusCode = conn.status().unwrap().try_into()?;
×
NEW
234
            let body = conn.response_body().read_bytes().await?;
×
235

236
            // Now transform the Trillium response back into an http::Response
NEW
237
            let mut builder = oauth2::http::Response::builder().status(status_code);
×
NEW
238
            let http_headers: oauth2::http::HeaderMap =
×
NEW
239
                conn.response_headers().clone().try_into()?;
×
NEW
240
            builder
×
NEW
241
                .headers_mut()
×
NEW
242
                .ok_or_else(|| OauthError::Other("no headers in builder?".into()))?
×
NEW
243
                .extend(http_headers);
×
NEW
244
            Ok::<_, OauthError>(builder.body(body)?)
×
NEW
245
        })
×
NEW
246
    }
×
247
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc