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

feat: add capability of handling appid extension #319

Merged
merged 20 commits into from
Nov 11, 2022

Conversation

santiagorodriguez96
Copy link
Contributor

@santiagorodriguez96 santiagorodriguez96 commented Jun 18, 2020

What

Add capability of internally handling the appid extension.

Now the user only has to add the legacy FIDO AppID in the configuration – legacy_u2f_appid – and the extension will be automatically requested and the method PublicKeyCredentialWithAssertion#verify will be smart enough to determine if it should use the AppID or the RP ID to verify the WebAuthn credential, depending on the output of the appid client extension.

This commits adds the capability of specifying which App ID was used
previously for registering U2F credentials.
Comment on lines 39 to 40
appid_domain = WebAuthn.configuration.appid || WebAuthn.configuration.rp_id ||
WebAuthn.configuration.origin || raise("Unspecified expected origin")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we should find a way of unifying the identification of where the rp_id comes from in one method available for all, cause we are doing something similar in here.

Base automatically changed from provide_extensions_as_an_output_of_the_verification to master June 22, 2020 14:21
@santiagorodriguez96 santiagorodriguez96 marked this pull request as ready for review June 22, 2020 14:22
Copy link
Contributor

@grzuy grzuy left a comment

Choose a reason for hiding this comment

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

Have you considered adding the extension automatically to the output of WebAuthn::Credential.options_for_get when the user set the config?

