Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

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

5 changes: 4 additions & 1 deletion deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ unknown-registry = "deny"
[advisories]
ignore = [
{ id = "RUSTSEC-2023-0071", reason = "a potential timing attack to recover a private key from rsa crate. however, the rsa library is used either for tests or signature verification, so private key material is not exposed." },
{ id = "RUSTSEC-2026-0049", reason = "`rustls-webpki` (requires upstream dep updates); low severity, requires compromised CA (2026-03-23)"}
{ id = "RUSTSEC-2026-0049", reason = "`rustls-webpki` (requires upstream dep updates); low severity, requires compromised CA (2026-03-23)" },
{ id = "RUSTSEC-2026-0098", reason = "`rustls-webpki` 0.101.7 via AWS SDK rustls 0.21.x; no fix in the 0.101.x range, requires upstream AWS SDK to bump rustls" },
{ id = "RUSTSEC-2026-0099", reason = "`rustls-webpki` 0.101.7 via AWS SDK rustls 0.21.x; no fix in the 0.101.x range, requires upstream AWS SDK to bump rustls" },
{ id = "RUSTSEC-2026-0104", reason = "`rustls-webpki` 0.101.7 via AWS SDK rustls 0.21.x; no fix in the 0.101.x range, requires upstream AWS SDK to bump rustls" },
]

