blob: 63dca5c8dcac78eb7f091df63b0e44f4dfa38deb [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Provide OAuth2 support for Google Cloud Storage (GCS) access.
//!
//! There are two main APIs here:
//! - new_refresh_token() gets a long-lived, storable (to disk) token than can
//! be used to create new access tokens.
//! - new_access_token() accepts a refresh token and returns a reusable though
//! short-lived token that can be used to access data.
//!
//! Caution: Some data handled here are security access keys (tokens) and must
//! not be logged (or traced) or otherwise put someplace where the
//! secrecy could be compromised. I.e. watch out when adding/reviewing
//! log::*, tracing::*, or `impl` of Display or Debug.
pub mod device;
pub mod info;
pub mod pkce;
use {
crate::error::GcsError,
anyhow::Result,
hyper::{Body, Method, Request},
info::{CLIENT_ID, CLIENT_SECRET, OAUTH_REFRESH_TOKEN_ENDPOINT},
serde::{Deserialize, Serialize},
std::fmt,
};
#[derive(Clone, PartialEq)]
pub struct GcsCredentials {
/// The OAuth2 client which requested data access on behalf of the user.
pub client_id: String,
/// The client "secret" for desktop applications are not actually secret.
/// In this case it acts like additional information for the client_id.
pub client_secret: String,
/// A long-lived token which is used to mint access tokens. This is private
/// to the user and must not be printed to a log or otherwise leaked.
pub refresh_token: String,
}
/// Custom debug to avoid printing the refresh_token.
impl fmt::Debug for GcsCredentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"GcsCredentials client_id: {:?}, client_secret: {:?}, refresh_token: <hidden>",
self.client_id, self.client_secret
)
}
}
impl GcsCredentials {
pub fn new(refresh_token: &str) -> Self {
Self {
client_id: CLIENT_ID.to_string(),
client_secret: CLIENT_SECRET.to_string(),
refresh_token: refresh_token.to_string(),
}
}
}
/// Response body from [`OAUTH_REFRESH_TOKEN_ENDPOINT`].
/// 'expires_in' is intentionally omitted.
#[derive(Deserialize)]
struct OauthTokenResponse {
/// A limited time (see `expires_in`) token used in an Authorization header.
access_token: String,
}
/// POST body to [`OAUTH_REFRESH_TOKEN_ENDPOINT`].
#[derive(Serialize)]
struct RefreshTokenRequest<'a> {
/// A value provided by GCS for fetching tokens.
client_id: &'a str,
/// A (normally secret) value provided by GCS for fetching tokens.
client_secret: &'a str,
/// A long lasting secret token used to attain a new `access_token`.
refresh_token: &'a str,
/// Will be "refresh_token" for a refresh token.
grant_type: &'a str,
}
/// Use the 'credentials' to request an access token.
///
/// Access tokens are short-lived. Unlike a refresh token, there's little value
/// in storing an access token to disk for later use, though it may be used many
/// times before needing to get a new access_token.
pub async fn new_access_token(credentials: &GcsCredentials) -> Result<String, GcsError> {
tracing::debug!("new_access_token");
let req_body = RefreshTokenRequest {
client_id: &credentials.client_id,
client_secret: &credentials.client_secret,
refresh_token: &credentials.refresh_token,
grant_type: "refresh_token",
};
let body = serde_json::to_vec(&req_body)?;
let req = Request::builder()
.method(Method::POST)
.uri(OAUTH_REFRESH_TOKEN_ENDPOINT)
.body(Body::from(body))?;
let https_client = fuchsia_hyper::new_https_client();
let res = https_client.request(req).await?;
if res.status().is_success() {
let bytes = hyper::body::to_bytes(res.into_body()).await?;
let info: OauthTokenResponse = serde_json::from_slice(&bytes)?;
Ok(info.access_token)
} else {
match res.status() {
http::StatusCode::BAD_REQUEST => return Err(GcsError::NeedNewRefreshToken),
_ => return Err(GcsError::HttpResponseError(res.status())),
}
}
}