system settings
git clone https://git.lucas.co/cce-system-interface.git
sec: implement dynamic PKCE for Google login flow
Cargo.toml | 3 +++
src/pages/accounts.rs | 28 ++++++++++++++++++++++------
2 files changed, 25 insertions(+), 6 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index f33f50e..aacea48 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,9 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
kdl = "4.6"
reqwest = { version = "0.12", features = ["json"] }
+ring = "0.17.14"
+base64 = "0.22"
+
[lib]
name = "cce_settings"
diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs
index d3fa5af..94f341d 100644
--- a/src/pages/accounts.rs
+++ b/src/pages/accounts.rs
@@ -147,8 +147,21 @@ pub async fn fetch_accounts() -> Vec<AccountInfo> {
const GOOGLE_CLIENT_ID: &str = "REDACTED.apps.googleusercontent.com";
const GOOGLE_CLIENT_SECRET: &str = "GOCSPX-REDACTED";
-const PKCE_VERIFIER: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
-const PKCE_CHALLENGE: &str = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
+
+fn generate_pkce() -> (String, String) {
+ use ring::rand::SecureRandom;
+ use base64::Engine;
+ let rand = ring::rand::SystemRandom::new();
+ let mut bytes = [0u8; 32];
+ rand.fill(&mut bytes).unwrap();
+ let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
+
+ let hash = ring::digest::digest(&ring::digest::SHA256, verifier.as_bytes());
+ let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash.as_ref());
+
+ (verifier, challenge)
+}
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GoogleClientConfig {
@@ -211,10 +224,12 @@ pub async fn run_google_login(sender: calloop::channel::Sender<AppAction>) {
let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage("Waiting for browser login...".to_string())));
+ let (verifier, challenge) = generate_pkce();
+
let auth_url = format!(
"https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri=http%3A%2F%2Flocalhost%3A36137%2Fauth%2Fcallback&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcclog+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fexperimentsandconfigs+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Faicode&access_type=offline&prompt=consent&code_challenge={}&code_challenge_method=S256",
client_config.client_id,
- PKCE_CHALLENGE
+ challenge
);
let mut cmd = std::process::Command::new("xdg-open");
cmd.arg(&auth_url);
@@ -233,7 +248,7 @@ pub async fn run_google_login(sender: calloop::channel::Sender<AppAction>) {
let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage("Exchanging code for token...".to_string())));
// Perform token exchange
- exchange_code_for_tokens(code, sender.clone()).await;
+ exchange_code_for_tokens(code, verifier, sender.clone()).await;
let response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n\
<html><head><style>body { font-family: sans-serif; background-color: #08080c; color: #fff; text-align: center; padding-top: 50px; }</style></head><body><h2>Clear System Settings Authentication Successful!</h2><p>You can close this tab and return to the application.</p></body></html>";
@@ -250,7 +265,7 @@ pub async fn run_google_login(sender: calloop::channel::Sender<AppAction>) {
}
}
-pub async fn exchange_code_for_tokens(code: String, sender: calloop::channel::Sender<AppAction>) {
+pub async fn exchange_code_for_tokens(code: String, verifier: String, sender: calloop::channel::Sender<AppAction>) {
let client_config = load_google_client_config();
let client = reqwest::Client::new();
let mut params = vec![
@@ -258,11 +273,12 @@ pub async fn exchange_code_for_tokens(code: String, sender: calloop::channel::Se
("client_id", client_config.client_id.as_str()),
("redirect_uri", "http://localhost:36137/auth/callback"),
("grant_type", "authorization_code"),
- ("code_verifier", PKCE_VERIFIER),
+ ("code_verifier", verifier.as_str()),
];
if !client_config.client_secret.is_empty() {
params.push(("client_secret", client_config.client_secret.as_str()));
}
+
match client.post("https://oauth2.googleapis.com/token")
.form(¶ms)