Skip to content

Commit

Permalink
Fix the endpoint for short-lived tokens (#1907)
Browse files Browse the repository at this point in the history
Co-authored-by: Tyler Cloutier <[email protected]>
  • Loading branch information
jsdt and cloutiertyler authored Oct 25, 2024
1 parent 839aa99 commit 25ea57d
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 84 deletions.
67 changes: 56 additions & 11 deletions crates/client-api/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl SpacetimeCreds {
pub fn decode_token(&self, public_key: &DecodingKey) -> Result<SpacetimeIdentityClaims, JwtError> {
decode_token(public_key, self.token()).map(|x| x.claims)
}
fn from_signed_token(token: String) -> Self {
pub fn from_signed_token(token: String) -> Self {
Self { token }
}
/// Mint a new credentials JWT for an identity.
Expand Down Expand Up @@ -98,43 +98,75 @@ impl SpacetimeCreds {
pub struct SpacetimeAuth {
pub creds: SpacetimeCreds,
pub identity: Identity,
pub subject: String,
pub issuer: String,
}

use jsonwebtoken;

struct TokenClaims {
pub struct TokenClaims {
pub issuer: String,
pub subject: String,
pub audience: Vec<String>,
}

impl From<SpacetimeAuth> for TokenClaims {
fn from(claims: SpacetimeAuth) -> Self {
Self {
issuer: claims.issuer,
subject: claims.subject,
// This will need to be changed when we care about audiencies.
audience: Vec::new(),
}
}
}

impl TokenClaims {
pub fn new(issuer: String, subject: String) -> Self {
Self {
issuer,
subject,
audience: Vec::new(),
}
}

// Compute the id from the issuer and subject.
fn id(&self) -> Identity {
pub fn id(&self) -> Identity {
Identity::from_claims(&self.issuer, &self.subject)
}

fn encode_and_sign(&self, private_key: &EncodingKey) -> Result<String, JwtError> {
pub fn encode_and_sign_with_expiry(
&self,
private_key: &EncodingKey,
expiry: Option<Duration>,
) -> Result<String, JwtError> {
let iat = SystemTime::now();
let exp = expiry.map(|dur| iat + dur);
let claims = SpacetimeIdentityClaims2 {
identity: self.id(),
subject: self.subject.clone(),
issuer: self.issuer.clone(),
audience: self.audience.clone(),
iat: SystemTime::now(),
exp: None,
iat,
exp,
};
let header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::ES256);
jsonwebtoken::encode(&header, &claims, private_key)
}

pub fn encode_and_sign(&self, private_key: &EncodingKey) -> Result<String, JwtError> {
self.encode_and_sign_with_expiry(private_key, None)
}
}

impl SpacetimeAuth {
/// Allocate a new identity, and mint a new token for it.
pub async fn alloc(ctx: &(impl NodeDelegate + ControlStateDelegate + ?Sized)) -> axum::response::Result<Self> {
// Generate claims with a random subject.
let subject = Uuid::new_v4().to_string();
let claims = TokenClaims {
issuer: ctx.local_issuer(),
subject: Uuid::new_v4().to_string(),
subject: subject.clone(),
// Placeholder audience.
audience: vec!["spacetimedb".to_string()],
};
Expand All @@ -145,17 +177,28 @@ impl SpacetimeAuth {
SpacetimeCreds::from_signed_token(token)
};

Ok(Self { creds, identity })
Ok(Self {
creds,
identity,
subject,
issuer: ctx.local_issuer(),
})
}

/// Get the auth credentials as headers to be returned from an endpoint.
pub fn into_headers(self) -> (TypedHeader<SpacetimeIdentity>, TypedHeader<SpacetimeIdentityToken>) {
let Self { creds, identity } = self;
(
TypedHeader(SpacetimeIdentity(identity)),
TypedHeader(SpacetimeIdentityToken(creds)),
TypedHeader(SpacetimeIdentity(self.identity)),
TypedHeader(SpacetimeIdentityToken(self.creds)),
)
}

// Sign a new token with the same claims and a new expiry.
// Note that this will not change the issuer, so the private_key might not match.
// We do this to create short-lived tokens that we will be able to verify.
pub fn re_sign_with_expiry(&self, private_key: &EncodingKey, expiry: Duration) -> Result<String, JwtError> {
TokenClaims::from(self.clone()).encode_and_sign_with_expiry(private_key, Some(expiry))
}
}

#[cfg(test)]
Expand Down Expand Up @@ -222,6 +265,8 @@ impl<S: NodeDelegate + Send + Sync> axum::extract::FromRequestParts<S> for Space
let auth = SpacetimeAuth {
creds,
identity: claims.identity,
subject: claims.subject,
issuer: claims.issuer,
};
Ok(Self { auth: Some(auth) })
}
Expand Down
9 changes: 7 additions & 2 deletions crates/client-api/src/routes/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use http::header::CONTENT_TYPE;
use http::StatusCode;
use serde::{Deserialize, Serialize};

use spacetimedb::auth::identity::encode_token_with_expiry;
use spacetimedb_lib::de::serde::DeserializeWrapper;
use spacetimedb_lib::Identity;

Expand Down Expand Up @@ -96,12 +95,18 @@ pub struct WebsocketTokenResponse {
pub token: String,
}

// This endpoint takes a token from a client and sends a newly signed token with a 60s expiry.
// Note that even if the token has a different issuer, we will sign it with our key.
// This is ok because `FullTokenValidator` checks if we signed the token before worrying about the issuer.
pub async fn create_websocket_token<S: NodeDelegate>(
State(ctx): State<S>,
SpacetimeAuthRequired(auth): SpacetimeAuthRequired,
) -> axum::response::Result<impl IntoResponse> {
let expiry = Duration::from_secs(60);
let token = encode_token_with_expiry(ctx.private_key(), auth.identity, Some(expiry)).map_err(log_and_500)?;
let token = auth
.re_sign_with_expiry(ctx.private_key(), expiry)
.map_err(log_and_500)?;
// let token = encode_token_with_expiry(ctx.private_key(), auth.identity, Some(expiry)).map_err(log_and_500)?;
Ok(axum::Json(WebsocketTokenResponse { token }))
}

Expand Down
Loading

2 comments on commit 25ea57d

@github-actions
Copy link

@github-actions github-actions bot commented on 25ea57d Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmarking failed. Please check the workflow run for details.

@github-actions
Copy link

@github-actions github-actions bot commented on 25ea57d Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callgrind benchmark results

Callgrind Benchmark Report

These benchmarks were run using callgrind,
an instruction-level profiler. They allow comparisons between sqlite (sqlite), SpacetimeDB running through a module (stdb_module), and the underlying SpacetimeDB data storage engine (stdb_raw). Callgrind emulates a CPU to collect the below estimates.

Measurement changes larger than five percent are in bold.

In-memory benchmarks

callgrind: empty transaction

db total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw 6516 6516 0.00% 6550 6550 0.00%
sqlite 5579 5579 0.00% 6037 6033 0.07%

callgrind: filter

db schema indices count preload _column data_type total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str no_index 64 128 1 u64 76682 76682 0.00% 77224 77020 0.26%
stdb_raw u32_u64_str no_index 64 128 2 string 120269 119180 0.91% 121021 119684 1.12%
stdb_raw u32_u64_str btree_each_column 64 128 2 string 25251 25231 0.08% 25803 25583 0.86%
stdb_raw u32_u64_str btree_each_column 64 128 1 u64 24199 24199 0.00% 24625 24517 0.44%
sqlite u32_u64_str no_index 64 128 2 string 144695 144695 0.00% 146037 146157 -0.08%
sqlite u32_u64_str no_index 64 128 1 u64 124044 124044 0.00% 125216 125292 -0.06%
sqlite u32_u64_str btree_each_column 64 128 1 u64 131361 131361 0.00% 132681 132749 -0.05%
sqlite u32_u64_str btree_each_column 64 128 2 string 134494 134494 0.00% 135968 136020 -0.04%

callgrind: insert bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 64 128 905746 906633 -0.10% 937174 962157 -2.60%
stdb_raw u32_u64_str btree_each_column 64 128 1055441 1054335 0.10% 1124325 1087655 3.37%
sqlite u32_u64_str unique_0 64 128 398320 398320 0.00% 418520 419248 -0.17%
sqlite u32_u64_str btree_each_column 64 128 983637 983637 0.00% 1024459 1021811 0.26%

callgrind: iterate

db schema indices count total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 153946 153946 0.00% 154044 154012 0.02%
stdb_raw u32_u64_str unique_0 64 16971 16971 0.00% 17053 17021 0.19%
sqlite u32_u64_str unique_0 1024 1067255 1067255 0.00% 1070515 1070519 -0.00%
sqlite u32_u64_str unique_0 64 76201 76219 -0.02% 77111 77245 -0.17%

callgrind: serialize_product_value

count format total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
64 json 47528 47528 0.00% 50218 50214 0.01%
64 bsatn 25509 25509 0.00% 27821 27821 0.00%
16 bsatn 8200 8200 0.00% 9628 9628 0.00%
16 json 12188 12188 0.00% 14126 14126 0.00%

callgrind: update bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 1024 21241622 20838955 1.93% 21944466 21505705 2.04%
stdb_raw u32_u64_str unique_0 64 128 1319269 1318226 0.08% 1367027 1395728 -2.06%
sqlite u32_u64_str unique_0 1024 1024 1802182 1802182 0.00% 1811600 1811732 -0.01%
sqlite u32_u64_str unique_0 64 128 128528 128528 0.00% 131380 131408 -0.02%
On-disk benchmarks

callgrind: empty transaction

db total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw 6521 6521 0.00% 6555 6555 0.00%
sqlite 5627 5621 0.11% 6247 6123 2.03%

callgrind: filter

db schema indices count preload _column data_type total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str no_index 64 128 1 u64 76687 76687 0.00% 77225 77021 0.26%
stdb_raw u32_u64_str no_index 64 128 2 string 119185 119185 0.00% 119921 119665 0.21%
stdb_raw u32_u64_str btree_each_column 64 128 2 string 25237 25257 -0.08% 25753 25609 0.56%
stdb_raw u32_u64_str btree_each_column 64 128 1 u64 24204 24204 0.00% 24634 24526 0.44%
sqlite u32_u64_str no_index 64 128 1 u64 125965 125965 0.00% 127597 127485 0.09%
sqlite u32_u64_str no_index 64 128 2 string 146616 146616 0.00% 148338 148378 -0.03%
sqlite u32_u64_str btree_each_column 64 128 2 string 136616 136616 0.00% 138612 138728 -0.08%
sqlite u32_u64_str btree_each_column 64 128 1 u64 133457 133475 -0.01% 135315 135337 -0.02%

callgrind: insert bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 64 128 855799 854969 0.10% 917141 909213 0.87%
stdb_raw u32_u64_str btree_each_column 64 128 1005674 1006790 -0.11% 1073280 1069406 0.36%
sqlite u32_u64_str unique_0 64 128 415857 415857 0.00% 435543 436191 -0.15%
sqlite u32_u64_str btree_each_column 64 128 1021898 1021898 0.00% 1061506 1058834 0.25%

callgrind: iterate

db schema indices count total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 153951 153951 0.00% 154033 154001 0.02%
stdb_raw u32_u64_str unique_0 64 16976 16976 0.00% 17058 17026 0.19%
sqlite u32_u64_str unique_0 1024 1070323 1070323 0.00% 1074153 1074129 0.00%
sqlite u32_u64_str unique_0 64 77973 77973 0.00% 79235 79259 -0.03%

callgrind: serialize_product_value

count format total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
64 json 47528 47528 0.00% 50218 50214 0.01%
64 bsatn 25509 25509 0.00% 27821 27821 0.00%
16 bsatn 8200 8200 0.00% 9628 9628 0.00%
16 json 12188 12188 0.00% 14126 14126 0.00%

callgrind: update bulk

db schema indices count preload total reads + writes old total reads + writes Δrw estimated cycles old estimated cycles Δcycles
stdb_raw u32_u64_str unique_0 1024 1024 19545197 19543703 0.01% 20285951 20264821 0.10%
stdb_raw u32_u64_str unique_0 64 128 1271549 1271823 -0.02% 1348815 1347875 0.07%
sqlite u32_u64_str unique_0 1024 1024 1809743 1809743 0.00% 1818541 1818453 0.00%
sqlite u32_u64_str unique_0 64 128 132654 132654 0.00% 135678 135622 0.04%

Please sign in to comment.