diff --git a/protocol/authenticator.go b/protocol/authenticator.go index 452475c..4b0ec51 100644 --- a/protocol/authenticator.go +++ b/protocol/authenticator.go @@ -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 ( @@ -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 diff --git a/protocol/options.go b/protocol/options.go index f04e69e..e177642 100644 --- a/protocol/options.go +++ b/protocol/options.go @@ -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. diff --git a/webauthn/login.go b/webauthn/login.go index 4ce3a88..c53fd59 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -15,6 +15,9 @@ 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 @@ -22,24 +25,34 @@ type LoginOption func(*protocol.PublicKeyCredentialRequestOptions) // 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{ @@ -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 } @@ -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. @@ -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") } diff --git a/webauthn/registration.go b/webauthn/registration.go index 5feed64..63ad9d5 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -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) { @@ -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, },