[licenses]
Expand Down
10 changes: 8 additions & 2 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ impl AuthHandler {
expected_factor_scope: FactorScope,
expected_challenge_context: ChallengeContext,
challenge_token: String,
client_name: Option<&str>,
) -> Result<(String, BackupMetadata), AuthError> {
// Step 1: Verify that the authorization type is supported
// `ECKeyPair` is the only supported factor type for `Sync` scope, other factors are rejected.
Expand Down Expand Up @@ -184,6 +185,7 @@ impl AuthHandler {
signature,
&challenge_token_payload,
expected_factor_scope,
client_name,
)
.await?
}
Expand Down Expand Up @@ -222,6 +224,7 @@ impl AuthHandler {
expected_challenge_context: ChallengeContext,
turnkey_provider_id: Option<String>,
is_sync_factor: bool,
client_name: Option<&str>,
) -> Result<ValidationResult, AuthError> {
// Step 1: Verify that the authorization type is valid for the factor scope
// Sync factors must be EC keypairs - passkeys and OIDC accounts are not allowed as sync factors
Expand Down Expand Up @@ -267,6 +270,7 @@ impl AuthHandler {
signature,
&challenge_token_payload,
turnkey_provider_id.ok_or_else(|| AuthError::MissingTurnkeyProviderId)?,
client_name,
)
.await?
}
Expand Down Expand Up @@ -439,10 +443,11 @@ impl AuthHandler {
signature: &str,
challenge_token_payload: &[u8],
turnkey_provider_id: String,
client_name: Option<&str>,
) -> Result<(Factor, FactorToLookup), AuthError> {
let claims = self
.oidc_token_verifier
.verify_token(oidc_token, public_key.to_string())
.verify_token(oidc_token, public_key.to_string(), client_name)
.await?;

verify_signature(public_key, signature, challenge_token_payload)?;
Expand Down Expand Up @@ -486,10 +491,11 @@ impl AuthHandler {
signature: &str,
challenge_token_payload: &[u8],
expected_factor_scope: FactorScope,
client_name: Option<&str>,
) -> Result<(String, BackupMetadata), AuthError> {
let claims = self
.oidc_token_verifier
.verify_token(oidc_token, public_key.to_string())
.verify_token(oidc_token, public_key.to_string(), client_name)
.await?;

verify_signature(public_key, signature, challenge_token_payload)?;
Expand Down
4 changes: 4 additions & 0 deletions src/headers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
use http::HeaderName;

pub static CLIENT_NAME: HeaderName = HeaderName::from_static("client-name");
pub static CLIENT_VERSION: HeaderName = HeaderName::from_static("client-version");
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod auth;
pub mod backup_storage;
pub mod challenge_manager;
pub mod factor_lookup;
pub mod headers;
pub mod kms_jwe;
pub mod middleware;
pub mod oidc_nonce_verifier;
Expand Down
100 changes: 75 additions & 25 deletions src/oidc_token_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ impl OidcTokenVerifier {
&self,
token: &OidcToken,
expected_public_key_sec1_base64: String,
client_name: Option<&str>,
) -> Result<IdTokenClaims<EmptyAdditionalClaims, CoreGenderClaim>, OidcTokenVerifierError> {
// Step 1: Extract the token and other parameters based on the OIDC provider
let (oidc_token, jwk_set_url, client_id, issuer_url) = match token {
Expand All @@ -115,20 +116,20 @@ impl OidcTokenVerifier {
OidcToken::Apple { token } => (
token,
self.environment.apple_jwk_set_url(),
self.environment.apple_client_id(),
self.environment.apple_client_id(client_name),
self.environment.apple_issuer_url(),
),
};

// Load the public keys from the OIDC provider
let signature_keys = self.get_jwk_set(&jwk_set_url).await?.as_ref().clone();

// Step 3: Create the token verifier
// Step 3: Create the token verifier.
let token_verifier =
CoreIdTokenVerifier::new_public_client(client_id, issuer_url.clone(), signature_keys)
.set_issue_time_verifier_fn(issue_time_verifier);

// Step 4: Verify the token and extract claims
// Step 4: Parse the OIDC token
let oidc_token = CoreIdToken::from_str(oidc_token).map_err(|err| {
tracing::warn!(message = "Failed to parse OIDC token", err = ?err);
OidcTokenVerifierError::TokenParseError
Expand All @@ -141,11 +142,11 @@ impl OidcTokenVerifier {
OidcNonceVerifier::new(expected_public_key_sec1_base64),
)
.map_err(|err| {
tracing::error!(message = "Token verification error", err = ?err, issuer = ?issuer_url);
tracing::error!(message = "Token verification error", err = ?err, issuer = ?issuer_url, client_name = ?client_name);
match err {
ClaimsVerificationError::InvalidNonce(e) =>
OidcTokenVerifierError::InvalidNonce(e.clone()),
_ => OidcTokenVerifierError::TokenVerificationError
_ => OidcTokenVerifierError::TokenVerificationError,
}
})?;

Expand Down Expand Up @@ -226,16 +227,17 @@ mod tests {
provider: OidcProvider,
token: String,
public_key: String,
client_name: Option<&str>,
) -> Result<IdTokenClaims<EmptyAdditionalClaims, CoreGenderClaim>, OidcTokenVerifierError> {
match provider {
OidcProvider::Google => {
verifier
.verify_token(&OidcToken::Google { token }, public_key)
.verify_token(&OidcToken::Google { token }, public_key, client_name)
.await
}
OidcProvider::Apple => {
verifier
.verify_token(&OidcToken::Apple { token }, public_key)
.verify_token(&OidcToken::Apple { token }, public_key, client_name)
.await
}
}
Expand All @@ -257,8 +259,14 @@ mod tests {
let token = oidc_server.generate_token(provider.into(), None, &public_key);

// Verify the token
let result =
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
let result = verify_token_for_provider(
&verifier,
provider,
token,
public_key.clone(),
Some("ios-id"),
)
.await;

// The test should pass with a valid token
assert!(result.is_ok());
Expand All @@ -281,8 +289,14 @@ mod tests {
let token = oidc_server.generate_expired_token(provider.into());

// Verify the token
let result =
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
let result = verify_token_for_provider(
&verifier,
provider,
token,
public_key.clone(),
Some("ios-id"),
)
.await;

// The test should fail with an expired token
assert!(result.is_err());
Expand All @@ -309,8 +323,14 @@ mod tests {
let token = oidc_server.generate_incorrectly_signed_token(provider.into());

// Verify the token
let result =
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
let result = verify_token_for_provider(
&verifier,
provider,
token,
public_key.clone(),
Some("ios-id"),
)
.await;

// The test should fail with an incorrectly signed token
assert!(result.is_err());
Expand Down Expand Up @@ -338,8 +358,14 @@ mod tests {
oidc_server.generate_token_with_incorrect_issuer(provider.into(), &public_key);

// Verify the token
let result =
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
let result = verify_token_for_provider(
&verifier,
provider,
token,
public_key.clone(),
Some("ios-id"),
)
.await;

// The test should fail with an incorrect issuer
assert!(result.is_err());
Expand Down Expand Up @@ -367,8 +393,14 @@ mod tests {
oidc_server.generate_token_with_incorrect_audience(provider.into(), &public_key);

// Verify the token
let result =
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
let result = verify_token_for_provider(
&verifier,
provider,
token,
public_key.clone(),
Some("ios-id"),
)
.await;

// The test should fail with an incorrect audience
assert!(result.is_err());
Expand Down Expand Up @@ -396,8 +428,14 @@ mod tests {
oidc_server.generate_token_with_incorrect_issued_at(provider.into(), &public_key);

// Verify the token
let result =
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
let result = verify_token_for_provider(
&verifier,
provider,
token,
public_key.clone(),
Some("ios-id"),
)
.await;

// The test should fail with an incorrect issued_at
assert!(result.is_err());
Expand Down Expand Up @@ -431,9 +469,14 @@ mod tests {
let token = oidc_server.generate_token(provider.into(), None, &correct_public_key);

// Verify the token but pass a different public key
let result =
verify_token_for_provider(&verifier, provider, token, incorrect_public_key.clone())
.await;
let result = verify_token_for_provider(
&verifier,
provider,
token,
incorrect_public_key.clone(),
Some("ios-id"),
)
.await;

// The test should fail with an incorrect public key
assert!(result.is_err());
Expand All @@ -460,6 +503,7 @@ mod tests {
OidcProvider::Google,
token.clone(),
public_key.clone(),
None,
)
.await
.unwrap(); // The first time is successful
Expand All @@ -470,6 +514,7 @@ mod tests {
OidcProvider::Google,
token.clone(),
public_key.clone(),
None,
)
.await;
assert!(result.is_err());
Expand All @@ -484,9 +529,14 @@ mod tests {
let new_token = oidc_server.generate_token(OidcProvider::Google.into(), None, &public_key);

assert_ne!(token, new_token);
let result =
verify_token_for_provider(&verifier, OidcProvider::Google, token, public_key.clone())
.await;
let result = verify_token_for_provider(
&verifier,
OidcProvider::Google,
token,
public_key.clone(),
None,
)
.await;
assert!(result.is_err());
assert!(matches!(
result,
Expand Down
9 changes: 8 additions & 1 deletion src/routes/add_factor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::auth::{AuthError, AuthHandler};
use crate::backup_storage::BackupStorage;
use crate::challenge_manager::{ChallengeContext, ChallengeManager, ChallengeType, NewFactorType};
use crate::factor_lookup::{FactorLookup, FactorScope, FactorToLookup};
use crate::headers::CLIENT_NAME;
use crate::turnkey_activity::{
verify_turnkey_activity_parameters, verify_turnkey_activity_webauthn_stamp,
};
Expand All @@ -15,6 +16,7 @@ use axum::{Extension, Json};
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use base64::Engine;
use chrono::Duration;
use http::HeaderMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use webauthn_rs::prelude::PublicKeyCredential;
Expand Down Expand Up @@ -64,8 +66,10 @@ pub async fn handler(
Extension(challenge_manager): Extension<Arc<ChallengeManager>>,
Extension(factor_lookup): Extension<Arc<FactorLookup>>,
Extension(auth_handler): Extension<AuthHandler>,
headers: HeaderMap,
request: Json<AddFactorRequest>,
) -> Result<Json<AddFactorResponse>, ErrorResponse> {
let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok());
Comment thread
aurel-fr marked this conversation as resolved.
// Step 1: Check authorization for the existing factor and get the backup ID
let (backup_id, expected_new_factor) = match &request.existing_factor_authorization {
Authorization::Passkey { credential, .. } => {
Expand Down Expand Up @@ -204,7 +208,9 @@ pub async fn handler(
(backup_id, new_factor_type)
}
Authorization::OidcAccount { .. } | Authorization::EcKeypair { .. } => {
// TODO/FIXME: Implement the logic for verifying the existing factor for OIDC and EC keypair
// TODO/FIXME: Implement the logic for verifying the existing factor for OIDC and EC keypair.
// When implementing OIDC here, pass `client_name` to `validate_oidc_authentication` so the
// correct provider client_id (per `Environment::{google,apple}_client_id`) is used.
return Err(ErrorResponse::bad_request("not_supported", "Not supported"));
}
};
Expand Down Expand Up @@ -252,6 +258,7 @@ pub async fn handler(
ChallengeContext::AddFactorByNewFactor {},
request.turnkey_provider_id.clone(),
false, // not a sync factor
client_name,
)
.await?;

Expand Down
Loading
Loading