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"
|
anyhow = "1.0.53"
|
||||||
headers = "0.3.6"
|
headers = "0.3.6"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
iri-string = { version = "0.4", features = ["serde-std"] }
|
iri-string = { version = "0.6", features = ["serde"] }
|
||||||
# openidconnect = "2.1.2"
|
# 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"
|
rand = "0.8.4"
|
||||||
rsa = { version = "0.5.0", features = ["alloc"] }
|
rsa = { version = "0.7.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.78"
|
serde_json = "1.0.78"
|
||||||
siwe = "0.2.0"
|
siwe = "0.5.0"
|
||||||
thiserror = "1.0.30"
|
thiserror = "1.0.30"
|
||||||
tracing = "0.1.30"
|
tracing = "0.1.30"
|
||||||
url = { version = "2.2", features = ["serde"] }
|
url = { version = "2.2", features = ["serde"] }
|
||||||
urlencoding = "2.1.0"
|
urlencoding = "2.1.0"
|
||||||
sha2 = "0.9.0"
|
sha2 = "0.10.0"
|
||||||
cookie = "0.16.0"
|
cookie = "0.16.0"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
async-trait = "0.1.52"
|
async-trait = "0.1.52"
|
||||||
ethers-core = "0.6.3"
|
ethers-core = "1.0.2"
|
||||||
ethers-providers = "0.6.2"
|
ethers-providers = "1.0.2"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
@ -56,12 +56,18 @@ matchit = "0.4.2"
|
|||||||
serde_urlencoded = "0.7.0"
|
serde_urlencoded = "0.7.0"
|
||||||
uuid = { version = "0.8", features = ["serde", "v4", "wasm-bindgen"] }
|
uuid = { version = "0.8", features = ["serde", "v4", "wasm-bindgen"] }
|
||||||
wee_alloc = { version = "0.4" }
|
wee_alloc = { version = "0.4" }
|
||||||
worker = "0.0.9"
|
worker = "0.0.12"
|
||||||
|
time = { version = "0.3.17", features = ["wasm-bindgen"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
lto = true
|
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]
|
# [target.'cfg(target_arch = "wasm32")'.profile.release]
|
||||||
# opt-level = "z"
|
# opt-level = "z"
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ use axum::{
|
|||||||
},
|
},
|
||||||
response::{self, IntoResponse, Redirect},
|
response::{self, IntoResponse, Redirect},
|
||||||
routing::{delete, get, get_service, post},
|
routing::{delete, get, get_service, post},
|
||||||
AddExtensionLayer, Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use bb8_redis::{bb8, RedisConnectionManager};
|
use bb8_redis::{bb8, RedisConnectionManager};
|
||||||
use figment::{
|
use figment::{
|
||||||
@ -25,7 +25,7 @@ use openidconnect::core::{
|
|||||||
};
|
};
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use rsa::{
|
use rsa::{
|
||||||
pkcs1::{FromRsaPrivateKey, ToRsaPrivateKey},
|
pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey, LineEnding},
|
||||||
RsaPrivateKey,
|
RsaPrivateKey,
|
||||||
};
|
};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
@ -130,8 +130,9 @@ async fn sign_in(
|
|||||||
Query(params): Query<oidc::SignInParams>,
|
Query(params): Query<oidc::SignInParams>,
|
||||||
TypedHeader(cookies): TypedHeader<headers::Cookie>,
|
TypedHeader(cookies): TypedHeader<headers::Cookie>,
|
||||||
Extension(redis_client): Extension<RedisClient>,
|
Extension(redis_client): Extension<RedisClient>,
|
||||||
|
Extension(config): Extension<config::Config>,
|
||||||
) -> Result<Redirect, CustomError> {
|
) -> 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(
|
Ok(Redirect::to(
|
||||||
url.as_str()
|
url.as_str()
|
||||||
.parse()
|
.parse()
|
||||||
@ -273,7 +274,7 @@ pub async fn main() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
info!("Generated key.");
|
info!("Generated key.");
|
||||||
info!("{:?}", private.to_pkcs1_pem().unwrap());
|
info!("{:?}", private.to_pkcs1_pem(LineEnding::LF).unwrap());
|
||||||
private
|
private
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -355,9 +356,9 @@ pub async fn main() {
|
|||||||
.route(&format!("{}/:id", oidc::CLIENT_PATH), post(client_update))
|
.route(&format!("{}/:id", oidc::CLIENT_PATH), post(client_update))
|
||||||
.route(oidc::SIGNIN_PATH, get(sign_in))
|
.route(oidc::SIGNIN_PATH, get(sign_in))
|
||||||
.route("/health", get(healthcheck))
|
.route("/health", get(healthcheck))
|
||||||
.layer(AddExtensionLayer::new(private_key))
|
.layer(Extension(private_key))
|
||||||
.layer(AddExtensionLayer::new(config.clone()))
|
.layer(Extension(config.clone()))
|
||||||
.layer(AddExtensionLayer::new(redis_client))
|
.layer(Extension(redis_client))
|
||||||
.layer(TraceLayer::new_for_http());
|
.layer(TraceLayer::new_for_http());
|
||||||
|
|
||||||
let addr = SocketAddr::from((config.address, config.port));
|
let addr = SocketAddr::from((config.address, config.port));
|
||||||
|
140
src/oidc.rs
140
src/oidc.rs
@ -2,6 +2,7 @@ use anyhow::{anyhow, Result};
|
|||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use cookie::{Cookie, SameSite};
|
use cookie::{Cookie, SameSite};
|
||||||
use ethers_core::{types::H160, utils::to_checksum};
|
use ethers_core::{types::H160, utils::to_checksum};
|
||||||
|
use ethers_providers::{Http, Middleware, Provider};
|
||||||
use headers::{self, authorization::Bearer};
|
use headers::{self, authorization::Bearer};
|
||||||
use hex::FromHex;
|
use hex::FromHex;
|
||||||
use iri_string::types::UriString;
|
use iri_string::types::UriString;
|
||||||
@ -23,9 +24,12 @@ use openidconnect::{
|
|||||||
ResponseTypes, Scope, StandardClaims, SubjectIdentifier, TokenUrl, UserInfoUrl,
|
ResponseTypes, Scope, StandardClaims, SubjectIdentifier, TokenUrl, UserInfoUrl,
|
||||||
};
|
};
|
||||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
use rsa::{pkcs1::ToRsaPrivateKey, RsaPrivateKey};
|
use rsa::{
|
||||||
|
pkcs1::{EncodeRsaPrivateKey, LineEnding},
|
||||||
|
RsaPrivateKey,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use siwe::{Message, TimeStamp, Version};
|
use siwe::{Message, TimeStamp, VerificationOpts, Version};
|
||||||
use std::{str::FromStr, time};
|
use std::{str::FromStr, time};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
@ -88,7 +92,7 @@ pub enum CustomError {
|
|||||||
|
|
||||||
fn jwk(private_key: RsaPrivateKey) -> Result<CoreRsaPrivateSigningKey> {
|
fn jwk(private_key: RsaPrivateKey) -> Result<CoreRsaPrivateSigningKey> {
|
||||||
let pem = private_key
|
let pem = private_key
|
||||||
.to_pkcs1_pem()
|
.to_pkcs1_pem(LineEnding::LF)
|
||||||
.map_err(|e| anyhow!("Failed to serialise key as PEM: {}", e))?;
|
.map_err(|e| anyhow!("Failed to serialise key as PEM: {}", e))?;
|
||||||
CoreRsaPrivateSigningKey::from_pem(&pem, Some(JsonWebKeyId::new(KID.to_string())))
|
CoreRsaPrivateSigningKey::from_pem(&pem, Some(JsonWebKeyId::new(KID.to_string())))
|
||||||
.map_err(|e| anyhow!("Invalid RSA private key: {}", e))
|
.map_err(|e| anyhow!("Invalid RSA private key: {}", e))
|
||||||
@ -167,31 +171,53 @@ pub fn metadata(base_url: Url) -> Result<CoreProviderMetadata, CustomError> {
|
|||||||
Ok(pm)
|
Ok(pm)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn resolve_name(eth_provider: Option<Url>, address: H160) -> String {
|
fn build_provider(eth_provider: Url) -> Result<Provider<Http>> {
|
||||||
let address_string = to_checksum(&address, None);
|
match Provider::<Http>::try_from(eth_provider.to_string()) {
|
||||||
if eth_provider.is_none() {
|
Ok(p) => Ok(p),
|
||||||
return address_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
use ethers_providers::{Http, Middleware, Provider};
|
|
||||||
let provider = match Provider::<Http>::try_from(eth_provider.unwrap().to_string()) {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to initialise Eth provider: {}", e);
|
error!("Failed to initialise Eth provider: {}", e);
|
||||||
return address_string;
|
Err(e)?
|
||||||
}
|
|
||||||
};
|
|
||||||
match provider.lookup_address(address).await {
|
|
||||||
Ok(n) => n,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to resolve Eth domain: {}", e);
|
|
||||||
address_string
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
None
|
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(
|
async fn resolve_claims(
|
||||||
@ -204,11 +230,17 @@ async fn resolve_claims(
|
|||||||
chain_id,
|
chain_id,
|
||||||
to_checksum(&address, None)
|
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)
|
StandardClaims::new(subject_id)
|
||||||
.set_preferred_username(Some(EndUserUsername::new(
|
.set_preferred_username(Some(EndUserUsername::new(username)))
|
||||||
resolve_name(eth_provider.clone(), address).await,
|
.set_picture(avatar.map(|a| {
|
||||||
)))
|
|
||||||
.set_picture(resolve_avatar(eth_provider, address).await.map(|a| {
|
|
||||||
let mut avatar_localized = LocalizedClaim::new();
|
let mut avatar_localized = LocalizedClaim::new();
|
||||||
avatar_localized.insert(None, EndUserPictureUrl::new(a.to_string()));
|
avatar_localized.insert(None, EndUserPictureUrl::new(a.to_string()));
|
||||||
avatar_localized
|
avatar_localized
|
||||||
@ -296,7 +328,7 @@ pub async fn token(
|
|||||||
.set_auth_time(Some(code_entry.auth_time));
|
.set_auth_time(Some(code_entry.auth_time));
|
||||||
|
|
||||||
let pem = private_key
|
let pem = private_key
|
||||||
.to_pkcs1_pem()
|
.to_pkcs1_pem(LineEnding::LF)
|
||||||
.map_err(|e| anyhow!("Failed to serialise key as PEM: {}", e))?;
|
.map_err(|e| anyhow!("Failed to serialise key as PEM: {}", e))?;
|
||||||
|
|
||||||
let id_token = CoreIdToken::new(
|
let id_token = CoreIdToken::new(
|
||||||
@ -519,6 +551,7 @@ pub struct SignInParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign_in(
|
pub async fn sign_in(
|
||||||
|
base_url: &Url,
|
||||||
params: SignInParams,
|
params: SignInParams,
|
||||||
// cookies_header: String,
|
// cookies_header: String,
|
||||||
cookies: headers::Cookie,
|
cookies: headers::Cookie,
|
||||||
@ -572,17 +605,34 @@ pub async fn sign_in(
|
|||||||
.to_eip4361_message()
|
.to_eip4361_message()
|
||||||
.map_err(|e| anyhow!("Failed to serialise message: {}", e))?;
|
.map_err(|e| anyhow!("Failed to serialise message: {}", e))?;
|
||||||
info!("{}", message);
|
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
|
message
|
||||||
.verify(signature)
|
.verify(
|
||||||
.map_err(|e| anyhow!("Failed signature validation: {}", e))?;
|
&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();
|
let domain = params.redirect_uri.url();
|
||||||
if *domain != Url::from_str(siwe_cookie.message.resources.get(0).unwrap().as_ref()).unwrap() {
|
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());
|
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 {
|
let code_entry = CodeEntry {
|
||||||
address: siwe_cookie.message.address,
|
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::{Basic, Bearer, Credentials},
|
||||||
Authorization, ContentType, Header, HeaderValue,
|
Authorization, ContentType, Header, HeaderValue,
|
||||||
};
|
};
|
||||||
use rsa::{pkcs1::FromRsaPrivateKey, RsaPrivateKey};
|
use rsa::{pkcs1::DecodeRsaPrivateKey, RsaPrivateKey};
|
||||||
use worker::*;
|
use worker::*;
|
||||||
|
|
||||||
use super::db::CFClient;
|
use super::db::CFClient;
|
||||||
@ -324,8 +324,9 @@ pub async fn main(req: Request, env: Env) -> Result<Response> {
|
|||||||
return Response::error("Missing cookies", 400);
|
return Response::error("Missing cookies", 400);
|
||||||
}
|
}
|
||||||
let url = req.url()?;
|
let url = req.url()?;
|
||||||
|
let base_url = ctx.var(BASE_URL_KEY)?.to_string().parse().unwrap();
|
||||||
let db_client = CFClient { ctx, url };
|
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),
|
Ok(url) => Response::redirect(url),
|
||||||
Err(e) => e.into(),
|
Err(e) => e.into(),
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user