Skip to content
This repository was archived by the owner on Feb 16, 2026. It is now read-only.

Commit 6dfd4aa

Browse files
committed
Add OAuth2 support (WIP)
Signed-off-by: Yuki Kishimoto <yukikishimoto@protonmail.com>
1 parent 6611b62 commit 6dfd4aa

9 files changed

Lines changed: 380 additions & 85 deletions

File tree

Cargo.lock

Lines changed: 233 additions & 75 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ rust-version = "1.85.0"
66
publish = false
77

88
[dependencies]
9+
async-trait = "0.1"
910
base64 = "0.22"
1011
chrono = { version = "0.4", features = ["serde"] }
12+
oauth2 = { git = "https://github.com/ramosbugs/oauth2-rs", tag = "5.0.0" }
1113
p256 = { version = "0.13", features = ["pem", "pkcs8"] }
1214
reqwest = { version = "0.12", default-features = false, features = ["http2", "json", "rustls-tls"] }
1315
ring = "0.17"

src/app/agent.rs

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,44 @@
1+
use std::sync::Arc;
12
use std::time::Duration;
23

4+
use oauth2::basic::{
5+
BasicClient, BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse,
6+
BasicTokenResponse,
7+
};
8+
use oauth2::{AuthUrl, EndpointNotSet, EndpointSet, RedirectUrl, StandardRevocableToken, TokenUrl};
39
use reqwest::header::{CONTENT_TYPE, HeaderValue, USER_AGENT};
410
use reqwest::{Client, Method, Response};
511
use url::Url;
612

713
use super::auth::CoinbaseAuth;
814
use super::auth::jwt::Jwt;
9-
use super::constant::{API_ROOT_URL, API_SANDBOX_URL, CB_VERSION, USER_AGENT_NAME};
15+
use super::constant::{
16+
API_ROOT_URL, API_SANDBOX_URL, CB_VERSION, OAUTH_ACCESS_TOKEN_URL, OAUTH_AUTHORIZE_URL,
17+
USER_AGENT_NAME,
18+
};
1019
use super::error::Error;
20+
use super::oauth::{CoinbaseAppOAuth2Callback, CoinbaseOAuth2Token};
21+
22+
#[derive(Debug, Clone)]
23+
enum Authenticator {
24+
None,
25+
Jwt(Jwt),
26+
OAuth2 {
27+
client: oauth2::Client<
28+
BasicErrorResponse,
29+
BasicTokenResponse,
30+
BasicTokenIntrospectionResponse,
31+
StandardRevocableToken,
32+
BasicRevocationErrorResponse,
33+
EndpointSet,
34+
EndpointNotSet,
35+
EndpointNotSet,
36+
EndpointNotSet,
37+
EndpointSet,
38+
>,
39+
token: Arc<Mutex<Option<CoinbaseOAuth2Token>>>
40+
},
41+
}
1142

