diff --git a/.github/workflows/deploy-to-production-on-merge-to-main.yaml b/.github/workflows/deploy-to-production-on-merge-to-main.yaml index b2717b0..b468ac5 100644 --- a/.github/workflows/deploy-to-production-on-merge-to-main.yaml +++ b/.github/workflows/deploy-to-production-on-merge-to-main.yaml @@ -37,4 +37,10 @@ jobs: - name: Deploy a docker container to Fly.io env: FLY_API_TOKEN: ${{ secrets.HOT_OR_NOT_AUTH_FLY_IO_GITHUB_ACTION }} - run: flyctl deploy --remote-only + AUTH_SIGN_KEY: ${{ secrets.AUTH_SESSION_COOKIE_SIGNING_SECRET_KEY }} + CLOUDFLARE_ACCOUNT_IDENTIFIER: ${{ secrets.CLOUDFLARE_WORKERS_KV_ACCOUNT_ID }} + CLOUDFLARE_NAMESPACE_IDENTIFIER: ${{ secrets.CLOUDFLARE_WORKERS_KV_NAMESPACE_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_WORKERS_KV_API_SECRET }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_SIGNING_OAUTH_CLIENT_CREDENTIAL_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_SIGNING_OAUTH_CLIENT_CREDENTIAL_CLIENT_SECRET }} + run: flyctl deploy --remote-only diff --git a/AuthConfig.toml b/AuthConfig.toml index 9522489..25287cb 100644 --- a/AuthConfig.toml +++ b/AuthConfig.toml @@ -1,5 +1,5 @@ # auth_ic_url = "https://ic0.app" -auth_ic_url = "http://0.0.0.0:4943" +auth_ic_url = "http://127.0.0.1:4943" auth_sign_key = "" # cloudflare_configs diff --git a/Cargo.lock b/Cargo.lock index 2b87b8b..ad86a85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.7" @@ -584,6 +619,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "cloudflare-api" version = "0.4.0" @@ -678,6 +723,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" dependencies = [ + "aes-gcm", "base64 0.21.7", "hmac", "percent-encoding", @@ -741,9 +787,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1143,6 +1199,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1357,8 +1423,10 @@ dependencies = [ "log", "oauth2", "rand", + "reqwest", "sec1", "serde", + "serde_json", "thiserror", "tiny-bip39", "tokio", @@ -1687,6 +1755,15 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -2388,6 +2465,18 @@ dependencies = [ "spki", ] +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3696,6 +3785,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 710b43b..0e8bfec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,11 @@ version = "0.1.0" edition = "2021" [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib", "rlib", "staticlib"] [dependencies] axum = { version = "0.7", optional = true, features = ["http2", "macros"] } -axum-extra = { version = "0.9", features = ["cookie", "cookie-signed"] } +axum-extra = { version = "0.9", features = ["cookie", "cookie-signed", "cookie-private"] } bip32 = { version = "0.5", optional = true } cfg-if = "1.0" chrono = { version = "0.4", optional = true } @@ -26,8 +26,10 @@ leptos_router = { version = "0.6.0-beta", features = ["nightly"], git = "https:/ log = "0.4" oauth2 = "4.4" rand = { version = "0.8", optional = true } +reqwest = { version = "0.11", optional = true, default-features = false, features = ["json", "rustls"] } sec1 = { version = "0.7", optional = true } serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", optional = true } thiserror = "1.0" tiny-bip39 = { version = "1.0", optional = true } tokio = { version = "1.35", optional = true, features = ["rt-multi-thread", "macros"] } @@ -49,7 +51,9 @@ ssr = [ "dep:k256", "dep:leptos_axum", "dep:rand", + "dep:reqwest", "dep:sec1", + "dep:serde_json", "dep:tiny-bip39", "dep:tokio", "dep:tower", diff --git a/src/init.rs b/src/init.rs index b5d4bb2..d2bc66d 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,4 +1,5 @@ use cfg_if::cfg_if; +use serde::Deserialize; cfg_if! { if #[cfg(feature = "ssr")] { extern crate tracing; @@ -7,7 +8,6 @@ use figment::{ providers::{Env, Format, Toml}, Figment, }; -use serde::Deserialize; }} #[cfg(feature = "ssr")] diff --git a/src/providers/google.rs b/src/providers/google.rs index 08d6e59..1138ecf 100644 --- a/src/providers/google.rs +++ b/src/providers/google.rs @@ -1,58 +1,190 @@ +use cfg_if::cfg_if; +use leptos::SignalGet; use leptos::*; +use leptos_router::{use_query, NavigateOptions, Params}; +use oauth2::TokenResponse; -#[server(endpoint = "google_login")] -async fn google_auth_url() -> Result { - use crate::auth::identity::IdentityKeeper; - use oauth2::{CsrfToken, PkceCodeChallenge, Scope}; - use tracing::log::info; +cfg_if! { +if #[cfg(feature="ssr")] { +use axum::{http::header, response::IntoResponse}; +use axum_extra::extract::cookie::{Cookie, Key, PrivateCookieJar}; +use crate::auth::identity::IdentityKeeper; +use leptos_axum::ResponseOptions; +use oauth2::{reqwest::{http_client}, AuthorizationCode, CsrfToken, PkceCodeVerifier, PkceCodeChallenge, Scope}; +use tracing::log::info; +} +} - info!("Google Login"); - let client = use_context::() - .ok_or_else(|| ServerFnError::new("Context not found!"))? - .oauth2_client; +#[server] +async fn google_auth_url() -> Result { + let identity_keeper = + use_context::().ok_or_else(|| ServerFnError::new("Context not found!"))?; + let mut jar: PrivateCookieJar = + leptos_axum::extract_with_state::, IdentityKeeper, ServerFnErrorErr>( + &identity_keeper, + ) + .await?; + let client = identity_keeper.oauth2_client; // Generate a PKCE challenge. - let (pkce_challenge, _pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); // Generate the full authorization URL. - let (auth_url, _csrf_token) = client + let (auth_url, csrf_token) = client .authorize_url(CsrfToken::new_random) // Set the desired scopes. - // .add_scope(Scope::new("read".to_string())) .add_scope(Scope::new("openid".to_string())) .add_scope(Scope::new("email".to_string())) // Set the PKCE code challenge. .set_pkce_challenge(pkce_challenge) .url(); - leptos_axum::redirect(auth_url.as_str()); + + let pkce_verifier = pkce_verifier.secret(); + let csrf_token = csrf_token.secret(); + + info!("b4 pkce sec: {}", pkce_verifier); + info!("b4 csrf sec: {}", csrf_token); + + let mut pkce_verifier = Cookie::new("pkce_verifier", pkce_verifier.to_owned()); + pkce_verifier.set_domain("hot-or-not-web-leptos-ssr.fly.dev"); + pkce_verifier.set_http_only(true); + jar = jar.add(pkce_verifier.clone()); + let mut csrf_token = Cookie::new("csrf_token", csrf_token.to_owned()); + csrf_token.set_domain("hot-or-not-web-leptos-ssr.fly.dev"); + csrf_token.set_http_only(true); + jar = jar.add(csrf_token.clone()); + + let jar_into_response = jar.into_response(); + + let response = expect_context::(); + for header_value in jar_into_response.headers().get_all(header::SET_COOKIE) { + response.append_header(header::SET_COOKIE, header_value.clone()); + } + Ok(auth_url.to_string()) } #[component] pub fn Login() -> impl IntoView { - // let oauth2_url = Action::::server(); - // oauth2_url.dispatch(GoogleAuthUrl {}); - // leptos::logging::log!("dispatched!"); - // create_effect(move |_| { - // if let Some(Ok(redirect)) = oauth2_url.value().get() { - // // let navigate = leptos_router::use_navigate(); - // // navigate(&redirect, Default::default()); - // leptos::logging::log!("navigated! {}", redirect); - // // window().location().set_href(&redirect).unwrap(); - // } - // }); + let g_auth = Action::::server(); + g_auth.dispatch(GoogleAuthUrl {}); + + create_effect(move |_| { + if let Some(Ok(redirect)) = g_auth.value().get() { + window().location().set_href(&redirect).unwrap(); + } + }); view! { - +
+
} } +#[server] +async fn google_verify_response( + provided_csrf: String, + code: String, +) -> Result<(String, u64), ServerFnError> { + let identity_keeper = + use_context::().ok_or_else(|| ServerFnError::new("Context not found!"))?; + let mut jar: PrivateCookieJar = + leptos_axum::extract_with_state::, IdentityKeeper, ServerFnErrorErr>( + &identity_keeper, + ) + .await?; + let client = identity_keeper.oauth2_client; + let csrf_token: Option = match jar.get("csrf_token") { + Some(val) => Some(val.value().to_owned()), + None => None, + }; + match csrf_token.clone() { + Some(csrf) => { + if !csrf.eq(&provided_csrf) { + return Err(ServerFnError::new("Invalid CSRF token!")); + } + } + None => return Err(ServerFnError::new("No CSRF token!")), + } + let pkce_verifier: Option = match jar.get("pkce_verifier") { + Some(val) => Some(val.value().to_owned()), + None => None, + }; + info!("aftr pkce sec: {}", pkce_verifier.clone().unwrap()); + info!("aftr csrf sec: {}", csrf_token.clone().unwrap()); + + let pkce_verifier = PkceCodeVerifier::new(pkce_verifier.unwrap()); + let token_result = client + .exchange_code(AuthorizationCode::new(code.clone())) + .set_pkce_verifier(pkce_verifier) + .request(http_client)?; + + info!("{:?}", &token_result); + let access_token = token_result.access_token().secret(); + let expires_in = token_result.expires_in().unwrap().as_secs(); + let refresh_secret = token_result.refresh_token().unwrap().secret(); + let user_info_url = "https://www.googleapis.com/oauth2/v3/userinfo"; + let client = reqwest::Client::new(); + let response = client + .get(user_info_url) + .bearer_auth(access_token) + .send() + .await?; + let email = if response.status().is_success() { + let response_json: serde_json::Value = response.json().await?; + leptos::logging::log!("{response_json:?}"); + response_json["email"] + .as_str() + .expect("email to parse to string") + .to_string() + } else { + return Err(ServerFnError::ServerError(format!( + "Response from google has status of {}", + response.status() + ))); + }; + + let access_token = token_result.access_token().secret(); + info!("aftr access_token: {:?}", access_token); + + Ok((email, expires_in as u64)) +} + #[component] pub fn OAuth2Response() -> impl IntoView { + let handle_g_auth_redirect = Action::::server(); + let (email, set_email) = create_signal("".to_owned()); + + let query = use_query::(); + let navigate = leptos_router::use_navigate(); + create_effect(move |_| { + if let Some(Ok((email, expires_in))) = handle_g_auth_redirect.value().get() { + leptos::logging::log!("{}", email); + leptos::logging::log!("{}", expires_in); + set_email.set(email); + // navigate("/", NavigateOptions::default()); + } + }); + + create_effect(move |_| { + if let Ok(OAuthParams { code, state }) = query.get_untracked() { + handle_g_auth_redirect.dispatch(GoogleVerifyResponse { + provided_csrf: state.unwrap(), + code: code.unwrap(), + }); + } else { + leptos::logging::log!("error parsing oauth params"); + } + }); view! {
+ "email: " {email.get()}
} } + +#[derive(Params, Debug, PartialEq, Clone)] +pub struct OAuthParams { + pub code: Option, + pub state: Option, +}