Skip to main content

ocre/runtime/
oauth.rs

1use wasm_bindgen::JsValue;
2use worker::{Fetch, Headers, Method, Request, RequestInit, send::SendFuture};
3
4use crate::{
5    Ctx, Error, Result,
6    oauth::{GITHUB, Profile, Provider, parse_profile, parse_token_response, token_request_body},
7};
8
9/// GitHub's API refuses requests without a `User-Agent`.
10const USER_AGENT: &str = "ocre (https://github.com/tgeselle/ocre.rs)";
11
12/// Trades the authorization `code` from the callback for an access token (one subrequest).
13///
14/// Sends the client id and secret from the provider's Worker secrets
15/// ([`Provider::client_id_secret`], [`Provider::client_secret_secret`]),
16/// the same `redirect_uri` as the authorization URL, and the PKCE
17/// `verifier` kept in the session.
18///
19/// # Errors
20///
21/// - [`Error::Unauthorized`] when the provider refuses the code (expired,
22///   used twice, wrong verifier); the reason is logged.
23/// - [`Error::Internal`] when a secret is missing (the message names it) or
24///   the provider cannot be reached.
25///
26/// # Examples
27///
28/// ```no_run
29/// use ocre::{Ctx, Result, oauth::{GITHUB, exchange_code, profile}};
30///
31/// async fn callback(ctx: &Ctx, code: &str, verifier: &str) -> Result<String> {
32///     let token = exchange_code(ctx, &GITHUB, "https://app.example.com/auth/github/callback", code, verifier).await?;
33///     Ok(profile(&GITHUB, &token).await?.uid)
34/// }
35/// # let _ = callback;
36/// ```
37pub fn exchange_code(
38    ctx: &Ctx,
39    provider: &'static Provider,
40    redirect_uri: &str,
41    code: &str,
42    verifier: &str,
43) -> impl Future<Output = Result<String>> + Send + use<> {
44    let env = ctx.env().clone();
45    let (redirect_uri, code, verifier) = (redirect_uri.to_owned(), code.to_owned(), verifier.to_owned());
46    SendFuture::new(async move {
47        let client_id = super::secrets::require(&env, provider.client_id_secret).await?;
48        let client_secret = super::secrets::require(&env, provider.client_secret_secret).await?;
49        let body = token_request_body(&client_id, &client_secret, &redirect_uri, &code, &verifier);
50        let headers = Headers::new();
51        headers.set("Accept", "application/json")?;
52        headers.set("Content-Type", "application/x-www-form-urlencoded")?;
53        let mut init = RequestInit::new();
54        init.with_method(Method::Post).with_headers(headers).with_body(Some(JsValue::from_str(&body)));
55        let mut response = Fetch::Request(Request::new_with_init(provider.token_url, &init)?).send().await?;
56        let status = response.status_code();
57        parse_token_response(provider, status, &response.text().await?)
58    })
59}
60
61/// Reads the signed-in user's [`Profile`] with an access token (one subrequest; two for GitHub).
62///
63/// The email is kept only when the provider marks it verified (GitHub: the
64/// primary verified address of `/user/emails`; Google: `email_verified`).
65///
66/// # Errors
67///
68/// [`Error::Internal`] when the provider cannot be reached or answers
69/// something unexpected (a revoked token included).
70///
71/// # Examples
72///
73/// ```no_run
74/// use ocre::{Result, oauth::{GOOGLE, profile}};
75///
76/// async fn email(token: &str) -> Result<Option<String>> {
77///     Ok(profile(&GOOGLE, token).await?.email)
78/// }
79/// # let _ = email;
80/// ```
81pub fn profile(
82    provider: &'static Provider,
83    access_token: &str,
84) -> impl Future<Output = Result<Profile>> + Send + use<> {
85    let access_token = access_token.to_owned();
86    SendFuture::new(async move {
87        let user = get_json(provider, provider.userinfo_url, &access_token).await?;
88        let emails = if provider == &GITHUB {
89            Some(get_json(provider, "https://api.github.com/user/emails", &access_token).await?)
90        } else {
91            None
92        };
93        parse_profile(provider, &user, emails.as_deref())
94    })
95}
96
97async fn get_json(provider: &Provider, url: &str, access_token: &str) -> Result<String> {
98    let headers = Headers::new();
99    headers.set("Authorization", &format!("Bearer {access_token}"))?;
100    headers.set("Accept", "application/json")?;
101    headers.set("User-Agent", USER_AGENT)?;
102    let mut init = RequestInit::new();
103    init.with_method(Method::Get).with_headers(headers);
104    let mut response = Fetch::Request(Request::new_with_init(url, &init)?).send().await?;
105    let (status, body) = (response.status_code(), response.text().await?);
106    if (200..300).contains(&status) {
107        Ok(body)
108    } else {
109        Err(Error::internal(format!("{} answered {status} for {url}: {body}", provider.name)))
110    }
111}