Add ENS domain information

This commit is contained in:
Simon Bihel
2022-01-25 16:29:02 +00:00
parent 452bd2d9fb
commit b34027b096
9 changed files with 447 additions and 43 deletions

View File

@@ -108,6 +108,7 @@ async fn token(
private_key,
config.base_url,
config.require_secret,
config.eth_provider,
&redis_client,
)
.await?;
@@ -272,6 +273,7 @@ async fn userinfo(
};
let claims = oidc::userinfo(
config.base_url,
config.eth_provider,
private_key,
bearer.map(|b| b.0 .0),
payload,

View File

@@ -15,6 +15,7 @@ pub struct Config {
pub default_clients: HashMap<String, String>,
// TODO secret is more complicated than that, and needs to be in the well-known config
pub require_secret: bool,
pub eth_provider: Option<Url>,
}
impl Default for Config {
@@ -27,6 +28,7 @@ impl Default for Config {
redis_url: Url::parse("redis://localhost").unwrap(),
default_clients: HashMap::default(),
require_secret: false,
eth_provider: None,
}
}
}

View File

@@ -1,6 +1,7 @@
use anyhow::Result;
use async_trait::async_trait;
use chrono::{offset::Utc, DateTime};
use ethers_core::types::H160;
use openidconnect::{core::CoreClientMetadata, Nonce};
use serde::{Deserialize, Serialize};
@@ -19,7 +20,7 @@ pub const ENTRY_LIFETIME: usize = 30;
#[derive(Clone, Serialize, Deserialize)]
pub struct CodeEntry {
pub exchange_count: usize,
pub address: String,
pub address: H160,
pub nonce: Option<Nonce>,
pub client_id: String,
pub auth_time: DateTime<Utc>,

View File

@@ -1,5 +1,6 @@
use anyhow::{anyhow, Result};
use chrono::{Duration, Utc};
use ethers_core::{types::H160, utils::to_checksum};
use headers::{self, authorization::Bearer};
use hex::FromHex;
use iri_string::types::UriString;
@@ -15,16 +16,16 @@ use openidconnect::{
registration::{EmptyAdditionalClientMetadata, EmptyAdditionalClientRegistrationResponse},
url::Url,
AccessToken, Audience, AuthUrl, ClientId, ClientSecret, EmptyAdditionalClaims,
EmptyAdditionalProviderMetadata, EmptyExtraTokenFields, IssuerUrl, JsonWebKeyId,
JsonWebKeySetUrl, Nonce, PrivateSigningKey, RedirectUrl, RegistrationUrl, RequestUrl,
ResponseTypes, Scope, StandardClaims, SubjectIdentifier, TokenUrl, UserInfoUrl,
EmptyAdditionalProviderMetadata, EmptyExtraTokenFields, EndUserUsername, IssuerUrl,
JsonWebKeyId, JsonWebKeySetUrl, Nonce, PrivateSigningKey, RedirectUrl, RegistrationUrl,
RequestUrl, ResponseTypes, Scope, StandardClaims, SubjectIdentifier, TokenUrl, UserInfoUrl,
};
use rsa::{pkcs1::ToRsaPrivateKey, RsaPrivateKey};
use serde::{Deserialize, Serialize};
use siwe::eip4361::{Message, Version};
use std::{str::FromStr, time};
use thiserror::Error;
use tracing::info;
use tracing::{error, info};
use urlencoding::decode;
use uuid::Uuid;
@@ -33,6 +34,12 @@ use super::db::*;
#[cfg(not(target_arch = "wasm32"))]
use siwe_oidc::db::*;
lazy_static::lazy_static! {
static ref SCOPES: [Scope; 2] = [
Scope::new("openid".to_string()),
Scope::new("profile".to_string()),
];
}
const SIGNING_ALG: [CoreJwsSigningAlgorithm; 1] = [CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256];
const KID: &str = "key1";
pub const METADATA_PATH: &str = "/.well-known/openid-configuration";
@@ -100,6 +107,7 @@ pub fn metadata(base_url: Url) -> Result<CoreProviderMetadata, CustomError> {
),
vec![
ResponseTypes::new(vec![CoreResponseType::Code]),
ResponseTypes::new(vec![CoreResponseType::IdToken]),
ResponseTypes::new(vec![CoreResponseType::Token, CoreResponseType::IdToken]),
],
vec![CoreSubjectIdentifierType::Pairwise],
@@ -117,24 +125,14 @@ pub fn metadata(base_url: Url) -> Result<CoreProviderMetadata, CustomError> {
.map_err(|e| anyhow!("Unable to join URL: {}", e))?,
)))
.set_userinfo_signing_alg_values_supported(Some(SIGNING_ALG.to_vec()))
.set_scopes_supported(Some(vec![
Scope::new("openid".to_string()),
// Scope::new("email".to_string()),
// Scope::new("profile".to_string()),
]))
.set_scopes_supported(Some(SCOPES.to_vec()))
.set_claims_supported(Some(vec![
CoreClaimName::new("sub".to_string()),
CoreClaimName::new("aud".to_string()),
// CoreClaimName::new("email".to_string()),
// CoreClaimName::new("email_verified".to_string()),
CoreClaimName::new("exp".to_string()),
CoreClaimName::new("iat".to_string()),
CoreClaimName::new("iss".to_string()),
// CoreClaimName::new("name".to_string()),
// CoreClaimName::new("given_name".to_string()),
// CoreClaimName::new("family_name".to_string()),
// CoreClaimName::new("picture".to_string()),
// CoreClaimName::new("locale".to_string()),
CoreClaimName::new("preferred_username".to_string()),
]))
.set_registration_endpoint(Some(RegistrationUrl::from_url(
base_url
@@ -150,6 +148,29 @@ 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,
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
}
}
}
#[derive(Serialize, Deserialize)]
pub struct TokenForm {
pub code: String,
@@ -165,6 +186,7 @@ pub async fn token(
private_key: RsaPrivateKey,
base_url: Url,
require_secret: bool,
eth_provider: Option<Url>,
db_client: &DBClientType,
) -> Result<CoreTokenResponse, CustomError> {
let code_entry = if let Some(c) = db_client.get_code(form.code.to_string()).await? {
@@ -218,7 +240,13 @@ pub async fn token(
vec![Audience::new(client_id.clone())],
Utc::now() + Duration::seconds(60),
Utc::now(),
StandardClaims::new(SubjectIdentifier::new(code_entry.address)),
StandardClaims::new(SubjectIdentifier::new(to_checksum(
&code_entry.address,
None,
)))
.set_preferred_username(Some(EndUserUsername::new(
resolve_name(eth_provider, code_entry.address).await,
))),
EmptyAdditionalClaims {},
)
.set_nonce(code_entry.nonce)
@@ -340,8 +368,10 @@ pub async fn authorize(
}
let _response_type = params.response_type.as_ref().unwrap();
if params.scope != Scope::new("openid".to_string()) {
return Err(anyhow!("Scope not supported").into());
for scope in params.scope.as_str().split(' ') {
if !SCOPES.contains(&Scope::new(scope.to_string())) {
return Err(anyhow!("Scope not supported: {}", scope).into());
}
}
let domain = params.redirect_uri.url().host().unwrap();
@@ -366,7 +396,7 @@ pub struct SiweCookie {
#[serde(rename_all = "camelCase")]
struct Web3ModalMessage {
pub domain: String,
pub address: String,
pub address: H160,
pub statement: String,
pub uri: String,
pub version: String,
@@ -394,7 +424,7 @@ impl Web3ModalMessage {
Ok(Message {
domain: self.domain.clone().try_into()?,
address: <[u8; 20]>::from_hex(self.address.chars().skip(2).collect::<String>())?,
address: self.address.0,
statement: self.statement.to_string(),
uri: UriString::from_str(&self.uri)?,
version: Version::from_str(&self.version)?,
@@ -529,6 +559,7 @@ pub enum UserInfoResponse {
pub async fn userinfo(
base_url: Url,
eth_provider: Option<Url>,
private_key: RsaPrivateKey,
bearer: Option<Bearer>,
payload: UserInfoPayload,
@@ -554,7 +585,13 @@ pub async fn userinfo(
};
let response = CoreUserInfoClaims::new(
StandardClaims::new(SubjectIdentifier::new(code_entry.address)),
StandardClaims::new(SubjectIdentifier::new(to_checksum(
&code_entry.address,
None,
)))
.set_preferred_username(Some(EndUserUsername::new(
resolve_name(eth_provider, code_entry.address).await,
))),
EmptyAdditionalClaims::default(),
)
.set_issuer(Some(IssuerUrl::from_url(base_url.clone())))

View File

@@ -12,6 +12,7 @@ use super::db::CFClient;
use super::oidc::{self, CustomError, TokenForm, UserInfoPayload};
const BASE_URL_KEY: &str = "BASE_URL";
const ETH_PROVIDER_KEY: &str = "ETH_PROVIDER";
const RSA_PEM_KEY: &str = "RSA_PEM";
// https://github.com/cloudflare/workers-rs/issues/64
@@ -61,12 +62,25 @@ pub async fn main(req: Request, env: Env) -> Result<Response> {
UserInfoPayload { access_token: None }
};
let base_url = ctx.var(BASE_URL_KEY)?.to_string().parse().unwrap();
let eth_provider = ctx
.var(ETH_PROVIDER_KEY)
.map(|p| p.to_string().parse().unwrap())
.ok();
let private_key = RsaPrivateKey::from_pkcs1_pem(&ctx.secret(RSA_PEM_KEY)?.to_string())
.map_err(|e| anyhow!("Failed to load private key: {}", e))
.unwrap();
let url = req.url()?;
let db_client = CFClient { ctx, url };
match oidc::userinfo(base_url, private_key, bearer, payload, &db_client).await {
match oidc::userinfo(
base_url,
eth_provider,
private_key,
bearer,
payload,
&db_client,
)
.await
{
Ok(oidc::UserInfoResponse::Json(r)) => Ok(Response::from_json(&r)?),
Ok(oidc::UserInfoResponse::Jwt(r)) => {
let mut headers = Headers::new();
@@ -144,6 +158,10 @@ pub async fn main(req: Request, env: Env) -> Result<Response> {
.unwrap();
let base_url = ctx.var(BASE_URL_KEY)?.to_string().parse().unwrap();
let url = req.url()?;
let eth_provider = ctx
.var(ETH_PROVIDER_KEY)
.map(|p| p.to_string().parse().unwrap())
.ok();
let db_client = CFClient { ctx, url };
let token_response = oidc::token(
TokenForm {
@@ -156,6 +174,7 @@ pub async fn main(req: Request, env: Env) -> Result<Response> {
private_key,
base_url,
false,
eth_provider,
&db_client,
)
.await;