Skip to content
This repository was archived by the owner on Dec 5, 2022. It is now read-only.

feat: discoverable login flow #129

Closed
wants to merge 2 commits into from
Closed
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
17 changes: 16 additions & 1 deletion protocol/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type AttestedCredentialData struct {
CredentialPublicKey []byte `json:"public_key"`
}

// AuthenticatorAttachment https://www.w3.org/TR/webauthn/#platform-attachment
// AuthenticatorAttachment https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-authenticatorattachment
type AuthenticatorAttachment string

const (
Expand All @@ -62,6 +62,21 @@ const (
CrossPlatform AuthenticatorAttachment = "cross-platform"
)

// ResidentKeyRequirement https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-residentkey
type ResidentKeyRequirement string

const (
// ResidentKeyRequirementDiscouraged indicates to the client we do not want a discoverable credential. This is the default.
ResidentKeyRequirementDiscouraged ResidentKeyRequirement = "discouraged"

// ResidentKeyRequirementPreferred indicates to the client we would prefer a discoverable credential.
ResidentKeyRequirementPreferred ResidentKeyRequirement = "preferred"

// ResidentKeyRequirementRequired indicates to the client we require a discoverable credential and that it should
// fail if the credential does not support this feature.
ResidentKeyRequirementRequired ResidentKeyRequirement = "required"
)

// Authenticators may implement various transports for communicating with clients. This enumeration defines
// hints as to how clients might communicate with a particular authenticator in order to obtain an assertion
// for a specific credential. Note that these hints represent the WebAuthn Relying Party's best belief as to
Expand Down
5 changes: 5 additions & 0 deletions protocol/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ type AuthenticatorSelection struct {
// credentials. If the parameter is set to true, the authenticator MUST create a client-side-resident
// public key credential source when creating a public key credential.
RequireResidentKey *bool `json:"requireResidentKey,omitempty"`

// ResidentKey this member describes the Relying Party's requirements regarding resident
// credentials per Webauthn Level 2.
ResidentKey ResidentKeyRequirement `json:"residentKey,omitempty"`

// UserVerification This member describes the Relying Party's requirements regarding user verification for
// the create() operation. Eligible authenticators are filtered to only those capable of satisfying this
// requirement.
Expand Down
62 changes: 49 additions & 13 deletions webauthn/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,44 @@ import (
// LoginOption is used to provide parameters that modify the default Credential Assertion Payload that is sent to the user.
type LoginOption func(*protocol.PublicKeyCredentialRequestOptions)

// DiscoverableUserHandler returns a *User given the provided userHandle.
type DiscoverableUserHandler func(userHandle []byte) (user User, err error)

// Creates the CredentialAssertion data payload that should be sent to the user agent for beginning the
// login/assertion process. The format of this data can be seen in §5.5 of the WebAuthn specification
// (https://www.w3.org/TR/webauthn/#assertion-options). These default values can be amended by providing
// additional LoginOption parameters. This function also returns sessionData, that must be stored by the
// RP in a secure manner and then provided to the FinishLogin function. This data helps us verify the
// ownership of the credential being retreived.
func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
challenge, err := protocol.CreateChallenge()
if err != nil {
return nil, nil, err
}

credentials := user.WebAuthnCredentials()

if len(credentials) == 0 { // If the user does not have any credentials, we cannot do login
if len(credentials) == 0 { // If the user does not have any credentials, we cannot perform an assertion.
return nil, nil, protocol.ErrBadRequest.WithDetails("Found no credentials for user")
}

var allowedCredentials = make([]protocol.CredentialDescriptor, len(credentials))

for i, credential := range credentials {
var credentialDescriptor protocol.CredentialDescriptor
credentialDescriptor.CredentialID = credential.ID
credentialDescriptor.Type = protocol.PublicKeyCredentialType
allowedCredentials[i] = credentialDescriptor
allowedCredentials[i] = protocol.CredentialDescriptor{
Type: protocol.PublicKeyCredentialType,

CredentialID: credential.ID,
}
}

return webauthn.beginLogin(user.WebAuthnID(), allowedCredentials, opts...)
}

// BeginDiscoverableLogin begins a client-side discoverable login, previously known as Resident Key logins.
func (webauthn *WebAuthn) BeginDiscoverableLogin(opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
return webauthn.beginLogin(nil, nil, opts...)
}

func (webauthn *WebAuthn) beginLogin(userID []byte, allowedCredentials []protocol.CredentialDescriptor, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
challenge, err := protocol.CreateChallenge()
if err != nil {
return nil, nil, err
}

requestOptions := protocol.PublicKeyCredentialRequestOptions{
Expand All @@ -56,13 +69,13 @@ func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.

newSessionData := SessionData{
Challenge: base64.RawURLEncoding.EncodeToString(challenge),
UserID: user.WebAuthnID(),
UserID: userID,
AllowedCredentialIDs: requestOptions.GetAllowedCredentialIDs(),
UserVerification: requestOptions.UserVerification,
Extensions: requestOptions.Extensions,
}

response := protocol.CredentialAssertion{requestOptions}
response := protocol.CredentialAssertion{Response: requestOptions}

return &response, &newSessionData, nil
}
Expand Down Expand Up @@ -105,6 +118,29 @@ func (webauthn *WebAuthn) ValidateLogin(user User, session SessionData, parsedRe
return nil, protocol.ErrBadRequest.WithDetails("ID mismatch for User and Session")
}

return webauthn.validateLogin(user, session, parsedResponse)
}

// ValidateDiscoverableLogin is an overloaded version of ValidateLogin that allows for discoverable credentials.
func (webauthn *WebAuthn) ValidateDiscoverableLogin(handler DiscoverableUserHandler, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (*Credential, error) {
if session.UserID != nil {
return nil, protocol.ErrBadRequest.WithDetails("Session was not initiated as a client-side discoverable login")
}

if parsedResponse.Response.UserHandle == nil {
return nil, protocol.ErrBadRequest.WithDetails("Client-side Discoverable Assertion was attempted with a blank User Handle")
}

user, err := handler(parsedResponse.Response.UserHandle)
if err != nil {
return nil, protocol.ErrBadRequest.WithDetails("Failed to lookup Client-side Discoverable Credential")
}

return webauthn.validateLogin(user, session, parsedResponse)
}

// validateLogin takes a parsed response and validates it against the user credentials and session data
func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (*Credential, error) {
// Step 1. If the allowCredentials option was given when this authentication ceremony was initiated,
// verify that credential.id identifies one of the public key credentials that were listed in
// allowCredentials.
Expand Down Expand Up @@ -143,7 +179,7 @@ func (webauthn *WebAuthn) ValidateLogin(user User, session SessionData, parsedRe
// This is in part handled by our Step 1

userHandle := parsedResponse.Response.UserHandle
if userHandle != nil && len(userHandle) > 0 {
if len(userHandle) > 0 {
if !bytes.Equal(userHandle, user.WebAuthnID()) {
return nil, protocol.ErrBadRequest.WithDetails("userHandle and User ID do not match")
}
Expand Down
35 changes: 25 additions & 10 deletions webauthn/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@ func WithExtensions(extension protocol.AuthenticationExtensions) RegistrationOpt
}
}

// WithResidentKeyRequirement sets both the resident key and require resident key protocol options. When
func WithResidentKeyRequirement(requirement protocol.ResidentKeyRequirement) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.AuthenticatorSelection.ResidentKey = requirement
switch requirement {
case protocol.ResidentKeyRequirementRequired:
cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyRequired()
case protocol.ResidentKeyRequirementPreferred:
cco.AuthenticatorSelection.RequireResidentKey = nil
case protocol.ResidentKeyRequirementDiscouraged:
cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyUnrequired()
}
}
}

// Take the response from the authenticator and client and verify the credential against the user's credentials and
// session data.
func (webauthn *WebAuthn) FinishRegistration(user User, session SessionData, response *http.Request) (*Credential, error) {
Expand Down Expand Up @@ -126,43 +141,43 @@ func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parse

func defaultRegistrationCredentialParameters() []protocol.CredentialParameter {
return []protocol.CredentialParameter{
protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES256,
},
protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES384,
},
protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES512,
},
protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS256,
},
protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS384,
},
protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS512,
},
protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS256,
},
protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS384,
},
protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS512,
},
protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgEdDSA,
},
Expand Down