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

feat: discoverable credential login #132

Merged
merged 1 commit into from
Aug 15, 2022
Merged
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
53 changes: 44 additions & 9 deletions webauthn/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,19 @@ 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(rawID, 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")
}

Expand All @@ -42,6 +40,20 @@ func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.
allowedCredentials[i] = credentialDescriptor
}

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{
Challenge: challenge,
Timeout: webauthn.Config.Timeout,
Expand All @@ -56,13 +68,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 +117,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.RawID, 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 +178,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