parent
782143415c
commit
d59c4db602
1772
Cargo.lock
generated
1772
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@ -14,24 +14,24 @@ crate-type = ["cdylib", "rlib"]
|
||||
anyhow = "1.0.53"
|
||||
headers = "0.3.6"
|
||||
hex = "0.4.3"
|
||||
iri-string = { version = "0.4", features = ["serde-std"] }
|
||||
iri-string = { version = "0.6", features = ["serde"] }
|
||||
# openidconnect = "2.1.2"
|
||||
openidconnect = { git = "https://github.com/sbihel/openidconnect-rs", branch = "main", default-features = false, features = ["reqwest", "rustls-tls", "rustcrypto"] }
|
||||
openidconnect = { git = "https://github.com/sbihel/openidconnect-rs", branch = "replace-ring", default-features = false, features = ["reqwest", "rustls-tls"] }
|
||||
rand = "0.8.4"
|
||||
rsa = { version = "0.5.0", features = ["alloc"] }
|
||||
rsa = { version = "0.7.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.78"
|
||||
siwe = "0.2.0"
|
||||
siwe = "0.5.0"
|
||||
thiserror = "1.0.30"
|
||||
tracing = "0.1.30"
|
||||
url = { version = "2.2", features = ["serde"] }
|
||||
urlencoding = "2.1.0"
|
||||
sha2 = "0.9.0"
|
||||
sha2 = "0.10.0"
|
||||
cookie = "0.16.0"
|
||||
bincode = "1.3.3"
|
||||
async-trait = "0.1.52"
|
||||
ethers-core = "0.6.3"
|
||||
ethers-providers = "0.6.2"
|
||||
ethers-core = "1.0.2"
|
||||
ethers-providers = "1.0.2"
|
||||
lazy_static = "1.4"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
@ -56,12 +56,18 @@ matchit = "0.4.2"
|
||||
serde_urlencoded = "0.7.0"
|
||||
uuid = { version = "0.8", features = ["serde", "v4", "wasm-bindgen"] }
|
||||
wee_alloc = { version = "0.4" }
|
||||
worker = "0.0.9"
|
||||
worker = "0.0.12"
|
||||
time = { version = "0.3.17", features = ["wasm-bindgen"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.10.0"
|
||||
test-log = "0.2.11"
|
||||
tokio = { version = "1.14.0", features = ["macros", "rt"] }
|
||||
|
||||
# [target.'cfg(target_arch = "wasm32")'.profile.release]
|
||||
# opt-level = "z"
|
||||
|
||||
|
@ -7,7 +7,7 @@ use axum::{
|
||||
},
|
||||
response::{self, IntoResponse, Redirect},
|
||||
routing::{delete, get, get_service, post},
|
||||
AddExtensionLayer, Json, Router,
|
||||
Json, Router,
|
||||
};
|
||||
use bb8_redis::{bb8, RedisConnectionManager};
|
||||
use figment::{
|
||||
@ -25,7 +25,7 @@ use openidconnect::core::{
|
||||
};
|
||||
use rand::rngs::OsRng;
|
||||
use rsa::{
|
||||
pkcs1::{FromRsaPrivateKey, ToRsaPrivateKey},
|
||||
pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey, LineEnding},
|
||||
RsaPrivateKey,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
@ -130,8 +130,9 @@ async fn sign_in(
|
||||
Query(params): Query<oidc::SignInParams>,
|
||||
TypedHeader(cookies): TypedHeader<headers::Cookie>,
|
||||
Extension(redis_client): Extension<RedisClient>,
|
||||
Extension(config): Extension<config::Config>,
|
||||
) -> Result<Redirect, CustomError> {
|
||||
let url = oidc::sign_in(params, cookies, &redis_client).await?;
|
||||
let url = oidc::sign_in(&config.base_url, params, cookies, &redis_client).await?;
|
||||
Ok(Redirect::to(
|
||||
url.as_str()
|
||||
.parse()
|
||||
@ -273,7 +274,7 @@ pub async fn main() {
|
||||
.unwrap();
|
||||
|
||||
info!("Generated key.");
|
||||
info!("{:?}", private.to_pkcs1_pem().unwrap());
|
||||
info!("{:?}", private.to_pkcs1_pem(LineEnding::LF).unwrap());
|
||||
private
|
||||
};
|
||||
|
||||
@ -355,9 +356,9 @@ pub async fn main() {
|
||||
.route(&format!("{}/:id", oidc::CLIENT_PATH), post(client_update))
|
||||
.route(oidc::SIGNIN_PATH, get(sign_in))
|
||||
.route("/health", get(healthcheck))
|
||||
.layer(AddExtensionLayer::new(private_key))
|
||||
.layer(AddExtensionLayer::new(config.clone()))
|
||||
.layer(AddExtensionLayer::new(redis_client))
|
||||
.layer(Extension(private_key))
|
||||
.layer(Extension(config.clone()))
|
||||
.layer(Extension(redis_client))
|
||||
.layer(TraceLayer::new_for_http());
|
||||
|
||||
let addr = SocketAddr::from((config.address, config.port));
|
||||
|
138
src/oidc.rs
138
src/oidc.rs
@ -2,6 +2,7 @@ use anyhow::{anyhow, Result};
|
||||
use chrono::{Duration, Utc};
|
||||
use cookie::{Cookie, SameSite};
|
||||
use ethers_core::{types::H160, utils::to_checksum};
|
||||
use ethers_providers::{Http, Middleware, Provider};
|
||||
use headers::{self, authorization::Bearer};
|
||||
use hex::FromHex;
|
||||
use iri_string::types::UriString;
|
||||
@ -23,9 +24,12 @@ use openidconnect::{
|
||||
ResponseTypes, Scope, StandardClaims, SubjectIdentifier, TokenUrl, UserInfoUrl,
|
||||
};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use rsa::{pkcs1::ToRsaPrivateKey, RsaPrivateKey};
|
||||
use rsa::{
|
||||
pkcs1::{EncodeRsaPrivateKey, LineEnding},
|
||||
RsaPrivateKey,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use siwe::{Message, TimeStamp, Version};
|
||||
use siwe::{Message, TimeStamp, VerificationOpts, Version};
|
||||
use std::{str::FromStr, time};
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
@ -88,7 +92,7 @@ pub enum CustomError {
|
||||
|
||||
fn jwk(private_key: RsaPrivateKey) -> Result<CoreRsaPrivateSigningKey> {
|
||||
let pem = private_key
|
||||
.to_pkcs1_pem()
|
||||
.to_pkcs1_pem(LineEnding::LF)
|
||||
.map_err(|e| anyhow!("Failed to serialise key as PEM: {}", e))?;
|
||||
CoreRsaPrivateSigningKey::from_pem(&pem, Some(JsonWebKeyId::new(KID.to_string())))
|
||||
.map_err(|e| anyhow!("Invalid RSA private key: {}", e))
|
||||
@ -167,32 +171,54 @@ pub fn metadata(base_url: Url) -> Result<CoreProviderMetadata, CustomError> {
|
||||
Ok(pm)
|
||||
}
|
||||
|
||||
async fn resolve_name(eth_provider: Option<Url>, address: H160) -> String {
|
||||
let address_string = to_checksum(&address, None);
|
||||
if eth_provider.is_none() {
|
||||
return address_string;
|
||||
}
|
||||
|
||||
use ethers_providers::{Http, Middleware, Provider};
|
||||
let provider = match Provider::<Http>::try_from(eth_provider.unwrap().to_string()) {
|
||||
Ok(p) => p,
|
||||
fn build_provider(eth_provider: Url) -> Result<Provider<Http>> {
|
||||
match Provider::<Http>::try_from(eth_provider.to_string()) {
|
||||
Ok(p) => Ok(p),
|
||||
Err(e) => {
|
||||
error!("Failed to initialise Eth provider: {}", e);
|
||||
return address_string;
|
||||
}
|
||||
};
|
||||
match provider.lookup_address(address).await {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
error!("Failed to resolve Eth domain: {}", e);
|
||||
address_string
|
||||
Err(e)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_avatar(_eth_provider: Option<Url>, _address: H160) -> Option<Url> {
|
||||
async fn resolve_name(eth_provider: Option<Url>, address: H160) -> Result<String, String> {
|
||||
let address_string = to_checksum(&address, None);
|
||||
let eth_provider = if let Some(p) = eth_provider {
|
||||
p
|
||||
} else {
|
||||
return Err(address_string);
|
||||
};
|
||||
let provider = if let Ok(p) = build_provider(eth_provider) {
|
||||
p
|
||||
} else {
|
||||
return Err(address_string);
|
||||
};
|
||||
match provider.lookup_address(address).await {
|
||||
Ok(n) => Ok(n),
|
||||
Err(e) => {
|
||||
error!("Failed to resolve Eth domain: {}", e);
|
||||
Err(address_string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_avatar(eth_provider: Option<Url>, ens_name: &str) -> Option<Url> {
|
||||
if let Some(provider) = eth_provider {
|
||||
if let Ok(p) = build_provider(provider) {
|
||||
match p.resolve_avatar(ens_name).await {
|
||||
Ok(a) => Some(a),
|
||||
Err(e) => {
|
||||
error!("Could not resolve avatar: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_claims(
|
||||
eth_provider: Option<Url>,
|
||||
@ -204,11 +230,17 @@ async fn resolve_claims(
|
||||
chain_id,
|
||||
to_checksum(&address, None)
|
||||
));
|
||||
let ens_name = resolve_name(eth_provider.clone(), address).await;
|
||||
let username = match ens_name.clone() {
|
||||
Ok(n) | Err(n) => n,
|
||||
};
|
||||
let avatar = match ens_name {
|
||||
Ok(n) => resolve_avatar(eth_provider.clone(), &n).await,
|
||||
Err(_) => None,
|
||||
};
|
||||
StandardClaims::new(subject_id)
|
||||
.set_preferred_username(Some(EndUserUsername::new(
|
||||
resolve_name(eth_provider.clone(), address).await,
|
||||
)))
|
||||
.set_picture(resolve_avatar(eth_provider, address).await.map(|a| {
|
||||
.set_preferred_username(Some(EndUserUsername::new(username)))
|
||||
.set_picture(avatar.map(|a| {
|
||||
let mut avatar_localized = LocalizedClaim::new();
|
||||
avatar_localized.insert(None, EndUserPictureUrl::new(a.to_string()));
|
||||
avatar_localized
|
||||
@ -296,7 +328,7 @@ pub async fn token(
|
||||
.set_auth_time(Some(code_entry.auth_time));
|
||||
|
||||
let pem = private_key
|
||||
.to_pkcs1_pem()
|
||||
.to_pkcs1_pem(LineEnding::LF)
|
||||
.map_err(|e| anyhow!("Failed to serialise key as PEM: {}", e))?;
|
||||
|
||||
let id_token = CoreIdToken::new(
|
||||
@ -519,6 +551,7 @@ pub struct SignInParams {
|
||||
}
|
||||
|
||||
pub async fn sign_in(
|
||||
base_url: &Url,
|
||||
params: SignInParams,
|
||||
// cookies_header: String,
|
||||
cookies: headers::Cookie,
|
||||
@ -572,17 +605,34 @@ pub async fn sign_in(
|
||||
.to_eip4361_message()
|
||||
.map_err(|e| anyhow!("Failed to serialise message: {}", e))?;
|
||||
info!("{}", message);
|
||||
|
||||
let domain = if let Some(d) = base_url.domain() {
|
||||
match d.try_into() {
|
||||
Ok(dd) => Some(dd),
|
||||
Err(e) => {
|
||||
error!("Failed to translate domain into authority: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
message
|
||||
.verify(signature)
|
||||
.map_err(|e| anyhow!("Failed signature validation: {}", e))?;
|
||||
.verify(
|
||||
&signature,
|
||||
&VerificationOpts {
|
||||
domain,
|
||||
nonce: Some(session_entry.siwe_nonce.clone()),
|
||||
timestamp: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed message verification: {}", e))?;
|
||||
|
||||
let domain = params.redirect_uri.url();
|
||||
if *domain != Url::from_str(siwe_cookie.message.resources.get(0).unwrap().as_ref()).unwrap() {
|
||||
return Err(anyhow!("Conflicting domains in message and redirect").into());
|
||||
}
|
||||
if session_entry.siwe_nonce != siwe_cookie.message.nonce {
|
||||
return Err(anyhow!("Conflicting nonces in message and session").into());
|
||||
}
|
||||
|
||||
let code_entry = CodeEntry {
|
||||
address: siwe_cookie.message.address,
|
||||
@ -775,3 +825,29 @@ pub async fn userinfo(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use test_log::test;
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn test_claims() {
|
||||
let res = resolve_claims(
|
||||
Some("https://cloudflare-eth.com".try_into().unwrap()),
|
||||
<[u8; 20]>::from_hex("d8da6bf26964af9d7eed9e03e53415d37aa96045")
|
||||
.unwrap()
|
||||
.into(),
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
res.preferred_username().map(|u| u.to_string()),
|
||||
Some("vitalik.eth".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
res.picture().map(|u| u.get(None).unwrap().as_str()),
|
||||
Some("https://ipfs.io/ipfs/QmSP4nq9fnN9dAiCj42ug9Wa79rqmQerZXZch82VqpiH7U/image.gif")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use headers::{
|
||||
authorization::{Basic, Bearer, Credentials},
|
||||
Authorization, ContentType, Header, HeaderValue,
|
||||
};
|
||||
use rsa::{pkcs1::FromRsaPrivateKey, RsaPrivateKey};
|
||||
use rsa::{pkcs1::DecodeRsaPrivateKey, RsaPrivateKey};
|
||||
use worker::*;
|
||||
|
||||
use super::db::CFClient;
|
||||
@ -324,8 +324,9 @@ pub async fn main(req: Request, env: Env) -> Result<Response> {
|
||||
return Response::error("Missing cookies", 400);
|
||||
}
|
||||
let url = req.url()?;
|
||||
let base_url = ctx.var(BASE_URL_KEY)?.to_string().parse().unwrap();
|
||||
let db_client = CFClient { ctx, url };
|
||||
match oidc::sign_in(params, cookies.unwrap(), &db_client).await {
|
||||
match oidc::sign_in(&base_url, params, cookies.unwrap(), &db_client).await {
|
||||
Ok(url) => Response::redirect(url),
|
||||
Err(e) => e.into(),
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user