Skip to content

Commit b51a782

Browse files
Add SIWE as auth option (separate endpoint) and clippy fixes
1 parent de72b50 commit b51a782

File tree

4 files changed

+95
-30
lines changed

4 files changed

+95
-30
lines changed

Diff for: src/main.rs

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use axum::{
2323
};
2424
use database::types::Database;
2525
use dotenvy::dotenv;
26+
use routes::login::user_login_siwe;
2627
use routes::payment::process_ethereum_payment;
2728
use routes::siwe::{get_siwe_nonce, siwe_add_wallet};
2829
use tokio::net::TcpListener;
@@ -84,6 +85,7 @@ async fn main() {
8485
.route("/api/register", post(register_user))
8586
.route("/api/activate", post(activate_account))
8687
.route("/api/login", post(user_login))
88+
.route("/api/login/siwe", post(user_login_siwe))
8789
.route("/api/recovery", post(update_password))
8890
.route("/api/recovery/:email", get(recover_password_email))
8991
.merge(api_keys)
@@ -98,3 +100,4 @@ async fn main() {
98100

99101
// todo:
100102
// - SIWE login route (instead of email + pw)
103+
// - Deploy sepolia contracts for Saul

Diff for: src/routes/login.rs

+79-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
use super::types::{Claims, JWT_KEY};
2-
use crate::database::{
3-
errors::ParsingError,
4-
types::{Customers, RELATIONAL_DATABASE, Role},
1+
use super::{
2+
siwe::Siwe,
3+
types::{Claims, JWT_KEY},
54
};
6-
use alloy::primitives::Address;
5+
use crate::{
6+
database::{
7+
errors::ParsingError,
8+
types::{Customers, RELATIONAL_DATABASE, Role},
9+
},
10+
eth_rpc::types::ETHEREUM_ENDPOINT,
11+
};
12+
use alloy::{primitives::Address, providers::ProviderBuilder};
713
use argon2::{Argon2, PasswordHash, PasswordVerifier};
814
use axum::{
915
Json,
@@ -12,14 +18,74 @@ use axum::{
1218
};
1319
use jwt_simple::{algorithms::MACLike, reexports::coarsetime::Duration};
1420
use serde::{Deserialize, Serialize};
21+
use siwe::{Message, VerificationError, VerificationOpts};
1522
use thiserror::Error;
23+
use time::OffsetDateTime;
1624

1725
#[derive(Serialize, Deserialize, Clone, Debug)]
1826
pub struct LoginRequest {
1927
pub(crate) email: String,
2028
pub(crate) password: String,
2129
}
2230

31+
pub struct SiweLogin {
32+
pub email: String,
33+
pub wallet: Option<String>,
34+
pub role: Role,
35+
pub nonce: Option<String>,
36+
}
37+
38+
#[tracing::instrument]
39+
pub async fn user_login_siwe(Json(payload): Json<Siwe>) -> Result<impl IntoResponse, LoginError> {
40+
let msg: Message = payload.message.parse()?;
41+
let address = Address::new(msg.address);
42+
43+
let customer = sqlx::query_as!(
44+
SiweLogin,
45+
r#"SELECT email, wallet, nonce, role as "role!: Role" FROM Customers where wallet = $1"#,
46+
address.to_string(),
47+
)
48+
.fetch_optional(RELATIONAL_DATABASE.get().unwrap())
49+
.await?
50+
.ok_or_else(|| LoginError::InvalidAddress)?;
51+
52+
let nonce = customer.nonce.ok_or_else(|| LoginError::MissingNonce)?;
53+
54+
let rpc = ProviderBuilder::new().on_http(ETHEREUM_ENDPOINT[0].as_str().parse().unwrap());
55+
56+
let verification_opts = VerificationOpts {
57+
domain: Some("Developer DAO Cloud".parse().unwrap()),
58+
nonce: Some(nonce),
59+
timestamp: Some(OffsetDateTime::now_utc()),
60+
rpc_provider: Some(rpc),
61+
};
62+
63+
msg.verify(&payload.signature, &verification_opts).await?;
64+
65+
let user_info = Claims {
66+
role: customer.role,
67+
email: customer.email,
68+
wallet: Some(address),
69+
};
70+
let claims = jwt_simple::claims::Claims::with_custom_claims(user_info, Duration::from_hours(2));
71+
let key = JWT_KEY.get().unwrap();
72+
let auth = key.authenticate(claims)?;
73+
74+
let mut headers = HeaderMap::new();
75+
headers.insert(
76+
SET_COOKIE,
77+
HeaderValue::from_str(&format!("jwt={}", auth)).unwrap(),
78+
);
79+
headers.append(SET_COOKIE, HeaderValue::from_str("Secure").unwrap());
80+
headers.append(SET_COOKIE, HeaderValue::from_str("HttpOnly").unwrap());
81+
headers.append(
82+
SET_COOKIE,
83+
HeaderValue::from_str("SameSite=Strict").unwrap(),
84+
);
85+
86+
Ok((StatusCode::OK, headers))
87+
}
88+
2389
#[tracing::instrument]
2490
pub async fn user_login(
2591
Json(payload): Json<LoginRequest>,
@@ -71,6 +137,10 @@ pub async fn user_login(
71137

72138
#[derive(Debug, Error)]
73139
pub enum LoginError {
140+
#[error(transparent)]
141+
VerificationError(#[from] VerificationError),
142+
#[error("User did not generate nonce")]
143+
MissingNonce,
74144
#[error("The email or password you provided is invalid.")]
75145
InvalidEmailOrPassword,
76146
#[error(transparent)]
@@ -85,6 +155,10 @@ pub enum LoginError {
85155
AddressParsingError(#[from] ParsingError),
86156
#[error(transparent)]
87157
BuilderResponseError(#[from] axum::http::Error),
158+
#[error("No account found for address")]
159+
InvalidAddress,
160+
#[error(transparent)]
161+
ParseError(#[from] siwe::ParseError),
88162
}
89163

90164
impl IntoResponse for LoginError {

Diff for: src/routes/payment.rs

+8-22
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,6 @@ use thiserror::Error;
2727
use tokio::task::JoinError;
2828
use tracing::info;
2929

30-
// const TOKENS_SUPPORTED: [&str; 8] = [
31-
// "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
32-
// "0x7F5c764cBc14f9669B88837ca1490cCa17c31607",
33-
// "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
34-
// "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8",
35-
// "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
36-
// "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
37-
// "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
38-
// "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
39-
// ];
40-
4130
#[derive(Serialize, Deserialize, Debug)]
4231
pub struct PriceData {
4332
data: AssetData,
@@ -134,13 +123,10 @@ pub async fn process_ethereum_payment(
134123
} else {
135124
ETHEREUM_ENDPOINT
136125
.iter()
137-
.find(|e| match (e, payload.chain) {
138-
(crate::eth_rpc::types::InternalEndpoints::Optimism(_), Chain::Optimism) => true,
139-
(crate::eth_rpc::types::InternalEndpoints::Arbitrum(_), Chain::Arbitrum) => true,
140-
(crate::eth_rpc::types::InternalEndpoints::Polygon(_), Chain::Polygon) => true,
141-
(crate::eth_rpc::types::InternalEndpoints::Base(_), Chain::Base) => true,
142-
_ => false,
143-
})
126+
.find(|e| matches!((e, payload.chain), (crate::eth_rpc::types::InternalEndpoints::Optimism(_), Chain::Optimism) |
127+
(crate::eth_rpc::types::InternalEndpoints::Arbitrum(_), Chain::Arbitrum) |
128+
(crate::eth_rpc::types::InternalEndpoints::Polygon(_), Chain::Polygon) |
129+
(crate::eth_rpc::types::InternalEndpoints::Base(_), Chain::Base)))
144130
.ok_or_else(|| PaymentError::InvalidNetwork)?
145131
.as_str()
146132
};
@@ -151,7 +137,7 @@ pub async fn process_ethereum_payment(
151137

152138
let res: tokio::task::JoinHandle<Result<Transaction, PaymentError>> = {
153139
tokio::spawn(async move {
154-
let eth = reqwest::Url::parse(&endpoint).unwrap();
140+
let eth = reqwest::Url::parse(endpoint).unwrap();
155141
let provider = ProviderBuilder::new().on_http(eth);
156142
provider
157143
.get_transaction_by_hash(FixedBytes::from(&fixed))
@@ -162,7 +148,7 @@ pub async fn process_ethereum_payment(
162148

163149
let receipt: tokio::task::JoinHandle<Result<TransactionReceipt, PaymentError>> = {
164150
tokio::spawn(async move {
165-
let eth = reqwest::Url::parse(&endpoint).unwrap();
151+
let eth = reqwest::Url::parse(endpoint).unwrap();
166152
let provider = ProviderBuilder::new().on_http(eth);
167153
provider
168154
.get_transaction_receipt(FixedBytes::from(&fixed))
@@ -173,7 +159,7 @@ pub async fn process_ethereum_payment(
173159

174160
let last_safe_block: tokio::task::JoinHandle<Result<u64, PaymentError>> = {
175161
tokio::spawn(async move {
176-
let eth = reqwest::Url::parse(&endpoint).unwrap();
162+
let eth = reqwest::Url::parse(endpoint).unwrap();
177163
let provider = ProviderBuilder::new().on_http(eth);
178164
provider
179165
.get_block(BlockId::safe(), BlockTransactionsKind::Full)
@@ -203,7 +189,7 @@ pub async fn process_ethereum_payment(
203189

204190
let res: &Transaction = &res??;
205191

206-
if jwt.custom.wallet.is_some_and(|e| res.from == e) == false {
192+
if jwt.custom.wallet.is_none_or(|e| res.from == e) {
207193
Err(PaymentError::SenderWalletMismatch)?
208194
}
209195

Diff for: src/routes/siwe.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use super::types::Claims;
1616

1717
#[derive(Debug, Serialize, Deserialize)]
1818
pub struct Siwe {
19-
message: String,
20-
signature: Vec<u8>,
19+
pub message: String,
20+
pub signature: Vec<u8>,
2121
}
2222

2323
#[derive(FromRow)]
@@ -29,7 +29,7 @@ pub async fn siwe_add_wallet(
2929
Extension(jwt): Extension<JWTClaims<Claims>>,
3030
Json(payload): Json<Siwe>,
3131
) -> Result<impl IntoResponse, SiweError> {
32-
let msg: Message = payload.message.parse().unwrap();
32+
let msg: Message = payload.message.parse()?;
3333

3434
let nonce = sqlx::query_as!(
3535
Nonce,
@@ -87,6 +87,8 @@ pub enum SiweError {
8787
IncorrectNonce,
8888
#[error("An error ocurred while querying the database")]
8989
QueryError(#[from] sqlx::Error),
90+
#[error(transparent)]
91+
ParseError(#[from] siwe::ParseError),
9092
}
9193

9294
impl IntoResponse for SiweError {

0 commit comments

Comments
 (0)