1243
#[derive(Debug, Clone)]
1344
struct HttpClientAgent {
@@ -87,31 +118,65 @@ impl HttpClientAgent {
87118

88119
#[derive(Debug, Clone)]
89120
pub struct SecureHttpClientAgent {
90-
/// JWT generator, disabled in sandbox mode.
91-
jwt: Option<Jwt>,
121+
/// Authenticator
122+
authenticator: Authenticator,
123+
/// OAuth2 callback
124+
oauth2_callback: Option<Arc<dyn CoinbaseAppOAuth2Callback>>,
92125
/// Base client that is responsible for making the requests.
93126
base: HttpClientAgent,
94127
}
95128

96129
impl SecureHttpClientAgent {
97-
pub(super) fn new(auth: CoinbaseAuth, sandbox: bool, timeout: Duration) -> Result<Self, Error> {
98-
let jwt: Option<Jwt> = match auth {
99-
CoinbaseAuth::None => None,
130+
pub(super) fn new(
131+
auth: CoinbaseAuth,
132+
sandbox: bool,
133+
timeout: Duration,
134+
oauth2_callback: Option<Arc<dyn CoinbaseAppOAuth2Callback>>,
135+
) -> Result<Self, Error> {
136+
let authenticator: Authenticator = match auth {
137+
CoinbaseAuth::None => Authenticator::None,
100138
CoinbaseAuth::ApiKeys {
101139
api_key,
102140
secret_key,
103141
} => {
104142
// Do not generate JWT in sandbox mode.
105143
if sandbox {
106-
None
144+
Authenticator::None
107145
} else {
108-
Some(Jwt::new(api_key, secret_key)?)
146+
Authenticator::Jwt(Jwt::new(api_key, secret_key)?)
109147
}
110148
}
149+
CoinbaseAuth::OAuth {
150+
client_id,
151+
client_secret,
152+
redirect_url,
153+
token,
154+
} => {
155+
let auth_url: Url = Url::parse(OAUTH_AUTHORIZE_URL)?;
156+
let auth_url: AuthUrl = AuthUrl::from_url(auth_url);
157+
158+
let token_url: Url = Url::parse(OAUTH_ACCESS_TOKEN_URL)?;
159+
let token_url: TokenUrl = TokenUrl::from_url(token_url);
160+
161+
let redirect_url: RedirectUrl = RedirectUrl::from_url(redirect_url);
162+
163+
let client = BasicClient::new(client_id)
164+
.set_client_secret(client_secret)
165+
.set_auth_uri(auth_url)
166+
.set_token_uri(token_url)
167+
.set_redirect_uri(redirect_url);
168+
169+
//client.exchange_refresh_token()
170+
171+
//client.exchange_code().request_async()
172+
173+
Authenticator::OAuth2 { client, token: Arc::new(Mutex::new(token)) }
174+
}
111175
};
112176

113177
Ok(Self {
114-
jwt,
178+
authenticator,
179+
oauth2_callback,
115180
base: HttpClientAgent::new(sandbox, timeout)?,
116181
})
117182
}

src/app/auth/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
55
use std::fmt;
66

7+
use oauth2::{ClientId, ClientSecret};
8+
use url::Url;
9+
710
pub(super) mod jwt;
811

12+
use crate::app::oauth::CoinbaseOAuth2Token;
13+
914
/// Coinbase authentication
1015
#[derive(Clone, Default)]
1116
pub enum CoinbaseAuth {
@@ -19,6 +24,17 @@ pub enum CoinbaseAuth {
1924
/// Secret Key
2025
secret_key: String,
2126
},
27+
/// OAuth2
28+
OAuth {
29+
/// Client ID
30+
client_id: ClientId,
31+
/// Client secret
32+
client_secret: ClientSecret,
33+
/// Redirect URL
34+
redirect_url: Url,
35+
/// Token
36+
token: Option<CoinbaseOAuth2Token>,
37+
},
2238
}
2339

2440
impl fmt::Debug for CoinbaseAuth {

src/app/builder.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
//! Coinbase App client builder
22
3+
use std::sync::Arc;
34
use std::time::Duration;
45

56
use super::auth::CoinbaseAuth;
67
use super::client::CoinbaseAppClient;
78
use super::error::Error;
9+
use super::oauth::CoinbaseAppOAuth2Callback;
810

911
/// Coinbase App client builder
1012
#[derive(Debug, Clone)]
1113
pub struct CoinbaseAppClientBuilder {
1214
/// Authentication
1315
pub auth: CoinbaseAuth,
16+
/// OAuth2 callback
17+
///
18+
/// This is needed for receiving the refreshed token
19+
pub oauth_callback: Option<Arc<dyn CoinbaseAppOAuth2Callback>>,
1420
/// Use sandbox APIs
1521
pub sandbox: bool,
1622
/// Requests timeout
@@ -21,6 +27,7 @@ impl Default for CoinbaseAppClientBuilder {
2127
fn default() -> Self {
2228
Self {
2329
auth: CoinbaseAuth::default(),
30+
oauth_callback: None,
2431
sandbox: false,
2532
timeout: Duration::from_secs(20),
2633
}
@@ -35,6 +42,16 @@ impl CoinbaseAppClientBuilder {
3542
self
3643
}
3744

45+
/// Set OAuth2 callback
46+
#[inline]
47+
pub fn oauth2_callback<T>(mut self, callback: T) -> Self
48+
where
49+
T: CoinbaseAppOAuth2Callback + 'static,
50+
{
51+
self.oauth_callback = Some(Arc::new(callback));
52+
self
53+
}
54+
3855
/// Set sandbox APIs
3956
#[inline]
4057
pub fn sandbox(mut self, sandbox: bool) -> Self {

src/app/client.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ impl CoinbaseAppClient {
2929
#[inline]
3030
pub(super) fn from_builder(builder: CoinbaseAppClientBuilder) -> Result<Self, Error> {
3131
Ok(Self {
32-
client: SecureHttpClientAgent::new(builder.auth, builder.sandbox, builder.timeout)?,
32+
client: SecureHttpClientAgent::new(
33+
builder.auth,
34+
builder.sandbox,
35+
builder.timeout,
36+
builder.oauth_callback,
37+
)?,
3338
})
3439
}
3540

src/app/constant.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
pub(super) const API_ROOT_URL: &str = "https://api.coinbase.com";
33
pub(super) const API_SANDBOX_URL: &str = "https://api-sandbox.coinbase.com";
44

5+
pub(super) const OAUTH_AUTHORIZE_URL: &str = "https://login.coinbase.com/oauth2/auth";
6+
pub(super) const OAUTH_ACCESS_TOKEN_URL: &str = "https://login.coinbase.com/oauth2/token";
7+
58
/// User Agent for the client
69
pub(super) const USER_AGENT_NAME: &str = concat!("coinbase-api/", env!("CARGO_PKG_VERSION"));
710

src/app/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ pub mod builder;
88
pub mod client;
99
mod constant;
1010
pub mod error;
11+
pub mod oauth;
1112
pub mod response;

src/app/oauth.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! Coinbase App OAuth2 client
2+
3+
use std::fmt::Debug;
4+
5+
use oauth2::{AccessToken, RefreshToken};
6+
7+
// /// Coinbase App OAuth2 refreshed token
8+
// #[derive(Debug, Clone)]
9+
// pub struct CoinbaseOAuth2Token {
10+
// /// The new access token
11+
// pub access_token: AccessToken,
12+
// /// The refresh token
13+
// pub refresh_token: RefreshToken,
14+
// /// The expiration time
15+
// pub expires_at: Option<u64>,
16+
// }
17+
18+
/// Coinbase App OAuth2 callback
19+
#[async_trait::async_trait]
20+
pub trait CoinbaseAppOAuth2Callback: Debug + Send + Sync {
21+
// /// The token has been refreshed
22+
// async fn token_refreshed(
23+
// &self,
24+
// token: CoinbaseOAuth2Token,
25+
// ) -> Result<(), Box<dyn std::error::Error>>;
26+
27+
async fn get_access_token(&self) -> Result<AccessToken, Box<dyn std::error::Error>>;
28+
}

0 commit comments

Comments
 (0)