Skip to content

Commit ee951d3

Browse files
ketzusakaaurel-fr
andauthored
Add support for multiple clients (#201)
* Add support for multiple clients As part of Project Prism we are expanding the apps and will need them all to be able to add backup methods. This updates the backup service to respect the client name when determining the appropriate bundle identifiers for verification. I've added this header to all of the routes for completeness, but add_factor is the primary one we're interested in right now. * docs * chore: tracing * remove google client id mapping, add android-id Google backup is okay. Just need android-id in here so apple backup can be implemented on android devices down the road * fmt --------- Co-authored-by: aurel-fr <105201452+aurel-fr@users.noreply.github.com>
1 parent 290a4b4 commit ee951d3

16 files changed

Lines changed: 254 additions & 56 deletions

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deny.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ unknown-registry = "deny"
88
[advisories]
99
ignore = [
1010
{ 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." },
11-
{ id = "RUSTSEC-2026-0049", reason = "`rustls-webpki` (requires upstream dep updates); low severity, requires compromised CA (2026-03-23)"}
11+
{ id = "RUSTSEC-2026-0049", reason = "`rustls-webpki` (requires upstream dep updates); low severity, requires compromised CA (2026-03-23)" },
12+
{ 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" },
13+
{ 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" },
14+
{ 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" },
1215
]
1316

1417
[licenses]

src/auth.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ impl AuthHandler {
140140
expected_factor_scope: FactorScope,
141141
expected_challenge_context: ChallengeContext,
142142
challenge_token: String,
143+
client_name: Option<&str>,
143144
) -> Result<(String, BackupMetadata), AuthError> {
144145
// Step 1: Verify that the authorization type is supported
145146
// `ECKeyPair` is the only supported factor type for `Sync` scope, other factors are rejected.
@@ -184,6 +185,7 @@ impl AuthHandler {
184185
signature,
185186
&challenge_token_payload,
186187
expected_factor_scope,
188+
client_name,
187189
)
188190
.await?
189191
}
@@ -222,6 +224,7 @@ impl AuthHandler {
222224
expected_challenge_context: ChallengeContext,
223225
turnkey_provider_id: Option<String>,
224226
is_sync_factor: bool,
227+
client_name: Option<&str>,
225228
) -> Result<ValidationResult, AuthError> {
226229
// Step 1: Verify that the authorization type is valid for the factor scope
227230
// Sync factors must be EC keypairs - passkeys and OIDC accounts are not allowed as sync factors
@@ -267,6 +270,7 @@ impl AuthHandler {
267270
signature,
268271
&challenge_token_payload,
269272
turnkey_provider_id.ok_or_else(|| AuthError::MissingTurnkeyProviderId)?,
273+
client_name,
270274
)
271275
.await?
272276
}
@@ -439,10 +443,11 @@ impl AuthHandler {
439443
signature: &str,
440444
challenge_token_payload: &[u8],
441445
turnkey_provider_id: String,
446+
client_name: Option<&str>,
442447
) -> Result<(Factor, FactorToLookup), AuthError> {
443448
let claims = self
444449
.oidc_token_verifier
445-
.verify_token(oidc_token, public_key.to_string())
450+
.verify_token(oidc_token, public_key.to_string(), client_name)
446451
.await?;
447452

448453
verify_signature(public_key, signature, challenge_token_payload)?;
@@ -486,10 +491,11 @@ impl AuthHandler {
486491
signature: &str,
487492
challenge_token_payload: &[u8],
488493
expected_factor_scope: FactorScope,
494+
client_name: Option<&str>,
489495
) -> Result<(String, BackupMetadata), AuthError> {
490496
let claims = self
491497
.oidc_token_verifier
492-
.verify_token(oidc_token, public_key.to_string())
498+
.verify_token(oidc_token, public_key.to_string(), client_name)
493499
.await?;
494500

495501
verify_signature(public_key, signature, challenge_token_payload)?;

src/headers.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
use http::HeaderName;
2+
3+
pub static CLIENT_NAME: HeaderName = HeaderName::from_static("client-name");
4+
pub static CLIENT_VERSION: HeaderName = HeaderName::from_static("client-version");

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod auth;
55
pub mod backup_storage;
66
pub mod challenge_manager;
77
pub mod factor_lookup;
8+
pub mod headers;
89
pub mod kms_jwe;
910
pub mod middleware;
1011
pub mod oidc_nonce_verifier;

src/oidc_token_verifier.rs

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ impl OidcTokenVerifier {
103103
&self,
104104
token: &OidcToken,
105105
expected_public_key_sec1_base64: String,
106+
client_name: Option<&str>,
106107
) -> Result<IdTokenClaims<EmptyAdditionalClaims, CoreGenderClaim>, OidcTokenVerifierError> {
107108
// Step 1: Extract the token and other parameters based on the OIDC provider
108109
let (oidc_token, jwk_set_url, client_id, issuer_url) = match token {
@@ -115,20 +116,20 @@ impl OidcTokenVerifier {
115116
OidcToken::Apple { token } => (
116117
token,
117118
self.environment.apple_jwk_set_url(),
118-
self.environment.apple_client_id(),
119+
self.environment.apple_client_id(client_name),
119120
self.environment.apple_issuer_url(),
120121
),
121122
};
122123

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

126-
// Step 3: Create the token verifier
127+
// Step 3: Create the token verifier.
127128
let token_verifier =
128129
CoreIdTokenVerifier::new_public_client(client_id, issuer_url.clone(), signature_keys)
129130
.set_issue_time_verifier_fn(issue_time_verifier);
130131

131-
// Step 4: Verify the token and extract claims
132+
// Step 4: Parse the OIDC token
132133
let oidc_token = CoreIdToken::from_str(oidc_token).map_err(|err| {
133134
tracing::warn!(message = "Failed to parse OIDC token", err = ?err);
134135
OidcTokenVerifierError::TokenParseError
@@ -141,11 +142,11 @@ impl OidcTokenVerifier {
141142
OidcNonceVerifier::new(expected_public_key_sec1_base64),
142143
)
143144
.map_err(|err| {
144-
tracing::error!(message = "Token verification error", err = ?err, issuer = ?issuer_url);
145+
tracing::error!(message = "Token verification error", err = ?err, issuer = ?issuer_url, client_name = ?client_name);
145146
match err {
146147
ClaimsVerificationError::InvalidNonce(e) =>
147148
OidcTokenVerifierError::InvalidNonce(e.clone()),
148-
_ => OidcTokenVerifierError::TokenVerificationError
149+
_ => OidcTokenVerifierError::TokenVerificationError,
149150
}
150151
})?;
151152

@@ -226,16 +227,17 @@ mod tests {
226227
provider: OidcProvider,
227228
token: String,
228229
public_key: String,
230+
client_name: Option<&str>,
229231
) -> Result<IdTokenClaims<EmptyAdditionalClaims, CoreGenderClaim>, OidcTokenVerifierError> {
230232
match provider {
231233
OidcProvider::Google => {
232234
verifier
233-
.verify_token(&OidcToken::Google { token }, public_key)
235+
.verify_token(&OidcToken::Google { token }, public_key, client_name)
234236
.await
235237
}
236238
OidcProvider::Apple => {
237239
verifier
238-
.verify_token(&OidcToken::Apple { token }, public_key)
240+
.verify_token(&OidcToken::Apple { token }, public_key, client_name)
239241
.await
240242
}
241243
}
@@ -257,8 +259,14 @@ mod tests {
257259
let token = oidc_server.generate_token(provider.into(), None, &public_key);
258260

259261
// Verify the token
260-
let result =
261-
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
262+
let result = verify_token_for_provider(
263+
&verifier,
264+
provider,
265+
token,
266+
public_key.clone(),
267+
Some("ios-id"),
268+
)
269+
.await;
262270

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

283291
// Verify the token
284-
let result =
285-
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
292+
let result = verify_token_for_provider(
293+
&verifier,
294+
provider,
295+
token,
296+
public_key.clone(),
297+
Some("ios-id"),
298+
)
299+
.await;
286300

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

311325
// Verify the token
312-
let result =
313-
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
326+
let result = verify_token_for_provider(
327+
&verifier,
328+
provider,
329+
token,
330+
public_key.clone(),
331+
Some("ios-id"),
332+
)
333+
.await;
314334

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

340360
// Verify the token
341-
let result =
342-
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
361+
let result = verify_token_for_provider(
362+
&verifier,
363+
provider,
364+
token,
365+
public_key.clone(),
366+
Some("ios-id"),
367+
)
368+
.await;
343369

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

369395
// Verify the token
370-
let result =
371-
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
396+
let result = verify_token_for_provider(
397+
&verifier,
398+
provider,
399+
token,
400+
public_key.clone(),
401+
Some("ios-id"),
402+
)
403+
.await;
372404

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

398430
// Verify the token
399-
let result =
400-
verify_token_for_provider(&verifier, provider, token, public_key.clone()).await;
431+
let result = verify_token_for_provider(
432+
&verifier,
433+
provider,
434+
token,
435+
public_key.clone(),
436+
Some("ios-id"),
437+
)
438+
.await;
401439

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

433471
// Verify the token but pass a different public key
434-
let result =
435-
verify_token_for_provider(&verifier, provider, token, incorrect_public_key.clone())
436-
.await;
472+
let result = verify_token_for_provider(
473+
&verifier,
474+
provider,
475+
token,
476+
incorrect_public_key.clone(),
477+
Some("ios-id"),
478+
)
479+
.await;
437480

438481
// The test should fail with an incorrect public key
439482
assert!(result.is_err());
@@ -460,6 +503,7 @@ mod tests {
460503
OidcProvider::Google,
461504
token.clone(),
462505
public_key.clone(),
506+
None,
463507
)
464508
.await
465509
.unwrap(); // The first time is successful
@@ -470,6 +514,7 @@ mod tests {
470514
OidcProvider::Google,
471515
token.clone(),
472516
public_key.clone(),
517+
None,
473518
)
474519
.await;
475520
assert!(result.is_err());
@@ -484,9 +529,14 @@ mod tests {
484529
let new_token = oidc_server.generate_token(OidcProvider::Google.into(), None, &public_key);
485530

486531
assert_ne!(token, new_token);
487-
let result =
488-
verify_token_for_provider(&verifier, OidcProvider::Google, token, public_key.clone())
489-
.await;
532+
let result = verify_token_for_provider(
533+
&verifier,
534+
OidcProvider::Google,
535+
token,
536+
public_key.clone(),
537+
None,
538+
)
539+
.await;
490540
assert!(result.is_err());
491541
assert!(matches!(
492542
result,

src/routes/add_factor.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::auth::{AuthError, AuthHandler};
44
use crate::backup_storage::BackupStorage;
55
use crate::challenge_manager::{ChallengeContext, ChallengeManager, ChallengeType, NewFactorType};
66
use crate::factor_lookup::{FactorLookup, FactorScope, FactorToLookup};
7+
use crate::headers::CLIENT_NAME;
78
use crate::turnkey_activity::{
89
verify_turnkey_activity_parameters, verify_turnkey_activity_webauthn_stamp,
910
};
@@ -15,6 +16,7 @@ use axum::{Extension, Json};
1516
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
1617
use base64::Engine;
1718
use chrono::Duration;
19+
use http::HeaderMap;
1820
use schemars::JsonSchema;
1921
use serde::{Deserialize, Serialize};
2022
use webauthn_rs::prelude::PublicKeyCredential;
@@ -64,8 +66,10 @@ pub async fn handler(
6466
Extension(challenge_manager): Extension<Arc<ChallengeManager>>,
6567
Extension(factor_lookup): Extension<Arc<FactorLookup>>,
6668
Extension(auth_handler): Extension<AuthHandler>,
69+
headers: HeaderMap,
6770
request: Json<AddFactorRequest>,
6871
) -> Result<Json<AddFactorResponse>, ErrorResponse> {
72+
let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok());
6973
// Step 1: Check authorization for the existing factor and get the backup ID
7074
let (backup_id, expected_new_factor) = match &request.existing_factor_authorization {
7175
Authorization::Passkey { credential, .. } => {
@@ -204,7 +208,9 @@ pub async fn handler(
204208
(backup_id, new_factor_type)
205209
}
206210
Authorization::OidcAccount { .. } | Authorization::EcKeypair { .. } => {
207-
// TODO/FIXME: Implement the logic for verifying the existing factor for OIDC and EC keypair
211+
// TODO/FIXME: Implement the logic for verifying the existing factor for OIDC and EC keypair.
212+
// When implementing OIDC here, pass `client_name` to `validate_oidc_authentication` so the
213+
// correct provider client_id (per `Environment::{google,apple}_client_id`) is used.
208214
return Err(ErrorResponse::bad_request("not_supported", "Not supported"));
209215
}
210216
};
@@ -252,6 +258,7 @@ pub async fn handler(
252258
ChallengeContext::AddFactorByNewFactor {},
253259
request.turnkey_provider_id.clone(),
254260
false, // not a sync factor
261+
client_name,
255262
)
256263
.await?;
257264

0 commit comments

Comments
 (0)