Enable avatar resolution (#45)

Also updates a variety of dependencies
This commit is contained in:
Simon Bihel 2022-12-19 18:51:46 +00:00 committed by GitHub
parent 782143415c
commit d59c4db602
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 1135 additions and 819 deletions

1772
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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));

View File

@ -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")
);
}
}

View File

@ -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(),
} }