-
Notifications
You must be signed in to change notification settings - Fork 969
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: passkey strategy with resident keys #3656
Conversation
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## master #3656 +/- ##
==========================================
- Coverage 79.47% 78.35% -1.13%
==========================================
Files 356 352 -4
Lines 25999 24312 -1687
==========================================
- Hits 20662 19049 -1613
+ Misses 3826 3794 -32
+ Partials 1511 1469 -42 ☔ View full report in Codecov by Sentry. |
Co-authored-by: Jonas Hungershausen <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preliminary review - also pushing some code changes now. Can you please add e2e tests for the various flows?
if err != nil { | ||
return s.handleRegistrationError(w, r, regFlow, params, err) | ||
} | ||
if params.Register == "" && params.Method != "passkey" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible that Register
is set to some value, but the method is actually not passkey but e.g. password
, and then we use the wrong strategy here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not in a benign case, because the register param is set right before submission.
As for an attacker, what would the gain be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the concern is, that params.Register == "values"
but params.Method != "passkey"
.
Which would lead to this strategy being executed, even though the method specifies something else. IMO, we should always return ErrStrategyNotResponsible
if params.Method != "passkey"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Requiring the method to be set will further complicate submitting the passkey, which is done through a form submit in JS, see:
kratos/x/webauthnx/js/webauthn.js
Line 121 in 96c2164
document.querySelector(triggerQuerySelector).closest("form").submit() |
We don't require the method to be set for webauthn register requests either, btw:
kratos/selfservice/strategy/webauthn/registration.go
Lines 94 to 114 in 96c2164
func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity) (err error) { | |
ctx := r.Context() | |
if regFlow.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) { | |
return flow.ErrStrategyNotResponsible | |
} | |
var p updateRegistrationFlowWithWebAuthnMethod | |
if err := s.decode(&p, r); err != nil { | |
return s.handleRegistrationError(w, r, regFlow, &p, err) | |
} | |
regFlow.TransientPayload = p.TransientPayload | |
if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { | |
return s.handleRegistrationError(w, r, regFlow, &p, err) | |
} | |
if len(p.Register) == 0 { | |
return flow.ErrStrategyNotResponsible | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifically, I could add a hidden input with name=method and value=passkey in JS. Is it worth it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say it's worth the effort also submitting the method, since we do have the (sometimes loose) convention to submit the method as well. Since this would just be another edge case, potentially causing issues down the road.
But, in the interest of velocity, I'll leave the decision up to you.
credentialWebAuthn := identity.CredentialFromWebAuthn(credential, true) | ||
credentialsConfig, err := json.Marshal(identity.CredentialsWebAuthnConfig{ | ||
Credentials: identity.CredentialsWebAuthn{*credentialWebAuthn}, | ||
UserHandle: webAuthnSess.UserID, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about the display name? Do we not need it because it is a PassKey?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The display name of the key will be derived from the AAGUID, see here:
kratos/identity/credentials_webauthn.go
Lines 36 to 39 in 60a1817
id := aaguid.Lookup(credential.Authenticator.AAGUID) | |
if id != nil { | |
cred.DisplayName = id.Name | |
} |
|
||
ident.UpsertCredentialsConfig(s.ID(), credentialsConfig, 1) | ||
passkeyCred, _ := ident.GetCredentials(s.ID()) | ||
passkeyCred.Identifiers = []string{string(webAuthnSess.UserID)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should return an easy to understand error if webAuthnSess.UserID
is empty. If it is empty, the current validation will pass it as valid
c := i.GetCredentialsOr(identity.CredentialsTypePasskey, &identity.Credentials{})
if len(c.Identifiers) == 0 {
return schema.NewMissingIdentifierError()
}
and will lead to an empty string identifier. We had this problem once before in the WebAuthn code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, webAuthnSess.UserID
is not user-supplied. It comes from the internal context, which is set here:
kratos/selfservice/strategy/passkey/passkey_registration.go
Lines 295 to 303 in 60a1817
user := &webauthnx.User{ | |
Name: identifier, | |
ID: []byte(randx.MustString(64, randx.AlphaNum)), | |
Config: s.d.Config().PasskeyConfig(ctx), | |
} | |
option, sessionData, err := webAuthn.BeginRegistration(user) | |
if err != nil { | |
return errors.WithStack(err) | |
} |
I can add another check to make sure it really is not empty, but it would be an internal server error, as the user can't do anything against it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, just a few minor remarks.
if err != nil { | ||
return s.handleRegistrationError(w, r, regFlow, params, err) | ||
} | ||
if params.Register == "" && params.Method != "passkey" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the concern is, that params.Register == "values"
but params.Method != "passkey"
.
Which would lead to this strategy being executed, even though the method specifies something else. IMO, we should always return ErrStrategyNotResponsible
if params.Method != "passkey"
test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts
Outdated
Show resolved
Hide resolved
@jonas-jonas are you good with the current state? |
@aeneasr I'll re-review now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except for #3656 (comment) this LGTM.
@jonas-jonas, @aeneasr, two points remain open:
|
I'd like to have one more pair of eyes on the security side of this before merging. @zepatrik maybe? |
I found a few bugs when enabling both webauthn and passkey strategies, probably the triggers get confused. Wrote those things in Slack - would be good to get those fixed in my view :) |
included in #3748 |
TODO
webauthnIdentifierNode
Related issue(s)
Docs PR: ory/docs#1626
Checklist
introduces a new feature.
contributing code guidelines.
vulnerability. If this pull request addresses a security vulnerability, I
confirm that I got the approval (please contact
[email protected]) from the maintainers to push
the changes.
works.
Further Comments