Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/namespaced client #2

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
31 changes: 29 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use consts::DEFAULT_AUTH_URL;
pub use error::*;
use ic_agent::{export::Principal, Identity};
use reqs::{EmptyReq, GetUserMetadataReqW, SetUserMetadataReqW, UpgradeRefreshClaimReq};
use reqwest::Url;
use reqwest::{header::{self, HeaderMap}, Url};
use serde::{de::DeserializeOwned, Serialize};
use types::{
metadata::{
Expand All @@ -34,10 +34,14 @@ impl Default for AuthClient {
}

impl AuthClient {
pub fn with_base_url(base_url: Url) -> Self {
pub fn with_base_url(base_url: Url, opt_token: Option<&str>) -> Self {
let mut headers = HeaderMap::new();
if let Some(token) = opt_token {
headers.insert(header::AUTHORIZATION, format!("Bearer {token}").parse().unwrap());
}
Self {
base_url,
client: Default::default(),
client: reqwest::Client::builder().default_headers(headers).build().unwrap(),
}
}

Expand Down
2 changes: 2 additions & 0 deletions ssr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ icondata = "0.3.0"
icondata_core = "0.1.0"
url = "2.5.0"
hmac = { version = "0.12.1", optional = true }
rand = { version = "0.8.5", features = ["std_rng"] }
jsonwebtoken = "9.3.0"

[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate", "ic-agent/wasm-bindgen"]
Expand Down
13 changes: 11 additions & 2 deletions ssr/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,26 @@ use types::{
DelegatedIdentityWire, SignedRefreshTokenClaim,
};

use crate::consts::NAMESPACE;

#[cfg(feature = "ssr")]
use crate::extractors::JwtAuth;

#[cfg(feature = "ssr")]
pub mod server_impl;

#[cfg(feature = "ssr")]
use leptos_axum::extract;

#[server(endpoint = "extract_or_generate", input = Json, output = Json)]
pub async fn extract_or_generate_identity() -> Result<DelegatedIdentityWire, ServerFnError> {
server_impl::extract_or_generate_identity_impl().await
let jwt_auth = extract::<JwtAuth>().await?;
server_impl::extract_or_generate_identity_impl(jwt_auth.namespace).await
}

#[server(endpoint = "logout", input = Json, output = Json)]
pub async fn logout_identity() -> Result<DelegatedIdentityWire, ServerFnError> {
server_impl::logout_identity_impl().await
server_impl::logout_identity_impl(NAMESPACE.into()).await
}

#[server(endpoint = "upgrade_refresh_claim", input = Json, output = Json)]
Expand Down
3 changes: 2 additions & 1 deletion ssr/src/api/server_impl/google.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub async fn perform_google_auth_impl(
.ok_or_else(|| ServerFnError::new("Attempting google login without a temp identity"))?;
let temp_id: TempIdentity = serde_json::from_str(temp_id_cookie.value())?;
let principal = temp_id.principal;
let namespace = temp_id.namespace.clone();
let host = temp_id.referrer_host.clone();
temp_id.validate()?;

Expand All @@ -147,7 +148,7 @@ pub async fn perform_google_auth_impl(
} else {
associate_principal_with_google_sub(&kv, principal, sub_id).await?
};
let claim = refresh_claim(principal, host);
let claim = refresh_claim(principal, host, namespace);
let s_claim = sign_refresh_claim(claim, &key)?;

Ok(s_claim)
Expand Down
95 changes: 69 additions & 26 deletions ssr/src/api/server_impl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use ic_agent::{
export::Principal,
identity::{Delegation, Identity, Secp256k1Identity, SignedDelegation},
};
use k256::sha2::Sha256;
use k256::{elliptic_curve::SecretKey, sha2::{Digest, Sha256}, Secp256k1};
use rand::{rngs::StdRng, SeedableRng};
use leptos::{expect_context, ServerFnError};
use leptos_axum::{extract, extract_with_state, ResponseOptions};
use rand_core::OsRng;
Expand Down Expand Up @@ -59,6 +60,7 @@ pub fn delegate_identity(from: &impl Identity) -> DelegatedIdentityWire {
#[derive(Serialize, Deserialize, Clone)]
pub struct TempIdentity {
pub principal: Principal,
pub namespace: String,
pub signature: Signature,
pub referrer_host: url::Host,
}
Expand Down Expand Up @@ -86,11 +88,12 @@ pub fn set_cookies(resp: &ResponseOptions, jar: impl IntoResponse) {
}

#[cfg(feature = "oauth")]
fn refresh_claim(principal: Principal, referrer_host: url::Host) -> types::RefreshTokenClaim {
fn refresh_claim(principal: Principal, referrer_host: url::Host, namespace: String) -> types::RefreshTokenClaim {
use types::REFRESH_TOKEN_CLAIM_MAX_AGE;

types::RefreshTokenClaim {
principal,
namespace,
expiry_epoch: current_epoch() + REFRESH_TOKEN_CLAIM_MAX_AGE,
referrer_host,
}
Expand Down Expand Up @@ -134,9 +137,10 @@ fn verify_refresh_claim(
Ok(claim.principal)
}

#[derive(Clone, Copy, Deserialize, Serialize)]
#[derive(Clone, Deserialize, Serialize)]
struct RefreshToken {
principal: Principal,
namespace: String,
expiry_epoch_ms: u128,
}

Expand All @@ -153,7 +157,7 @@ async fn extract_principal_from_cookie(
Ok(Some(token.principal))
}

async fn fetch_identity_from_kv(
async fn fetch_identity_key_from_kv(
kv: &KVStoreImpl,
principal: Principal,
) -> Result<Option<k256::SecretKey>, ServerFnError> {
Expand All @@ -171,27 +175,60 @@ pub async fn try_extract_identity(
let Some(principal) = extract_principal_from_cookie(jar).await? else {
return Ok(None);
};
fetch_identity_from_kv(kv, principal).await
fetch_identity_key_from_kv(kv, principal).await
}

async fn generate_and_save_identity(kv: &KVStoreImpl) -> Result<Secp256k1Identity, ServerFnError> {
async fn generate_and_save_identity_key(kv: &KVStoreImpl) -> Result<SecretKey<Secp256k1>, ServerFnError> {
let base_identity_key = k256::SecretKey::random(&mut OsRng);
let base_identity = Secp256k1Identity::from_private_key(base_identity_key.clone());
let principal = base_identity.sender().unwrap();
save_identity_in_kv(kv, base_identity_key.clone()).await?;
Ok(base_identity_key)
}

async fn save_identity_in_kv(kv: &KVStoreImpl, identity_key: SecretKey<Secp256k1>) -> Result<(), ServerFnError> {

let identity = Secp256k1Identity::from_private_key(identity_key.clone());
let principal = identity.sender().unwrap();
let jwk = identity_key.to_jwk_string();
kv.write(principal.to_text(), jwk.to_string()).await?;
Ok(())
}

fn generate_namespaced_identity_key(namespace: &str, from_secret_key: SecretKey<Secp256k1>) -> SecretKey<Secp256k1>{

if namespace.to_uppercase().eq("YRAL") {
return from_secret_key;
}

let app_name = namespace.as_bytes();

let base_jwk = base_identity_key.to_jwk_string();
kv.write(principal.to_text(), base_jwk.to_string()).await?;
Ok(base_identity)
let mut combined_bytes:Vec<u8> = Vec::new();
combined_bytes.extend_from_slice(&from_secret_key.to_bytes());
combined_bytes.extend_from_slice(app_name);

let mut hasher = Sha256::new();
hasher.update(combined_bytes);
let hashed_val = hasher.finalize();

let mut seed = [0u8; 32];
seed.copy_from_slice(&hashed_val[..32]);

k256::SecretKey::random(&mut StdRng::from_seed(seed))
}

pub async fn update_user_identity(


pub async fn set_cookie_and_get_namespaced_identity(
response_opts: &ResponseOptions,
mut jar: SignedCookieJar,
identity: impl Identity,
identity_key: SecretKey<Secp256k1>,
namespace: &str
) -> Result<DelegatedIdentityWire, ServerFnError> {
let refresh_max_age = REFRESH_MAX_AGE;
let identity = Secp256k1Identity::from_private_key(identity_key.clone());
let principal = identity.sender().unwrap();
let refresh_token = RefreshToken {
principal: identity.sender().unwrap(),
principal,
namespace: namespace.to_owned(),
expiry_epoch_ms: (current_epoch() + refresh_max_age).as_millis(),
};
let refresh_token_enc = serde_json::to_string(&refresh_token)?;
Expand All @@ -207,34 +244,37 @@ pub async fn update_user_identity(
jar = jar.add(refresh_cookie);
set_cookies(response_opts, jar);

Ok(delegate_identity(&identity))
let namespaced_identity_key = generate_namespaced_identity_key(&namespace, identity_key);
let namespaced_identity = Secp256k1Identity::from_private_key(namespaced_identity_key);

Ok(delegate_identity(&namespaced_identity))
}

pub async fn extract_or_generate_identity_impl() -> Result<DelegatedIdentityWire, ServerFnError> {
pub async fn extract_or_generate_identity_impl(namespace: String) -> Result<DelegatedIdentityWire, ServerFnError> {
let key: Key = expect_context();
let jar: SignedCookieJar = extract_with_state(&key).await?;
let kv: KVStoreImpl = expect_context();

let base_identity = if let Some(identity) = try_extract_identity(&jar, &kv).await? {
Secp256k1Identity::from_private_key(identity)
let base_identity_key = if let Some(identity) = try_extract_identity(&jar, &kv).await? {
identity
} else {
generate_and_save_identity(&kv).await?
generate_and_save_identity_key(&kv).await?
};

let resp: ResponseOptions = expect_context();
let delegated = update_user_identity(&resp, jar, base_identity).await?;
let delegated = set_cookie_and_get_namespaced_identity(&resp, jar, base_identity_key, &namespace).await?;

Ok(delegated)
}

pub async fn logout_identity_impl() -> Result<DelegatedIdentityWire, ServerFnError> {
pub async fn logout_identity_impl(namespace: String) -> Result<DelegatedIdentityWire, ServerFnError> {
let key: Key = expect_context();
let kv: KVStoreImpl = expect_context();
let jar: SignedCookieJar = extract_with_state(&key).await?;
let base_identity = generate_and_save_identity(&kv).await?;
let base_identity_key = generate_and_save_identity_key(&kv).await?;

let resp: ResponseOptions = expect_context();
let delegated = update_user_identity(&resp, jar, base_identity).await?;
let delegated = set_cookie_and_get_namespaced_identity(&resp, jar, base_identity_key, &namespace).await?;
Ok(delegated)
}

Expand All @@ -257,13 +297,16 @@ pub async fn upgrade_refresh_claim_impl(
.ok_or_else(|| ServerFnError::new("No referrer host"))?
.to_owned();

let principal = verify_refresh_claim(s_claim, host, &key)?;
let sk = fetch_identity_from_kv(&kv, principal)
let principal = verify_refresh_claim(s_claim.clone(), host, &key)?;
let sk = fetch_identity_key_from_kv(&kv, principal)
.await?
.ok_or_else(|| ServerFnError::new("No identity found"))?;


let namespace = s_claim.claim.namespace.clone();

let delegated =
update_user_identity(&resp, jar, Secp256k1Identity::from_private_key(sk)).await?;
set_cookie_and_get_namespaced_identity(&resp, jar, sk, &namespace).await?;
Ok(delegated)
}

Expand Down
1 change: 1 addition & 0 deletions ssr/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub const DELEGATION_MAX_AGE: Duration = Duration::from_secs(60 * 60 * 24 * 7);
pub const REFRESH_MAX_AGE: Duration = Duration::from_secs(60 * 60 * 24 * 30);
pub const REFRESH_TOKEN_COOKIE: &str = "user-identity";
pub const TEMP_IDENTITY_COOKIE: &str = "temp-identity";
pub const NAMESPACE: &str = "YRAL";

#[cfg(all(feature = "ssr", feature = "oauth-google"))]
pub mod google {
Expand Down
39 changes: 39 additions & 0 deletions ssr/src/extractors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::env;

use axum::{async_trait, extract::FromRequestParts};
use http::{header, request::Parts};
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct JwtAuth {
pub namespace: String,
}

pub fn decode_jwt_token(token: &str) -> Result<JwtAuth, String>{
let pub_identity = env::var("YRAL_PUBLIC_KEY").expect("Yral public key should be present");
let decoding_key = DecodingKey::from_ec_pem(pub_identity.as_bytes()).map_err(|e| e.to_string())?;

let token_data = decode::<JwtAuth>(token, &decoding_key, &Validation::new(jsonwebtoken::Algorithm::ES256));
match token_data {
Ok(data) => Ok(data.claims),
Err(e) => Err(e.to_string())
}
}



#[async_trait]
impl FromRequestParts<()> for JwtAuth {
type Rejection = String;

async fn from_request_parts(parts: &mut Parts, _: &()) -> Result<Self, Self::Rejection>{
let access_token = parts.headers.get(header::AUTHORIZATION).and_then(|val| val.to_str().ok()).and_then(|str| str.split(" ").nth(1));
match access_token {
Some(token) => {
decode_jwt_token(token)
},
None => Err("Unauthorized".into())
}
}
}
Loading
Loading