coinbase_v3/
basic_oauth.rs1use std::collections::HashSet;
4use std::io::{BufRead, BufReader, Write};
5use std::net::TcpListener;
6
7use oauth2::reqwest::async_http_client;
8use oauth2::{
9 basic::BasicClient, revocation::StandardRevocableToken, AccessToken, AuthUrl,
10 AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, RefreshToken, RevocationUrl,
11 Scope, TokenResponse, TokenUrl,
12};
13use url::Url;
14
15use crate::scopes::VALID_SCOPES;
16
17const AUTH_URL_STR: &str = "https://www.coinbase.com/oauth/authorize";
18const TOKEN_URL_STR: &str = "https://www.coinbase.com/oauth/token";
19const REVOKE_URL_STR: &str = "https://api.coinbase.com/oauth/revoke";
20
21pub trait AccessTokenProvider {
32 fn access_token(&self) -> AccessToken;
34}
35
36impl AccessTokenProvider for OAuthCbClient {
40 fn access_token(&self) -> AccessToken {
41 self.access_token.clone().unwrap()
42 }
43}
44
45fn set_oauth_cb_urls() -> (AuthUrl, TokenUrl, RevocationUrl) {
46 let auth_url =
47 AuthUrl::new(AUTH_URL_STR.to_string()).expect("Invalid authorization endpoint URL");
48 let token_url = TokenUrl::new(TOKEN_URL_STR.to_string()).expect("Invalid token endpoint URL");
49 let revoke_url =
50 RevocationUrl::new(REVOKE_URL_STR.to_string()).expect("Invalid revocation endpoint URL");
51
52 (auth_url, token_url, revoke_url)
53}
54
55pub struct OAuthCbClient {
57 client: BasicClient,
58 access_token: Option<AccessToken>,
59 refresh_token: Option<RefreshToken>,
60 scopes: HashSet<Scope>,
61}
62
63impl OAuthCbClient {
64 pub fn new(client_id: &str, client_secret: &str, redirect_url: &str) -> Self {
79 let client_id = ClientId::new(client_id.to_string());
80 let client_secret = ClientSecret::new(client_secret.to_string());
81 let redirect_url =
82 RedirectUrl::new(redirect_url.to_string()).expect("Invalid redirect_url");
83
84 let (auth_url, token_url, revoke_url) = set_oauth_cb_urls();
85
86 let client = BasicClient::new(client_id, Some(client_secret), auth_url, Some(token_url))
87 .set_redirect_uri(redirect_url)
88 .set_revocation_uri(revoke_url);
89
90 Self {
91 client,
92 access_token: None,
93 refresh_token: None,
94 scopes: HashSet::new(),
95 }
96 }
97
98 pub fn add_scope(mut self, scope_description: &str) -> Self {
111 assert!(VALID_SCOPES.contains(&scope_description));
112
113 self.scopes
114 .insert(Scope::new(scope_description.to_string()));
115
116 self
117 }
118
119 pub async fn authorize_once(mut self: Self) -> Self {
134 let redirect_url = self.client.redirect_url().unwrap();
135 let scheme = redirect_url.url().scheme().to_string();
136 let host = redirect_url.url().host().unwrap().to_string();
137 let port = redirect_url.url().port().unwrap();
138
139 let listener_address = host.to_string() + ":" + &port.to_string();
140
141 let (authorize_url, csrf_state) = self
142 .client
143 .authorize_url(CsrfToken::new_random)
144 .add_scopes(self.scopes.clone())
145 .url();
146
147 println!(
148 "\nOpen this URL in your browser:\n{}\n\n",
149 authorize_url.to_string()
150 );
151
152 let listener = TcpListener::bind(listener_address).unwrap();
153 for stream in listener.incoming() {
154 if let Ok(mut stream) = stream {
155 let code;
156 let state;
157 {
158 let mut reader = BufReader::new(&stream);
159
160 let mut request_line = String::new();
161 reader.read_line(&mut request_line).unwrap();
162
163 let redirect_url = request_line.split_whitespace().nth(1).unwrap();
164 let url = Url::parse(&(scheme + "://" + &host + redirect_url)).unwrap();
165
166 let code_pair = url
167 .query_pairs()
168 .find(|pair| {
169 let &(ref key, _) = pair;
170 key == "code"
171 })
172 .unwrap();
173
174 let (_, value) = code_pair;
175 code = AuthorizationCode::new(value.into_owned());
176
177 let state_pair = url
178 .query_pairs()
179 .find(|pair| {
180 let &(ref key, _) = pair;
181 key == "state"
182 })
183 .unwrap();
184
185 let (_, value) = state_pair;
186 state = CsrfToken::new(value.into_owned());
187 }
188
189 let message = "Go back to your terminal :)";
190 let response = format!(
191 "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
192 message.len(),
193 message
194 );
195 stream.write_all(response.as_bytes()).unwrap();
196 assert!(state.secret() == csrf_state.secret());
197
198 let token_response = self
200 .client
201 .exchange_code(code)
202 .request_async(async_http_client)
203 .await;
204
205 let token_response = token_response.unwrap();
206 if let Some(tok) = token_response.refresh_token() {
207 self.refresh_token = Some(tok.clone());
208 }
209 self.access_token = Some(token_response.access_token().clone());
210
211 break;
212 }
213 }
214 self
215 }
216
217 pub async fn revoke_access(&self) {
222 let token_to_revoke: StandardRevocableToken = match self.refresh_token.as_ref() {
223 Some(token) => token.into(),
224 None => self.access_token.as_ref().unwrap().into(),
225 };
226
227 self.client
228 .revoke_token(token_to_revoke)
229 .unwrap()
230 .request_async(async_http_client)
231 .await
232 .expect("Failed to revoke token");
233
234 println!("=============== ACCESS REVOKED =================");
235 }
236}
237
238