Skip to content

Commit

Permalink
feat: discoverable login (#18)
Browse files Browse the repository at this point in the history
This adds discoverable login tooling previously known as resident keys. It does so via an overloaded ValidateLogin that retrieves the user object for a given UserHandle if available in the response.
  • Loading branch information
james-d-elliott authored Mar 1, 2022
1 parent 4c7efcd commit 401a3f6
Showing 1 changed file with 44 additions and 9 deletions.
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 @@ -39,6 +37,20 @@ func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.
allowedCredentials[i] = credential.Descriptor()
}

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 @@ -51,17 +63,17 @@ func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.
setter(&requestOptions)
}

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

response := protocol.CredentialAssertion{Response: requestOptions}

return &response, &newSessionData, nil
return &response, &sessionData, nil
}

// Updates the allowed credential list with Credential Descripiptors, discussed in §5.10.3
Expand Down Expand Up @@ -118,6 +130,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

0 comments on commit 401a3f6

Please sign in to comment.