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

Suppressing unexpected error with WebAuthn::PublicKeyCredentialWithAttestation#verify #413

Conversation

soartec-lab
Copy link
Contributor

WebAuthn::PublicKeyCredentialWithAttestation#verify causes NoMethodError if challenge is nil. as below:

undefined method `end_with?' for nil:NilClass

if !str.end_with?("=") && str.length % 4 != 0
           ^^^^^^^^^^
base64.rb:102:in `urlsafe_decode64'

Since challenge is assumed to refer to a value stored in temporary storage such as session, there may be cases where it does not exist by accident.
This change will make the error messages that occur when the above problem occurs more user-friendly and will help users of this library understand the errors.

@santiagorodriguez96
Copy link
Contributor

I wonder how it is possible in a real scenario for the challenge to be nil. Did you run into this issue in a prod environment?

Code looks good to me 🙂

@soartec-lab
Copy link
Contributor Author

@santiagorodriguez96

Thank you for review.

Yes, this issue is an approach to a phenomenon that occurs when operating a production environment.

For example, if you are using external storage for session store. When using an in-memory data store, data
may be deleted if the limit is reached.
Also, since external data stores are accessed via the network, errors are expected.

This is to help troubleshoot unforeseen circumstances and is not something that occurs in routine scenarios.
However, since this is a function that handles authentication, I would like to troubleshoot this issue smoothly.

def self.response_class
WebAuthn::AuthenticatorAttestationResponse
end

def verify(challenge, user_verification: nil)
challenge.is_a?(String) || raise(InvalidChallengeError, "challenge must be a String. input challenge class: #{challenge.class}")
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about using the already existent ChallengeVerificationError?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@santiagorodriguez96

Indeed, I think it is reasonable to handle WebAuthn::ChallengeVerificationError because the meaning of ChallengeVerificationError and the defined level are directly under WebAuthn.

  • The name and responsibility of ChallengeVerificationError is broad. The WebAuthn::PublicKeyCredentialWithAttestation::InvalidChallengeError added this time has a limited class definition and naming, so it helps detailed error analysis.
  • The usage of ChallengeVerificationError is unified in that the class is dynamically selected by the verify_item method. This time, explicitly specifying a different class for a different purpose may cause confusion for users.

After that, I recommend defining a new class like the current implementation.
And, either choice makes sense, so I respect your opinion.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough, there are other cases where we are raising a custom error, for example in https://github.com/cedarcode/webauthn-ruby/blob/23f3be4/lib/webauthn/attestation_statement/base.rb#L163

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thank you. In that case, for the reasons mentioned above, I thought it would be better to add a custom error, which is the current modification content.
What do you think after sharing my opinion?

Copy link
Contributor

Choose a reason for hiding this comment

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

Some part of me thinks that receiving a nil challenge in some sense it is a challenge verification error, thus my initial idea of just re-using the ChallengeVerificationError and, as you are correctly doing, use the message to be specific about what was the underlying issue with the challenge.

However, I do see how using VerificationError classes could cause confusion given that, as you correctly pointed out, VerificationErrors are dynamically raised depending on which verification was the one that failed.

Furthermore, we are already using the pattern of raising an error if there's a validation error, so I'm okay with the approach.

@soartec-lab
Copy link
Contributor Author

The Rubocopoffence was occurring so I fixed it below:

bf16de5

If necessary, squash all commits after all reviews are complete.

@santiagorodriguez96
Copy link
Contributor

I'm just realizing that we should do this same check in lib/webauthn/public_key_credential_with_assertion.rb. Do you mind?

@soartec-lab soartec-lab force-pushed the feature/suppressing-error-in-pb-key-verify branch from 2fb9f92 to 9f86fe4 Compare November 11, 2023 00:42
@soartec-lab
Copy link
Contributor Author

@santiagorodriguez96

of course. Thank you for letting me know.

I fixed it with the commit bellow:

9f86fe4

WebAuthn::PublicKeyCredentialWithAssertion and WebAuthn::PublicKeyCredentialWithAttestation both inherit from WebAuthn::PublicKeyCredential, so the checking process has been moved to WebAuthn::PublicKeyCredential.
At the same time, we have changed the argument of WebAuthn::PublicKeyCredential#verify to receive challenge.

Copy link
Contributor

@santiagorodriguez96 santiagorodriguez96 left a comment

Choose a reason for hiding this comment

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

LGTM. Just one minor comment and I'll proceed to merge it 🙂

Thank you so much! 🙌

@@ -36,7 +38,13 @@ def initialize(
@relying_party = relying_party
end

def verify(*_args)
def verify(challenge, *_args)
unless valid_class?(challenge)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just:

Suggested change
unless valid_class?(challenge)
unless challenge.is_a?(String)

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It follows the existing check processes valid_type? and valid_id?.
For example, valid_type? is a simple value comparison, but it is implemented as a private method.
Therefore, following the implementation of the entire class, we also extracted the method as valid_class? when checking the class this time.

However, as you suspected, I think it is also more readable to use is_a? in the verify method without extracting it to the method. If you need it, please let me know so I can move it.

def self.response_class
WebAuthn::AuthenticatorAttestationResponse
end

def verify(challenge, user_verification: nil)
challenge.is_a?(String) || raise(InvalidChallengeError, "challenge must be a String. input challenge class: #{challenge.class}")
Copy link
Contributor

Choose a reason for hiding this comment

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

Some part of me thinks that receiving a nil challenge in some sense it is a challenge verification error, thus my initial idea of just re-using the ChallengeVerificationError and, as you are correctly doing, use the message to be specific about what was the underlying issue with the challenge.

However, I do see how using VerificationError classes could cause confusion given that, as you correctly pointed out, VerificationErrors are dynamically raised depending on which verification was the one that failed.

Furthermore, we are already using the pattern of raising an error if there's a validation error, so I'm okay with the approach.

@soartec-lab
Copy link
Contributor Author

@santiagorodriguez96

Thank you very much for giving me a good time with this PR.
I am replying to your comment, so please check it as well.

Also, does this PR commit require squash? Looking at other PRs, it seems that squash of commits is not necessary, but if it is necessary, please let me know.

@santiagorodriguez96
Copy link
Contributor

@santiagorodriguez96

Thank you very much for giving me a good time with this PR. I am replying to your comment, so please check it as well.

The same goes for you! Sorry for the back and forth, I'm lately getting back to maintaining the gem, so I'm still a little bit rough 🙂

Also, does this PR commit require squash? Looking at other PRs, it seems that squash of commits is not necessary, but if it is necessary, please let me know.

I can just Squash and merge and it should be good! 🙂

@santiagorodriguez96 santiagorodriguez96 merged commit 6a5d7e9 into cedarcode:master Nov 21, 2023
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants