Skip to content

Commit

Permalink
Merge pull request #45 from go-bazzinga/9-add-login-page
Browse files Browse the repository at this point in the history
Storing keypair in cloudflare kv
  • Loading branch information
rosarp-gobazzinga authored Jan 31, 2024
2 parents fdb56d1 + 43adf4d commit 8af7e6a
Show file tree
Hide file tree
Showing 21 changed files with 270 additions and 108 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib", "staticlib"]
[dependencies]
axum = { version = "0.7", optional = true, features = ["http2", "macros"] }
axum-extra = { version = "0.9", features = ["cookie", "cookie-signed", "cookie-private"] }
base64 = "0.21"
bip32 = { version = "0.5", optional = true }
cfg-if = "1.0"
chrono = { version = "0.4", optional = true }
Expand Down
2 changes: 1 addition & 1 deletion cloudflare-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ reqwest = { version = "0.11", default-features = false, features = ["json", "mul
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"
serde_with = { version = "3.4", features = ["base64"] }
serde_with = { version = "3.5", features = ["base64"] }
tracing = "0.1"
url = "2.5"
4 changes: 0 additions & 4 deletions cloudflare-api/src/connect/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,3 @@ impl Credentials {
}
}
}

pub trait AuthClient {
fn client(&mut self, credentials: &Credentials) -> &Self;
}
62 changes: 34 additions & 28 deletions cloudflare-api/src/connect/client.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,53 @@
use crate::connect::{AuthClient, Credentials};
use super::EndPoint;
use crate::connect::Credentials;
use reqwest::{header, Client, Error, Method, RequestBuilder};
use serde::Deserialize;
use tracing::{instrument::WithSubscriber, log::info};

use super::EndPoint;

#[derive(Debug, Clone)]
pub struct HttpApiClient {
client: Client,
}

