Skip to content

Commit

Permalink
Add UserVerification capabilities to LoginFlow (#37963) (#38487)
Browse files Browse the repository at this point in the history
* Disallow the passwordless scope on regular LoginFlow

* fix: Use the correct `t` for assertions

* nit: Fix test target name

* Add UserVerificationRequirement to ChallengeExtensions

* Update generated protos

* Add user verification support to LoginFlow

* nit: Add my scopes TODO

* Change user_verification_requirement to a string

* nit: Use slices.Clone

* Tweak disallowed scope error message
  • Loading branch information
codingllama authored Feb 21, 2024
1 parent 7478a30 commit 70c5407
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 56 deletions.
123 changes: 91 additions & 32 deletions api/gen/proto/go/teleport/mfa/v1/mfa.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions api/proto/teleport/mfa/v1/mfa.proto
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ message ChallengeExtensions {
// Note that reuse is only permitted for specific actions by the discretion
// of the server. See the server implementation for details.
ChallengeAllowReuse allow_reuse = 2;
// User verification requirement for the challenge.
//
// * https://www.w3.org/TR/webauthn-2/#enum-userVerificationRequirement.
// * https://pkg.go.dev/github.com/go-webauthn/webauthn/protocol#UserVerificationRequirement.
//
// Optional. Empty is equivalent to "discouraged".
string user_verification_requirement = 3;
}

// ChallengeScope is a scope authorized by an MFA challenge resolution.
Expand Down
39 changes: 29 additions & 10 deletions lib/auth/webauthn/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,14 @@ func (f *loginFlow) begin(ctx context.Context, user string, challengeExtensions
return nil, trace.BadParameter("mfa challenges with scope %s cannot allow reuse", challengeExtensions.Scope)
}

passwordless := challengeExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN
if user == "" && !passwordless {
// discoverableLogin identifies logins started with an unknown/empty user.
discoverableLogin := challengeExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN
if user == "" && !discoverableLogin {
return nil, trace.BadParameter("user required")
}

var u *webUser
if passwordless {
if discoverableLogin {
u = &webUser{} // Issue anonymous challenge.
} else {
webID, err := f.getWebID(ctx, user)
Expand Down Expand Up @@ -149,20 +150,27 @@ func (f *loginFlow) begin(ctx context.Context, user string, challengeExtensions
wantypes.AppIDExtension: f.U2F.AppID,
}))
}
// Set the user verification requirement, if present, only for
// non-discoverable logins.
// For discoverable logins we rely on the wan.WebAuthn default set below.
if !discoverableLogin && challengeExtensions.UserVerificationRequirement != "" {
uvr := protocol.UserVerificationRequirement(challengeExtensions.UserVerificationRequirement)
opts = append(opts, wan.WithUserVerification(uvr))
}

// Create the WebAuthn object and issue a new challenge.
web, err := newWebAuthn(webAuthnParams{
cfg: f.Webauthn,
rpID: f.Webauthn.RPID,
requireUserVerification: passwordless,
requireUserVerification: discoverableLogin,
})
if err != nil {
return nil, trace.Wrap(err)
}

var assertion *protocol.CredentialAssertion
var sessionData *wan.SessionData
if passwordless {
if discoverableLogin {
assertion, sessionData, err = web.BeginDiscoverableLogin(opts...)
} else {
assertion, sessionData, err = web.BeginLogin(u, opts...)
Expand Down Expand Up @@ -212,10 +220,10 @@ func (f *loginFlow) finish(ctx context.Context, user string, resp *wantypes.Cred
return nil, trace.BadParameter("requested challenge extensions must be supplied.")
}

passwordless := requiredExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN
discoverableLogin := requiredExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN

switch {
case user == "" && !passwordless:
case user == "" && !discoverableLogin:
return nil, trace.BadParameter("user required")
case resp == nil:
// resp != nil is good enough to proceed, we leave remaining validations to
Expand All @@ -235,7 +243,7 @@ func (f *loginFlow) finish(ctx context.Context, user string, resp *wantypes.Cred
}

var webID []byte
if passwordless {
if discoverableLogin {
webID = parsedResp.Response.UserHandle
if len(webID) == 0 {
return nil, trace.BadParameter("webauthn user handle required for passwordless")
Expand Down Expand Up @@ -301,6 +309,17 @@ func (f *loginFlow) finish(ctx context.Context, user string, resp *wantypes.Cred
return nil, trace.AccessDenied("the given webauthn session allows reuse, but reuse is not permitted in this context")
}

// Verify (and possibly correct) the user verification requirement.
// A mismatch here could indicate a programming error or even foul play.
uvr := protocol.UserVerificationRequirement(requiredExtensions.UserVerificationRequirement)
if (discoverableLogin || uvr == protocol.VerificationRequired) && sd.UserVerification != string(protocol.VerificationRequired) {
// This is not a failure yet, but will likely become one.
sd.UserVerification = string(protocol.VerificationRequired)
log.Warnf(""+
"WebAuthn: User verification required by extensions but not by challenge. "+
"Increased SessionData.UserVerification to %s.", sd.UserVerification)
}

sessionData := wantypes.SessionDataToProtocol(sd)

// Make sure _all_ credentials in the session are accounted for by the user.
Expand All @@ -320,14 +339,14 @@ func (f *loginFlow) finish(ctx context.Context, user string, resp *wantypes.Cred
cfg: f.Webauthn,
rpID: rpID,
origin: origin,
requireUserVerification: passwordless,
requireUserVerification: discoverableLogin,
})
if err != nil {
return nil, trace.Wrap(err)
}

var credential *wan.Credential
if passwordless {
if discoverableLogin {
discoverUser := func(_, _ []byte) (wan.User, error) { return u, nil }
credential, err = web.ValidateDiscoverableLogin(discoverUser, *sessionData, parsedResp)
} else {
Expand Down
22 changes: 19 additions & 3 deletions lib/auth/webauthn/login_mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"context"
"errors"

"github.com/gravitational/trace"

mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
"github.com/gravitational/teleport/api/types"
wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes"
Expand Down Expand Up @@ -78,6 +80,12 @@ func (l *loginWithDevices) GetMFADevices(_ context.Context, _ string, _ bool) ([
// 4. Server runs Finish()
// 5. If all server-side checks are successful, then login/authentication is
// complete.
//
// LoginFlow is used in the following scenarios:
// - Password plus challenge logins
// - Presence verification checks (eg, session MFA)
// - User verification checks after the initial login (eg, password changes
// with only a discoverable credential).
type LoginFlow struct {
U2F *types.U2F
Webauthn *types.Webauthn
Expand All @@ -96,10 +104,18 @@ type LoginFlow struct {
// record. These extensions indicate additional rules/properties of the webauthn
// challenge that can be validated in the final login step.
func (f *LoginFlow) Begin(ctx context.Context, user string, challengeExtensions *mfav1.ChallengeExtensions) (*wantypes.CredentialAssertion, error) {
// Disallow passwordless through here.
// lf.begin() does other challengeExtensions checks, including `nil`.
if challengeExtensions != nil && challengeExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN {
return nil, trace.BadParameter("passwordless challenge scope is not allowed for MFA flows")
}

lf := &loginFlow{
U2F: f.U2F,
Webauthn: f.Webauthn,
identity: mfaIdentity{f.Identity},
U2F: f.U2F,
Webauthn: f.Webauthn,
identity: mfaIdentity{f.Identity},
// TODO(codingllama): Record session data to distinct scope keys based on
// the actual challenge scope.
sessionData: (*userSessionStorage)(f),
}
return lf.begin(ctx, user, challengeExtensions)
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/webauthn/login_passwordless.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ type PasswordlessIdentity interface {
}

// PasswordlessFlow provides passwordless authentication.
//
// PasswordlessFlow is used mainly for the initial passwordless login.
// For UV=1 assertions after login, use [LoginFlow.Begin] with the desired
// [mfav1.ChallengeExtensions.UserVerificationRequirement].
type PasswordlessFlow struct {
Webauthn *types.Webauthn
Identity PasswordlessIdentity
Expand Down
Loading

0 comments on commit 70c5407

Please sign in to comment.