Skip to content
Open
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
11 changes: 9 additions & 2 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ impl AuthHandler {
///
/// This is called when creating a new backup with fresh factors or when adding a new `Sync` or `Main` factor to an existing backup.
///
/// `consume_oidc_nonce` should normally be `true`. Pass `false` when the same OIDC ID token /
/// session keypair already had its nonce marked used earlier in this request (same-account
/// add-factor upgrade using one sign-in).
///
/// # Errors
/// Returns error if the factor is not valid, or is improperly authenticated (following each factor type's specific rules).
pub async fn validate_factor_registration(
Expand All @@ -222,6 +226,7 @@ impl AuthHandler {
expected_challenge_context: ChallengeContext,
turnkey_provider_id: Option<String>,
is_sync_factor: bool,
consume_oidc_nonce: bool,
) -> 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 +272,7 @@ impl AuthHandler {
signature,
&challenge_token_payload,
turnkey_provider_id.ok_or_else(|| AuthError::MissingTurnkeyProviderId)?,
consume_oidc_nonce,
)
.await?
}
Expand Down Expand Up @@ -439,10 +445,11 @@ impl AuthHandler {
signature: &str,
challenge_token_payload: &[u8],
turnkey_provider_id: String,
consume_oidc_nonce: bool,
) -> 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(), consume_oidc_nonce)
.await?;

verify_signature(public_key, signature, challenge_token_payload)?;
Expand Down Expand Up @@ -489,7 +496,7 @@ impl AuthHandler {
) -> 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(), true)
.await?;

verify_signature(public_key, signature, challenge_token_payload)?;
Expand Down
33 changes: 21 additions & 12 deletions src/oidc_token_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,15 @@ impl OidcTokenVerifier {
///
/// # Errors
/// - `OidcTokenVerifierError`s will be raised if the token is not valid or the nonce has been used before.
///
/// Set `consume_nonce` to `false` when the nonce was already marked used earlier in the same
/// request (e.g. same OIDC session authorizing both existing and new factor sides of add-factor).
/// Cryptographic nonce↔session-key binding is still verified.
pub async fn verify_token(
&self,
token: &OidcToken,
expected_public_key_sec1_base64: String,
consume_nonce: bool,
) -> 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 Down Expand Up @@ -149,15 +154,19 @@ impl OidcTokenVerifier {
}
})?;

// Step 6: Track the nonce to prevent replays
let nonce = claims
.nonce()
.ok_or(OidcTokenVerifierError::MissingNonce)?
.secret();

self.redis_cache_manager
.use_oidc_nonce(nonce, &token.into())
.await?;
// Step 6: Track the nonce to prevent replays (unless already consumed in this request).
if consume_nonce {
let nonce = claims
.nonce()
.ok_or(OidcTokenVerifierError::MissingNonce)?
.secret();

self.redis_cache_manager
.use_oidc_nonce(nonce, &token.into())
.await?;
} else if claims.nonce().is_none() {
return Err(OidcTokenVerifierError::MissingNonce);
}

Ok(claims.clone())
}
Expand Down Expand Up @@ -259,13 +268,13 @@ mod tests {
match provider {
OidcProvider::Google => {
verifier
.verify_token(&OidcToken::Google { token }, public_key)
.verify_token(&OidcToken::Google { token }, public_key, true)
.await
}
OidcProvider::Apple => {
verifier
// Use the default
.verify_token(&OidcToken::Apple { token, aud }, public_key)
.verify_token(&OidcToken::Apple { token, aud }, public_key, true)
.await
}
}
Expand Down Expand Up @@ -354,7 +363,7 @@ mod tests {
let oidc_token: OidcToken = serde_json::from_str(&json).unwrap();
assert!(matches!(oidc_token, OidcToken::Apple { aud: None, .. }));

let result = verifier.verify_token(&oidc_token, public_key).await;
let result = verifier.verify_token(&oidc_token, public_key, true).await;

assert!(result.is_ok());
}
Expand Down
Loading