Skip to content
This repository was archived by the owner on Oct 20, 2024. It is now read-only.

Commit e2615f4

Browse files
committed
Split out HTTP handlers
This can be useful when nesting the assertion or attestaion responses inside another object which can contain other data, such as the name of the authenticator.
1 parent 01c77d1 commit e2615f4

2 files changed

Lines changed: 111 additions & 93 deletions

File tree

webauthn/login.go

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,15 @@ import (
1111
"github.com/koesie10/webauthn/protocol"
1212
)
1313

14-
// StartLogin is a HTTP request handler which writes the options to be passed to navigator.credentials.get()
15-
// to the http.ResponseWriter. The user argument is optional and can be nil, in which case the allowCredentials
16-
// option will not be set and AuthenticatorStore.GetAuthenticators will not be called.
17-
func (w *WebAuthn) StartLogin(r *http.Request, rw http.ResponseWriter, user User, session Session) {
14+
// GetLoginOptions will return the options that need to be passed to navigator.credentials.get(). This should
15+
// be returned to the user via e.g. JSON over HTTP. For convenience, use StartLogin.
16+
func (w *WebAuthn) GetLoginOptions(user User, session Session) (*protocol.CredentialRequestOptions, error) {
1817
chal, err := protocol.NewChallenge()
1918
if err != nil {
20-
w.writeError(r, rw, err)
21-
return
19+
return nil, err
2220
}
2321

24-
options := protocol.CredentialRequestOptions{
22+
options := &protocol.CredentialRequestOptions{
2523
PublicKey: protocol.PublicKeyCredentialRequestOptions{
2624
Challenge: chal,
2725
Timeout: w.Config.Timeout,
@@ -31,8 +29,7 @@ func (w *WebAuthn) StartLogin(r *http.Request, rw http.ResponseWriter, user User
3129
if user != nil {
3230
authenticators, err := w.Config.AuthenticatorStore.GetAuthenticators(user)
3331
if err != nil {
34-
w.writeError(r, rw, err)
35-
return
32+
return nil, err
3633
}
3734

3835
allowCredentials := make([]protocol.PublicKeyCredentialDescriptor, len(authenticators))
@@ -48,56 +45,53 @@ func (w *WebAuthn) StartLogin(r *http.Request, rw http.ResponseWriter, user User
4845
}
4946

5047
if err := session.Set(w.Config.SessionKeyPrefixChallenge+".login", []byte(chal)); err != nil {
48+
return nil, err
49+
}
50+
51+
return options, nil
52+
}
53+
54+
// StartLogin is a HTTP request handler which writes the options to be passed to navigator.credentials.get()
55+
// to the http.ResponseWriter. The user argument is optional and can be nil, in which case the allowCredentials
56+
// option will not be set and AuthenticatorStore.GetAuthenticators will not be called.
57+
func (w *WebAuthn) StartLogin(r *http.Request, rw http.ResponseWriter, user User, session Session) {
58+
options, err := w.GetLoginOptions(user, session)
59+
if err != nil {
5160
w.writeError(r, rw, err)
5261
return
5362
}
5463

5564
w.write(r, rw, options)
5665
}
5766

58-
// FinishLogin is a HTTP request handler which should receive the response of navigator.credentials.get(). If
67+
// ParseAndFinishLogin should receive the response of navigator.credentials.get(). If
5968
// user is non-nil, it will be checked that the authenticator is owned by that user. If the request is valid,
60-
// the authenticator will be returned and nothing will have been written to http.ResponseWriter. If authenticator is
61-
// nil, an error has been written to http.ResponseWriter and should be returned as-is.
62-
func (w *WebAuthn) FinishLogin(r *http.Request, rw http.ResponseWriter, user User, session Session) Authenticator {
69+
// the authenticator will be returned. For convenience, use FinishLogin.
70+
func (w *WebAuthn) ParseAndFinishLogin(assertionResponse protocol.AssertionResponse, user User, session Session) (Authenticator, error) {
6371
rawChal, err := session.Get(w.Config.SessionKeyPrefixChallenge + ".login")
6472
if err != nil {
65-
w.writeErrorCode(r, rw, http.StatusBadRequest, err)
66-
return nil
73+
return nil, protocol.ErrInvalidRequest.WithDebug("missing challenge in session")
6774
}
6875
chal, ok := rawChal.([]byte)
6976
if !ok {
70-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug("invalid challenge session value"))
71-
return nil
77+
return nil, protocol.ErrInvalidRequest.WithDebug("invalid challenge session value")
7278
}
7379

7480
if err := session.Delete(w.Config.SessionKeyPrefixChallenge + ".login"); err != nil {
75-
w.writeError(r, rw, err)
76-
return nil
77-
}
78-
79-
var assertionResponse protocol.AssertionResponse
80-
81-
d := json.NewDecoder(r.Body)
82-
d.DisallowUnknownFields()
83-
if err := d.Decode(&assertionResponse); err != nil {
84-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug(err.Error()))
85-
return nil
81+
return nil, err
8682
}
8783

8884
p, err := protocol.ParseAssertionResponse(assertionResponse)
8985
if err != nil {
90-
w.writeError(r, rw, err)
91-
return nil
86+
return nil, err
9287
}
9388

9489
// 1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that
9590
// credential.id identifies one of the public key credentials that were listed in allowCredentials.
9691
if user != nil {
9792
authenticators, err := w.Config.AuthenticatorStore.GetAuthenticators(user)
9893
if err != nil {
99-
w.writeError(r, rw, err)
100-
return nil
94+
return nil, err
10195
}
10296

10397
var authrFound bool
@@ -109,7 +103,7 @@ func (w *WebAuthn) FinishLogin(r *http.Request, rw http.ResponseWriter, user Use
109103
}
110104

111105
if !authrFound {
112-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug("authenticator is not owned by user"))
106+
return nil, protocol.ErrInvalidRequest.WithDebug("authenticator is not owned by user")
113107
}
114108
}
115109

@@ -118,14 +112,12 @@ func (w *WebAuthn) FinishLogin(r *http.Request, rw http.ResponseWriter, user Use
118112
if p.Response.UserHandle != nil && len(p.Response.UserHandle) > 0 {
119113
if user != nil {
120114
if !bytes.Equal(p.Response.UserHandle, user.WebAuthID()) {
121-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug("authenticator's user handle does not equal user ID"))
122-
return nil
115+
return nil, protocol.ErrInvalidRequest.WithDebug("authenticator's user handle does not equal user ID")
123116
}
124117
} else {
125118
authenticators, err := w.Config.AuthenticatorStore.GetAuthenticators(&defaultUser{id: p.Response.UserHandle})
126119
if err != nil {
127-
w.writeError(r, rw, err)
128-
return nil
120+
return nil, err
129121
}
130122

131123
var authrFound bool
@@ -137,8 +129,7 @@ func (w *WebAuthn) FinishLogin(r *http.Request, rw http.ResponseWriter, user Use
137129
}
138130

139131
if !authrFound {
140-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug("authenticator is not owned by user"))
141-
return nil
132+
return nil, protocol.ErrInvalidRequest.WithDebug("authenticator is not owned by user")
142133
}
143134
}
144135
}
@@ -147,32 +138,50 @@ func (w *WebAuthn) FinishLogin(r *http.Request, rw http.ResponseWriter, user Use
147138
// case), look up the corresponding credential public key.
148139
authr, err := w.Config.AuthenticatorStore.GetAuthenticator(p.RawID)
149140
if err != nil {
150-
w.writeError(r, rw, err)
151-
return nil
141+
return nil, err
152142
}
153143

154144
block, _ := pem.Decode(authr.WebAuthPublicKey())
155145
if block == nil {
156-
w.writeError(r, rw, fmt.Errorf("invalid stored public key, unable to decode"))
157-
return nil
146+
return nil, fmt.Errorf("invalid stored public key, unable to decode")
158147
}
159148

160149
cert, err := x509.ParsePKIXPublicKey(block.Bytes)
161150
if err != nil {
162-
w.writeError(r, rw, err)
163-
return nil
151+
return nil, err
164152
}
165153

166154
valid, err := protocol.IsValidAssertion(p, chal, w.Config.RelyingPartyID, w.Config.RelyingPartyOrigin, &x509.Certificate{
167155
PublicKey: cert,
168156
})
169157
if err != nil {
170-
w.writeError(r, rw, err)
171-
return nil
158+
return nil, err
172159
}
173160

174161
if !valid {
175-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug("invalid login"))
162+
return nil, protocol.ErrInvalidRequest.WithDebug("invalid login")
163+
}
164+
165+
return authr, nil
166+
}
167+
168+
// FinishLogin is a HTTP request handler which should receive the response of navigator.credentials.get(). If
169+
// user is non-nil, it will be checked that the authenticator is owned by that user. If the request is valid,
170+
// the authenticator will be returned and nothing will have been written to http.ResponseWriter. If authenticator is
171+
// nil, an error has been written to http.ResponseWriter and should be returned as-is.
172+
func (w *WebAuthn) FinishLogin(r *http.Request, rw http.ResponseWriter, user User, session Session) Authenticator {
173+
var assertionResponse protocol.AssertionResponse
174+
175+
d := json.NewDecoder(r.Body)
176+
d.DisallowUnknownFields()
177+
if err := d.Decode(&assertionResponse); err != nil {
178+
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug(err.Error()))
179+
return nil
180+
}
181+
182+
authr, err := w.ParseAndFinishLogin(assertionResponse, user, session)
183+
if err != nil {
184+
w.writeError(r, rw, err)
176185
return nil
177186
}
178187

webauthn/registration.go

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ import (
1010
"github.com/koesie10/webauthn/protocol"
1111
)
1212

13-
// StartRegistration is a HTTP request handler which writes the options to be passed to navigator.credentials.create()
14-
// to the http.ResponseWriter.
15-
func (w *WebAuthn) StartRegistration(r *http.Request, rw http.ResponseWriter, user User, session Session) {
13+
// GetRegistrationOptions will return the options that need to be passed to navigator.credentials.create(). This should
14+
// be returned to the user via e.g. JSON over HTTP. For convenience, use StartRegistration.
15+
func (w *WebAuthn) GetRegistrationOptions(user User, session Session) (*protocol.CredentialCreationOptions, error) {
1616
chal, err := protocol.NewChallenge()
1717
if err != nil {
18-
w.writeError(r, rw, err)
19-
return
18+
return nil, err
2019
}
2120

2221
u := protocol.PublicKeyCredentialUserEntity{
@@ -27,7 +26,7 @@ func (w *WebAuthn) StartRegistration(r *http.Request, rw http.ResponseWriter, us
2726
DisplayName: user.WebAuthDisplayName(),
2827
}
2928

30-
options := protocol.CredentialCreationOptions{
29+
options := &protocol.CredentialCreationOptions{
3130
PublicKey: protocol.PublicKeyCredentialCreationOptions{
3231
Challenge: chal,
3332
RP: protocol.PublicKeyCredentialRpEntity{
@@ -50,8 +49,7 @@ func (w *WebAuthn) StartRegistration(r *http.Request, rw http.ResponseWriter, us
5049

5150
authenticators, err := w.Config.AuthenticatorStore.GetAuthenticators(user)
5251
if err != nil {
53-
w.writeError(r, rw, err)
54-
return
52+
return nil, err
5553
}
5654

5755
excludeCredentials := make([]protocol.PublicKeyCredentialDescriptor, len(authenticators))
@@ -66,86 +64,76 @@ func (w *WebAuthn) StartRegistration(r *http.Request, rw http.ResponseWriter, us
6664
options.PublicKey.ExcludeCredentials = excludeCredentials
6765

6866
if err := session.Set(w.Config.SessionKeyPrefixChallenge+".register", []byte(chal)); err != nil {
69-
w.writeError(r, rw, err)
70-
return
67+
return nil, err
7168
}
7269
if err := session.Set(w.Config.SessionKeyPrefixUserID+".register", u.ID); err != nil {
70+
return nil, err
71+
}
72+
73+
return options, nil
74+
}
75+
76+
// StartRegistration is a HTTP request handler which writes the options to be passed to navigator.credentials.create()
77+
// to the http.ResponseWriter.
78+
func (w *WebAuthn) StartRegistration(r *http.Request, rw http.ResponseWriter, user User, session Session) {
79+
options, err := w.GetRegistrationOptions(user, session)
80+
if err != nil {
7381
w.writeError(r, rw, err)
7482
return
7583
}
7684

7785
w.write(r, rw, options)
7886
}
7987

80-
// FinishRegistration is a HTTP request handler which should receive the response of navigator.credentials.create(). If
81-
// the request is valid, AuthenticatorStore.AddAuthenticator will be called and an empty response with HTTP status code
82-
// 201 (Created) will be written to the http.ResponseWriter. If authenticator is nil, an error has been written to
83-
// http.ResponseWriter and should be returned as-is.
84-
func (w *WebAuthn) FinishRegistration(r *http.Request, rw http.ResponseWriter, user User, session Session) Authenticator {
88+
// ParseAndFinishRegistration should receive the response of navigator.credentials.create(). If
89+
// the request is valid, AuthenticatorStore.AddAuthenticator will be called and the authenticator that was registered
90+
// will be returned. For convenience, use FinishRegistration.
91+
func (w *WebAuthn) ParseAndFinishRegistration(attestationResponse protocol.AttestationResponse, user User, session Session) (Authenticator, error) {
8592
rawChal, err := session.Get(w.Config.SessionKeyPrefixChallenge + ".register")
8693
if err != nil {
87-
w.writeErrorCode(r, rw, http.StatusBadRequest, err)
88-
return nil
94+
return nil, protocol.ErrInvalidRequest.WithDebug("missing challenge in session")
8995
}
9096
chal, ok := rawChal.([]byte)
9197
if !ok {
92-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug("invalid challenge session value"))
93-
return nil
98+
return nil, protocol.ErrInvalidRequest.WithDebug("invalid challenge session value")
9499
}
95100
if err := session.Delete(w.Config.SessionKeyPrefixChallenge + ".register"); err != nil {
96-
w.writeError(r, rw, err)
97-
return nil
101+
return nil, err
98102
}
99103

100104
rawUserID, err := session.Get(w.Config.SessionKeyPrefixUserID + ".register")
101105
if err != nil {
102-
w.writeErrorCode(r, rw, http.StatusBadRequest, err)
103-
return nil
106+
return nil, protocol.ErrInvalidRequest.WithDebug("missing user ID in session")
104107
}
105108
userID, ok := rawUserID.([]byte)
106109
if !ok {
107-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug("invalid user ID session value"))
108-
return nil
110+
return nil, protocol.ErrInvalidRequest.WithDebug("invalid user ID session value")
109111
}
110112
if err := session.Delete(w.Config.SessionKeyPrefixUserID + ".register"); err != nil {
111-
w.writeError(r, rw, err)
112-
return nil
113+
return nil, err
113114
}
114115

115116
if !bytes.Equal(user.WebAuthID(), userID) {
116-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug("user has changed since start of registration"))
117-
return nil
118-
}
119-
120-
var attestationResponse protocol.AttestationResponse
121-
d := json.NewDecoder(r.Body)
122-
d.DisallowUnknownFields()
123-
if err := d.Decode(&attestationResponse); err != nil {
124-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug(err.Error()))
125-
return nil
117+
return nil, protocol.ErrInvalidRequest.WithDebug("user has changed since start of registration")
126118
}
127119

128120
p, err := protocol.ParseAttestationResponse(attestationResponse)
129121
if err != nil {
130-
w.writeError(r, rw, err)
131-
return nil
122+
return nil, err
132123
}
133124

134125
valid, err := protocol.IsValidAttestation(p, chal, w.Config.RelyingPartyID, w.Config.RelyingPartyOrigin)
135126
if err != nil {
136-
w.writeError(r, rw, err)
137-
return nil
127+
return nil, err
138128
}
139129

140130
if !valid {
141-
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug("invalid registration"))
142-
return nil
131+
return nil, protocol.ErrInvalidRequest.WithDebug("invalid registration")
143132
}
144133

145134
data, err := x509.MarshalPKIXPublicKey(p.Response.Attestation.AuthData.AttestedCredentialData.COSEKey)
146135
if err != nil {
147-
w.writeErrorCode(r, rw, http.StatusBadRequest, err)
148-
return nil
136+
return nil, err
149137
}
150138

151139
authr := &defaultAuthenticator{
@@ -160,6 +148,27 @@ func (w *WebAuthn) FinishRegistration(r *http.Request, rw http.ResponseWriter, u
160148
}
161149

162150
if err := w.Config.AuthenticatorStore.AddAuthenticator(user, authr); err != nil {
151+
return nil, err
152+
}
153+
154+
return authr, nil
155+
}
156+
157+
// FinishRegistration is a HTTP request handler which should receive the response of navigator.credentials.create(). If
158+
// the request is valid, AuthenticatorStore.AddAuthenticator will be called and an empty response with HTTP status code
159+
// 201 (Created) will be written to the http.ResponseWriter. If authenticator is nil, an error has been written to
160+
// http.ResponseWriter and should be returned as-is.
161+
func (w *WebAuthn) FinishRegistration(r *http.Request, rw http.ResponseWriter, user User, session Session) Authenticator {
162+
var attestationResponse protocol.AttestationResponse
163+
d := json.NewDecoder(r.Body)
164+
d.DisallowUnknownFields()
165+
if err := d.Decode(&attestationResponse); err != nil {
166+
w.writeError(r, rw, protocol.ErrInvalidRequest.WithDebug(err.Error()))
167+
return nil
168+
}
169+
170+
authr, err := w.ParseAndFinishRegistration(attestationResponse, user, session)
171+
if err != nil {
163172
w.writeError(r, rw, err)
164173
return nil
165174
}

0 commit comments

Comments
 (0)