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

View File

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

View File

@ -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,31 +171,53 @@ 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(
@ -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")
);
}
}

View File

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