impl HttpApiClient {
pub fn new(client: Client) -> HttpApiClient {
pub fn new(credentials: &Credentials) -> HttpApiClient {
let mut headers = header::HeaderMap::new();
for header in credentials.headers() {
let mut auth_header = header::HeaderValue::from_str(&header.1).unwrap();
auth_header.set_sensitive(true);
headers.insert(header.0, auth_header);
}
headers.insert(
header::ACCEPT,
header::HeaderValue::from_static("application/json"),
);
// TODO: Replace expect() and handle error gracefully
let client = reqwest::Client::builder()
.default_headers(headers)
.build()
.expect("Could not initialize connection with Cloudflare V4 API!");
HttpApiClient { client }
}

pub async fn send<T>(&self, end_point: impl EndPoint<T>) -> Result<T, Error>
where
for<'de> T: Deserialize<'de>,
{
let mut request_builder = self
.request_builder(end_point.url().as_str(), end_point.method())
.header(header::CONTENT_TYPE, end_point.content_type().as_ref());
let mut request_builder =
self.request_builder(end_point.url().as_str(), end_point.method());
if end_point.multipart().is_some() {
request_builder = request_builder.multipart(end_point.multipart().unwrap());
} else {
request_builder =
request_builder.header(header::CONTENT_TYPE, end_point.content_type().as_ref());
}
if end_point.body().is_some() {
request_builder = request_builder.body(end_point.body().unwrap());
}

let body = request_builder.send().await?.json::<T>().await?;
info!("builder: {:?}", request_builder);
let response = request_builder.send().await?;
info!("response: {:?}", response);
let body = response.json::<T>().await?;
Ok(body)
}

Expand All @@ -42,23 +62,9 @@ impl HttpApiClient {
}
}

impl AuthClient for HttpApiClient {
fn client(&mut self, credentials: &Credentials) -> &Self {
let mut headers = header::HeaderMap::new();
for header in credentials.headers() {
let mut auth_header = header::HeaderValue::from_str(&header.1).unwrap();
auth_header.set_sensitive(true);
headers.insert(header.0, auth_header);
}
headers.insert(
header::ACCEPT,
header::HeaderValue::from_static("application/json"),
);
// TODO: Replace expect() and handle error gracefully
self.client = reqwest::Client::builder()
.default_headers(headers)
.build()
.expect("Could not initialize connection with Cloudflare V4 API!");
self
}
#[derive(Debug, Clone)]
pub struct ApiClientConfig {
pub account_identifier: String,
pub namespace_identifier: String,
pub cloudflare_client: HttpApiClient,
}
4 changes: 2 additions & 2 deletions cloudflare-api/src/connect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ mod auth;
mod client;
mod spec;

pub use self::auth::{AuthClient, Credentials};
pub use self::client::HttpApiClient;
pub use self::auth::Credentials;
pub use self::client::{ApiClientConfig, HttpApiClient};
pub use self::spec::EndPoint;
5 changes: 3 additions & 2 deletions cloudflare-api/src/connect/spec.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use reqwest::multipart::Form;
use std::{borrow::Cow, collections::HashMap};
use tracing::log::info;
use url::Url;

pub trait EndPoint<T> {
Expand Down Expand Up @@ -38,9 +39,9 @@ pub trait EndPoint<T> {

// TODO: make URL configurable
fn url(&self) -> String {
let mut url = Url::parse("https://api.cloudflare.com/client/v4")
let mut url = Url::parse("https://api.cloudflare.com")
.unwrap()
.join(serde_urlencoded::to_string(self.path()).as_deref().unwrap())
.join(&format!("/client/v4{}", &self.path()))
.unwrap();
url.set_query(self.serialize_query().as_deref());
url.to_string()
Expand Down
20 changes: 10 additions & 10 deletions cloudflare-api/src/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ pub mod storage_kv;

use serde::Deserialize;

#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
pub struct CloudflareResponse<T> {
errors: Vec<Info>,
messages: Vec<Info>,
result: T,
result_info: Option<String>,
success: bool,
pub errors: Vec<Info>,
pub messages: Vec<Info>,
pub result: Option<T>,
pub result_info: Option<String>,
pub success: bool,
}

#[derive(Deserialize)]
struct Info {
code: u32,
message: String,
#[derive(Debug, Deserialize)]
pub struct Info {
pub code: u32,
pub message: String,
}
6 changes: 3 additions & 3 deletions cloudflare-api/src/endpoints/storage_kv/delete_kv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use reqwest::Method;

// https://developers.cloudflare.com/api/operations/workers-kv-namespace-delete-key-value-pair
pub struct DeleteKV<'a> {
account_identifier: &'a str,
namespace_identifier: &'a str,
key_name: &'a str,
pub account_identifier: &'a str,
pub namespace_identifier: &'a str,
pub key_name: &'a str,
}

impl<'a> EndPoint<CloudflareResponse<String>> for DeleteKV<'a> {
Expand Down
2 changes: 2 additions & 0 deletions cloudflare-api/src/endpoints/storage_kv/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod delete_kv;
mod read_kv;
mod read_metadata;
mod write_kv_with_metadata;

pub use self::delete_kv::DeleteKV;
pub use self::read_kv::ReadKV;
pub use self::read_metadata::ReadMetadata;
pub use self::write_kv_with_metadata::WriteKVWithMetadata;
7 changes: 4 additions & 3 deletions cloudflare-api/src/endpoints/storage_kv/read_kv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ use crate::connect::EndPoint;
use reqwest::Method;

// https://developers.cloudflare.com/api/operations/workers-kv-namespace-read-key-value-pair
#[derive(Debug)]
pub struct ReadKV<'a> {
account_identifier: &'a str,
namespace_identifier: &'a str,
key_name: &'a str,
pub account_identifier: &'a str,
pub namespace_identifier: &'a str,
pub key_name: &'a str,
}

impl<'a> EndPoint<String> for ReadKV<'a> {
Expand Down
24 changes: 24 additions & 0 deletions cloudflare-api/src/endpoints/storage_kv/read_metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::{connect::EndPoint, endpoints::CloudflareResponse};
use reqwest::Method;
use std::collections::HashMap;

// https://developers.cloudflare.com/api/operations/workers-kv-namespace-read-the-metadata-for-a-key
#[derive(Debug)]
pub struct ReadMetadata<'a> {
pub account_identifier: &'a str,
pub namespace_identifier: &'a str,
pub key_name: &'a str,
}

impl<'a> EndPoint<CloudflareResponse<HashMap<String, String>>> for ReadMetadata<'a> {
fn method(&self) -> Method {
Method::GET
}

fn path(&self) -> String {
format!(
"/accounts/{}/storage/kv/namespaces/{}/metadata/{}",
self.account_identifier, self.namespace_identifier, self.key_name,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use crate::{connect::EndPoint, endpoints::CloudflareResponse};
use reqwest::multipart::Form;
use std::{borrow::Cow, collections::HashMap};
use tracing::log::info;

// https://developers.cloudflare.com/api/operations/workers-kv-namespace-write-key-value-pair-with-metadata
#[derive(Debug)]
pub struct WriteKVWithMetadata<'a> {
account_identifier: &'a str,
namespace_identifier: &'a str,
key_name: &'a str,
metadata: HashMap<&'a str, &'a str>,
value: &'a str,
pub account_identifier: &'a str,
pub namespace_identifier: &'a str,
pub key_name: &'a str,
pub value: &'a str,
pub metadata: HashMap<&'a str, &'a str>,
}

impl<'a> EndPoint<CloudflareResponse<String>> for WriteKVWithMetadata<'a> {
Expand Down
69 changes: 45 additions & 24 deletions src/auth/identity.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
use super::agent_js;
use super::generate;
use axum_extra::extract::cookie::{Cookie, Key, SignedCookieJar};
use leptos::*;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc};

use super::{agent_js, generate};
use crate::store::cloudflare::{read_kv, read_metadata, write_kv};
use axum::{extract::FromRef, http::header, response::IntoResponse};
use axum_extra::extract::cookie::{Cookie, Key, SignedCookieJar};
use base64::{engine::general_purpose, Engine as _};
use chrono::{Duration, Utc};
use ic_agent::{
identity::{DelegatedIdentity, Delegation, Secp256k1Identity, SignedDelegation},
Identity,
};
use leptos::*;
use leptos_axum::ResponseOptions;
use leptos_router::RouteListing;
use tokio::sync::RwLock;
use std::collections::HashMap;
use tracing::log::info;

#[server(endpoint = "generate_session")]
pub async fn generate_session() -> Result<agent_js::SessionResponse, ServerFnError> {
let identity_keeper: IdentityKeeper = use_context::<IdentityKeeper>().unwrap();
let app_state: AppState = use_context::<AppState>().unwrap();
let mut jar =
leptos_axum::extract_with_state::<SignedCookieJar<Key>, IdentityKeeper>(&identity_keeper)
.await?;
leptos_axum::extract_with_state::<SignedCookieJar<Key>, AppState>(&app_state).await?;

let user_identity: Option<String> = match jar.get("user_identity") {
Some(val) => Some(val.value().to_owned()),
Expand All @@ -33,18 +30,41 @@ pub async fn generate_session() -> Result<agent_js::SessionResponse, ServerFnErr
let user_key_pair: Option<generate::KeyPair> = if user_identity.is_none() {
None
} else {
let read_access = identity_keeper.oauth_map.read().await;
read_access.get(&user_identity.unwrap()).cloned()
let public_key = user_identity.unwrap();
let private_key = read_kv(&public_key, &app_state.cloudflare_config)
.await
.unwrap();
let private_key = general_purpose::STANDARD_NO_PAD
.decode(private_key)
.unwrap();
let metadata: HashMap<String, String> =
read_metadata(&public_key, &app_state.cloudflare_config)
.await
.unwrap();
let private_pem = metadata.get("private_pem").unwrap();
Some(generate::KeyPair {
public_key,
private_key,
private_pem: private_pem.to_owned(),
})
};
let user_key_pair = match user_key_pair {
Some(kp) => kp,
None => {
let new_key_pair = generate::key_pair().unwrap();
{
let write_access = identity_keeper.oauth_map.write();
write_access
.await
.insert(new_key_pair.public_key.to_owned(), new_key_pair.clone());
let private_key =
general_purpose::STANDARD_NO_PAD.encode(&new_key_pair.private_key);
let mut metadata = HashMap::new();
metadata.insert("private_pem", new_key_pair.private_pem.as_str());
let _ = write_kv(
&new_key_pair.public_key,
&private_key,
metadata,
app_state.cloudflare_config,
)
.await
.unwrap();
}
new_key_pair
}
Expand Down Expand Up @@ -142,31 +162,32 @@ pub async fn generate_session() -> Result<agent_js::SessionResponse, ServerFnErr
}

// pub fn authenticate(
// identity_keeper: State<Arc<RwLock<IdentityKeeper>>>,
// app_state: State<Arc<RwLock<AppState>>>,
// user_oauth_id: String,
// user_identity: String,
// ) -> Json<SessionResponse> {
// }

#[derive(Clone)]
pub struct IdentityKeeper {
pub struct AppState {
pub leptos_options: LeptosOptions,
pub routes: Vec<RouteListing>,
pub oauth_map: Arc<RwLock<HashMap<String, generate::KeyPair>>>,
// pub oauth_map: Arc<RwLock<HashMap<String, generate::KeyPair>>>,
pub key: Key,
pub oauth2_client: oauth2::basic::BasicClient,
pub reqwest_client: reqwest::Client,
pub auth_cookie_domain: String,
pub cloudflare_config: cloudflare_api::connect::ApiClientConfig,
}

impl FromRef<IdentityKeeper> for Key {
fn from_ref(state: &IdentityKeeper) -> Self {
impl FromRef<AppState> for Key {
fn from_ref(state: &AppState) -> Self {
state.key.clone()
}
}

impl FromRef<IdentityKeeper> for LeptosOptions {
fn from_ref(state: &IdentityKeeper) -> Self {
impl FromRef<AppState> for LeptosOptions {
fn from_ref(state: &AppState) -> Self {
state.leptos_options.clone()
}
}
Loading

0 comments on commit 8af7e6a

Please sign in to comment.