From ced95c78fcd075f385b48723660dbc81637edb07 Mon Sep 17 00:00:00 2001 From: Adrian Kumpf <8999358+adriankumpf@users.noreply.github.com> Date: Tue, 14 Sep 2021 21:26:47 +0200 Subject: [PATCH] Move OAuth2 related parts into separate module --- src/auth.rs | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 117 ++++++++++++---------------------------------------- 2 files changed, 142 insertions(+), 90 deletions(-) create mode 100644 src/auth.rs diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..b739eaa --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,115 @@ +use std::collections::HashMap; + +use oauth2::basic::BasicClient; +use oauth2::reqwest::http_client; +use oauth2::url::Url; +use oauth2::{ + AuthType, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, + RedirectUrl, Scope, TokenResponse, TokenUrl, +}; + +use reqwest::header::AUTHORIZATION; + +use serde::Deserialize; + +const AUTH_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize"; +const TOKEN_URL: &str = "https://auth.tesla.com/oauth2/v3/token"; +const REDIRECT_URL: &str = "https://auth.tesla.com/void/callback"; + +const SSO_CLIENT_ID: &str = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384"; +const SSO_CLIENT_SECRET: &str = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3"; +const SSO_TOKEN_URL: &str = "https://owner-api.teslamotors.com/oauth/token"; + +#[derive(Deserialize, Debug)] +struct SsoTokenResponse { + access_token: String, +} + +#[derive(Debug)] +pub struct Tokens { + pub access: String, + pub refresh: String, +} + +pub struct Client { + oauth_client: BasicClient, + pkce_verifier: Option, + csrf_token: Option, +} + +impl Client { + pub fn new() -> Self { + let client = BasicClient::new( + ClientId::new("ownerapi".to_string()), + None, + AuthUrl::new(AUTH_URL.to_string()).unwrap(), + Some(TokenUrl::new(TOKEN_URL.to_string()).unwrap()), + ) + .set_auth_type(AuthType::RequestBody) + .set_redirect_uri(RedirectUrl::new(REDIRECT_URL.to_string()).unwrap()); + + Client { + oauth_client: client, + pkce_verifier: None, + csrf_token: None, + } + } + + pub fn authorization_url(&mut self) -> Url { + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + self.pkce_verifier = Some(pkce_verifier); + + let (auth_url, csrf_token) = self + .oauth_client + .authorize_url(CsrfToken::new_random) + .add_scope(Scope::new("openid".to_string())) + .add_scope(Scope::new("email".to_string())) + .add_scope(Scope::new("offline_access".to_string())) + .set_pkce_challenge(pkce_challenge) + .url(); + + self.csrf_token = Some(csrf_token); + + auth_url + } + + pub fn verify_csrf_state(&self, state: String) { + let csrf_token = self.csrf_token.as_ref().unwrap(); + assert_eq!(&&state, &csrf_token.secret()); + } + + pub fn retrieve_tokens(self, code: AuthorizationCode) -> Tokens { + let token_result = self + .oauth_client + .exchange_code(code) + .set_pkce_verifier(self.pkce_verifier.unwrap()) + .request(http_client) + .unwrap(); + + let short_lived_access_token = token_result.access_token().secret(); + let refresh_token = token_result.refresh_token().unwrap().secret().to_string(); + + let mut body = HashMap::new(); + body.insert("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"); + body.insert("client_id", SSO_CLIENT_ID); + body.insert("client_secret", SSO_CLIENT_SECRET); + + let tokens: SsoTokenResponse = reqwest::blocking::Client::new() + .post(SSO_TOKEN_URL) + .header( + AUTHORIZATION, + format!("Bearer {}", short_lived_access_token), + ) + .json(&body) + .send() + .unwrap() + .json() + .unwrap(); + + Tokens { + access: tokens.access_token, + refresh: refresh_token, + } + } +} diff --git a/src/main.rs b/src/main.rs index 1a16060..5ed4869 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,18 @@ -use oauth2::basic::BasicClient; -use oauth2::reqwest::http_client; -use oauth2::url::Url; -use oauth2::{ - AuthType, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge, RedirectUrl, - Scope, TokenResponse, TokenUrl, -}; - -use reqwest; -use reqwest::header::AUTHORIZATION; - -use serde::Deserialize; -use serde_json::Value; +mod auth; use std::collections::HashMap; use std::sync::mpsc::channel; use std::thread; -use wry::{ - application::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::{Window, WindowBuilder}, - }, - webview::{RpcRequest, RpcResponse, WebViewBuilder}, -}; +use oauth2::url::Url; +use oauth2::AuthorizationCode; + +use serde_json::Value; + +use wry::application::event::{Event, WindowEvent}; +use wry::application::event_loop::{ControlFlow, EventLoop}; +use wry::application::window::{Window, WindowBuilder}; +use wry::webview::{RpcRequest, RpcResponse, WebViewBuilder}; const INITIALIZATION_SCRIPT: &str = r#" (function () { @@ -33,38 +22,11 @@ const INITIALIZATION_SCRIPT: &str = r#" })(); "#; -#[derive(Deserialize, Debug)] -struct SsoTokenResponse { - access_token: String, -} - fn main() -> wry::Result<()> { - let (sender, receiver) = channel(); + let mut client = auth::Client::new(); + let auth_url = client.authorization_url(); - let client = BasicClient::new( - ClientId::new("ownerapi".to_string()), - None, - AuthUrl::new("https://auth.tesla.com/oauth2/v3/authorize".to_string())?, - Some(TokenUrl::new( - "https://auth.tesla.com/oauth2/v3/token".to_string(), - )?), - ) - .set_auth_type(AuthType::RequestBody) - .set_redirect_uri(RedirectUrl::new( - "https://auth.tesla.com/void/callback".to_string(), - )?); - - let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); - - let (auth_url, csrf_token) = client - .authorize_url(CsrfToken::new_random) - .add_scope(Scope::new("openid".to_string())) - .add_scope(Scope::new("email".to_string())) - .add_scope(Scope::new("offline_access".to_string())) - .set_pkce_challenge(pkce_challenge) - .url(); - - // println!("Browse to: {}", auth_url); + println!("Opening {} ...", auth_url); let event_loop = EventLoop::new(); let event_proxy = event_loop.create_proxy(); @@ -74,13 +36,11 @@ fn main() -> wry::Result<()> { .build(&event_loop) .unwrap(); + let (sender, receiver) = channel(); + let handler = move |_window: &Window, mut req: RpcRequest| match req.method.as_str() { "url" => { - let params = req.params.take().unwrap(); - let mut args: Vec = serde_json::from_value(params).unwrap(); - let arg = args.swap_remove(0); - - let url = Url::parse(&arg).expect("Invalid URL"); + let url = parse_url(&mut req); sender.send(url).unwrap(); Some(RpcResponse::new_result( @@ -110,45 +70,14 @@ fn main() -> wry::Result<()> { let state = query.get("state").expect("No state parameter found"); let code = query.get("code").expect("No code parameter found"); - assert_eq!(&state.to_string(), csrf_token.secret()); + client.verify_csrf_state(state.to_string()); let code = AuthorizationCode::new(code.to_string()); - - let token_result = client - .exchange_code(code) - .set_pkce_verifier(pkce_verifier) - .request(http_client) - .unwrap(); - - let req_client = reqwest::blocking::Client::new(); - - let mut body = HashMap::new(); - body.insert("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"); - body.insert( - "client_id", - "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384", - ); - body.insert( - "client_secret", - "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3", - ); - - let tokens: SsoTokenResponse = req_client - .post("https://owner-api.teslamotors.com/oauth/token") - .header( - AUTHORIZATION, - format!("Bearer {}", token_result.access_token().secret()), - ) - .json(&body) - .send() - .unwrap() - .json() - .unwrap(); + let tokens = client.retrieve_tokens(code); println!( "Access Token: {}\nRefresh Token: {}", - tokens.access_token, - token_result.refresh_token().unwrap().secret() + tokens.access, tokens.refresh ); event_proxy.send_event(()).unwrap(); @@ -170,3 +99,11 @@ fn main() -> wry::Result<()> { } }); } + +fn parse_url(req: &mut RpcRequest) -> Url { + let params = req.params.take().unwrap(); + let mut args: Vec = serde_json::from_value(params).unwrap(); + let arg = args.swap_remove(0); + + Url::parse(&arg).expect("Invalid URL") +}