lib/webauthn/configuration.rb Outdated Show resolved Hide resolved
@@ -20,7 +20,7 @@ def initialize(type:, id:, raw_id:, client_extension_outputs: {}, response:)
@type = type
@id = id
@raw_id = raw_id
@client_extension_outputs = client_extension_outputs
@client_extension_outputs = client_extension_outputs || {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why we need this here, this is the default in the argument definition.
Is it for some specific case in which the keyword argument is passed explicitly with nil?
If so, why we still want to override with {}? Shouldn't the caller be the one who needs to specify {} then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it for some specific case in which the keyword argument is passed explicitly with nil?

No, it's just an attempt to normalize the output, related to the talk that we discuss here.
Besides, this helped me to avoid doing something like client_extension_outputs&.[](:appid) in this line.

But after we agreed on not normalize the output yet, I will go ahead an change it 🙂

spec/webauthn/public_key_credential_with_assertion_spec.rb Outdated Show resolved Hide resolved
end
end

context "when verifying a migrated U2F credential" do
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

I think it would be great to also have its testing when the verification correctly fails if response is not valid.
These test cases are as important as the ones that test it works when all valid, we don't want false positives :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call!

@@ -16,7 +16,8 @@ def verify(challenge, public_key:, sign_count:, user_verification: nil)
encoder.decode(challenge),
public_key: encoder.decode(public_key),
sign_count: sign_count,
user_verification: user_verification
user_verification: user_verification,
rp_id: client_extension_outputs[:appid] ? appid : nil
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we also need to check the new config is present here?

Copy link
Contributor Author

@santiagorodriguez96 santiagorodriguez96 Jun 29, 2020

Choose a reason for hiding this comment

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

Yes!! That's what we are doing here 🙂

@santiagorodriguez96
Copy link
Contributor Author

Have you considered adding the extension automatically to the output of WebAuthn::Credential.options_for_get when the user set the config?

That's actually a good idea! I'll give it a try 👀

This commits adds the `appid` extension automatically to the output of
`WebAuthn::Credential.options_for_get` when the user set the
`legacy_u2f_appid` config, if the said extension wasn't already
requested in the call to that method.
Copy link
Contributor

@grzuy grzuy left a comment

Choose a reason for hiding this comment

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

Thanks for the testing efforts!

lib/webauthn/public_key_credential/request_options.rb Outdated Show resolved Hide resolved
@grzuy
Copy link
Contributor

grzuy commented Mar 9, 2021

Also, separated the tests suites for PublicKeyCredentialWithAttestation (which was the one that was before, but named PublicKeyCredential) and PublicKeyCredentialWithAssertion so we can test this feature separately.

Split off this ☝️ into it's own separate commit (f787a2f) and pushed to master because that's valuable and independent of this PR.

Copy link
Member

@brauliomartinezlm brauliomartinezlm left a comment

Choose a reason for hiding this comment

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

Great contribution @santiagorodriguez96 !

Left you a couple of comments/questions.

Also, do you know why this was not included in the RelyingParty API in 3.0.0.alpha1 ?

lib/webauthn/public_key_credential_with_assertion.rb Outdated Show resolved Hide resolved
@@ -12,7 +12,7 @@ class Options

def initialize(timeout: default_timeout, extensions: nil)
@timeout = timeout
@extensions = extensions
@extensions = default_extensions.merge(extensions || {})
Copy link
Member

Choose a reason for hiding this comment

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

I was analyzing #319 (comment) and I'm not sure if you processed with what you concluded or. This looks to me as if you're still trying to cover the case when extensions is still pass in as nil. Else I would have the default in the method definition itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems that this changes came after the comment that you mentioned – take a look at this comment: #319 (comment). I can change it so we do not support the case when extensions is still pass in as nil but I also don't think it hurts to have it 🤔

@santiagorodriguez96
Copy link
Contributor Author

Also, do you know why this was not included in the RelyingParty API in 3.0.0.alpha1 ?

I don't really know, but the PR now has some conflicts to resolve that are related to the RelayingParty API. I'll try to fix them so we can get this back on track and hopefully merge it 🙂

Comment on lines 44 to 49
def appid
appid_domain = relying_party.legacy_u2f_appid || relying_party.id ||
relying_party.origin || raise("Unspecified expected origin")

URI.parse(appid_domain).to_s
end
Copy link
Contributor Author

@santiagorodriguez96 santiagorodriguez96 Oct 27, 2022

Choose a reason for hiding this comment

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

Correct me if I'm wrong but I suspect this is incorrect – we should only check for the legacy_u2f_appid if the appid extension is present given that if it is not set, even if we pass in the rp_id, the verification will fail.

I would also consider raising an exception if the appid extension is present but the legacy_u2f_appid was not set 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in 4058bea.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we should only check for the legacy_u2f_appid if the appid extension is present given that if it is not set, even if we pass in the rp_id, the verification will fail.

I confirmed that this is the case: if I leave PublicKeyCredentialWithAssertion#appid to be:

    def appid
      appid_domain = relying_party.legacy_u2f_appid || relying_party.id ||
                     relying_party.origin || raise("Unspecified expected origin")

      URI.parse(appid_domain).to_s
    end

then this spec that I created will fail with a WebAuthn::RpIdVerificationError

appid_domain = relying_party.legacy_u2f_appid || relying_party.id ||
relying_party.origin || raise("Unspecified expected origin")

URI.parse(appid_domain).to_s
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think this is needed either: AppID should be a web origin – like https://login.example.com – so doing URI.parse(appid).to_s should always return appid if I'm not mistaken

@santiagorodriguez96 santiagorodriguez96 requested review from brauliomartinezlm and removed request for grzuy October 27, 2022 17:52
If the appid extension was requested and the output was `true` but the
`legacy_u2f_appid` config was not set, raise an error.

We were previously were passing the `rp_id` instead if the mentioned
config was not present, but that is needlessly given that it will fail
with a rp verification error.
@@ -233,5 +235,111 @@
end
end
end

context "when verifying a migrated U2F credential" do
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've just noticed that we have very similar test cases here...

Maybe it's a good idea to change spec/webauthn/public_key_credential_with_assertion_spec.rb to mock WebAuthn::AuthenticatorAssertionResponse#verify so that we can just test that we are sending the right parameters to it 🤔

Comment on lines +312 to +319
it "raises an error" do
expect do
public_key_credential.verify(
challenge,
public_key: credential_public_key,
sign_count: credential_sign_count
)
end.to raise_error("Unspecified legacy U2F AppID")
Copy link
Contributor Author

@santiagorodriguez96 santiagorodriguez96 Oct 28, 2022

Choose a reason for hiding this comment

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

This one is a tricky one tbh...

PublicKeyCredentialWithAssertion #verify when verifying a migrated U2F credential and appid is not set in configuration file raises an error

We currently support that you can call AuthenticatorAssertionResponse.from_get(extensions: { appid: { 'my_appid' } – without the need of setting the appid in the config – and that will correctly send the extension. But doing it this way will raise an error if you use PublicKeyCredentialWithAssertion#verify – in the case that the extension output returning from the client was true.

In the U2F migration docs it states that you have to call AuthenticatorAssertionResponse#verify instead and pass the appid as the rp_id, but I feel that we could also accept the rp_id param in PublicKeyCredentialWithAssertion#verify so we don't force users to use the former – which is not that "smart" and does not perform all the verifications.

@santiagorodriguez96
Copy link
Contributor Author

I've updated the docs – both the U2F migration doc and the Configuration section in the README. @bdewater it would be great if you could take a quick look at them and check that they still make sense 🙂

README.md Outdated
# Otherwise you would have to re-register those credentials to scope them to the RP ID.
# See https://github.com/cedarcode/webauthn-ruby/blob/master/docs/u2f_migration.md.
#
# config.legacy_u2f_appid = "https://login.example.com"
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would keep this out of the readme - by now the U2F API is well on its way out (even removed from Chrome) so this will confuse newcomers looking to get started.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call! Updated in 1e6564f 🙂

@santiagorodriguez96
Copy link
Contributor Author

The build is failing because there's a fixup commit in the branch. I tried rebasing to squash it but there were too much conflicts at this point. I think I'll just go ahead and Squash and Merge it instead.

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

4 participants