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

Commit 1ca19ff

Browse files
authored
Merge pull request #132 from james-d-elliott/feat-discoverable-login
feat: discoverable credential login
2 parents c796f69 + 09bc59f commit 1ca19ff

File tree

1 file changed

+44
-9
lines changed

1 file changed

+44
-9
lines changed

webauthn/login.go

+44-9
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,19 @@ import (
1515
// LoginOption is used to provide parameters that modify the default Credential Assertion Payload that is sent to the user.
1616
type LoginOption func(*protocol.PublicKeyCredentialRequestOptions)
1717

18+
// DiscoverableUserHandler returns a *User given the provided userHandle.
19+
type DiscoverableUserHandler func(rawID, userHandle []byte) (user User, err error)
20+
1821
// Creates the CredentialAssertion data payload that should be sent to the user agent for beginning the
1922
// login/assertion process. The format of this data can be seen in §5.5 of the WebAuthn specification
2023
// (https://www.w3.org/TR/webauthn/#assertion-options). These default values can be amended by providing
2124
// additional LoginOption parameters. This function also returns sessionData, that must be stored by the
2225
// RP in a secure manner and then provided to the FinishLogin function. This data helps us verify the
2326
// ownership of the credential being retreived.
2427
func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
25-
challenge, err := protocol.CreateChallenge()
26-
if err != nil {
27-
return nil, nil, err
28-
}
29-
3028
credentials := user.WebAuthnCredentials()
3129

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

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

43+
return webauthn.beginLogin(user.WebAuthnID(), allowedCredentials, opts...)
44+
}
45+
46+
// BeginDiscoverableLogin begins a client-side discoverable login, previously known as Resident Key logins.
47+
func (webauthn *WebAuthn) BeginDiscoverableLogin(opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
48+
return webauthn.beginLogin(nil, nil, opts...)
49+
}
50+
51+
func (webauthn *WebAuthn) beginLogin(userID []byte, allowedCredentials []protocol.CredentialDescriptor, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
52+
challenge, err := protocol.CreateChallenge()
53+
if err != nil {
54+
return nil, nil, err
55+
}
56+
4557
requestOptions := protocol.PublicKeyCredentialRequestOptions{
4658
Challenge: challenge,
4759
Timeout: webauthn.Config.Timeout,
@@ -56,13 +68,13 @@ func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.
5668

5769
newSessionData := SessionData{
5870
Challenge: base64.RawURLEncoding.EncodeToString(challenge),
59-
UserID: user.WebAuthnID(),
71+
UserID: userID,
6072
AllowedCredentialIDs: requestOptions.GetAllowedCredentialIDs(),
6173
UserVerification: requestOptions.UserVerification,
6274
Extensions: requestOptions.Extensions,
6375
}
6476

65-
response := protocol.CredentialAssertion{requestOptions}
77+
response := protocol.CredentialAssertion{Response: requestOptions}
6678

6779
return &response, &newSessionData, nil
6880
}
@@ -105,6 +117,29 @@ func (webauthn *WebAuthn) ValidateLogin(user User, session SessionData, parsedRe
105117
return nil, protocol.ErrBadRequest.WithDetails("ID mismatch for User and Session")
106118
}
107119

120+
return webauthn.validateLogin(user, session, parsedResponse)
121+
}
122+
123+
// ValidateDiscoverableLogin is an overloaded version of ValidateLogin that allows for discoverable credentials.
124+
func (webauthn *WebAuthn) ValidateDiscoverableLogin(handler DiscoverableUserHandler, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (*Credential, error) {
125+
if session.UserID != nil {
126+
return nil, protocol.ErrBadRequest.WithDetails("Session was not initiated as a client-side discoverable login")
127+
}
128+
129+
if parsedResponse.Response.UserHandle == nil {
130+
return nil, protocol.ErrBadRequest.WithDetails("Client-side Discoverable Assertion was attempted with a blank User Handle")
131+
}
132+
133+
user, err := handler(parsedResponse.RawID, parsedResponse.Response.UserHandle)
134+
if err != nil {
135+
return nil, protocol.ErrBadRequest.WithDetails("Failed to lookup Client-side Discoverable Credential")
136+
}
137+
138+
return webauthn.validateLogin(user, session, parsedResponse)
139+
}
140+
141+
// validateLogin takes a parsed response and validates it against the user credentials and session data
142+
func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (*Credential, error) {
108143
// Step 1. If the allowCredentials option was given when this authentication ceremony was initiated,
109144
// verify that credential.id identifies one of the public key credentials that were listed in
110145
// allowCredentials.
@@ -143,7 +178,7 @@ func (webauthn *WebAuthn) ValidateLogin(user User, session SessionData, parsedRe
143178
// This is in part handled by our Step 1
144179

145180
userHandle := parsedResponse.Response.UserHandle
146-
if userHandle != nil && len(userHandle) > 0 {
181+
if len(userHandle) > 0 {
147182
if !bytes.Equal(userHandle, user.WebAuthnID()) {
148183
return nil, protocol.ErrBadRequest.WithDetails("userHandle and User ID do not match")
149184
}

0 commit comments

Comments
 (0)