-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebauthn.go
More file actions
301 lines (257 loc) · 10.5 KB
/
webauthn.go
File metadata and controls
301 lines (257 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
package mfa
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
)
// WebauthnMeta holds metadata about the calling service for use in WebAuthn responses.
// Since this service/api is consumed by multiple sources this information cannot
// be stored in the envConfig
type WebauthnMeta struct {
RPDisplayName string `json:"RPDisplayName"` // Display Name for your site
RPID string `json:"RPID"` // Generally the FQDN for your site
RPOrigin string `json:"RPOrigin"` // The origin URL for WebAuthn requests
RPIcon string `json:"RPIcon"` // Optional icon URL for your site
UserUUID string `json:"UserUUID"`
Username string `json:"Username"`
UserDisplayName string `json:"UserDisplayName"`
UserIcon string `json:"UserIcon"`
}
// beginRegistrationResponse adds uuid to response for consumers that depend on this api to generate the uuid
type beginRegistrationResponse struct {
UUID string `json:"uuid"`
protocol.CredentialCreation
}
// finishRegistrationResponse contains the response data for the FinishRegistration endpoint
type finishRegistrationResponse struct {
KeyHandleHash string `json:"key_handle_hash"`
}
// finishLoginResponse contains the response data for the FinishLogin endpoint
type finishLoginResponse struct {
KeyHandleHash string `json:"key_handle_hash"`
}
// BeginRegistration processes the first half of the Webauthn Registration flow. It is the handler for the
// "POST /webauthn/register" endpoint, initiated by the client when creation of a new passkey is requested.
func (a *App) BeginRegistration(w http.ResponseWriter, r *http.Request) {
user, err := getWebauthnUser(r)
if err != nil {
log.Printf("failed to get user for BeginRegistration: %s", err)
jsonResponse(w, internalServerError, http.StatusInternalServerError)
return
}
// If user.id is empty, treat as new user/registration
if user.ID == "" {
user.ID = NewUUID()
}
options, err := user.BeginRegistration()
if err != nil {
log.Printf("failed to begin registration: %s", err)
jsonResponse(w, invalidRequest, http.StatusBadRequest)
return
}
response := beginRegistrationResponse{
user.ID,
*options,
}
jsonResponse(w, response, http.StatusOK)
}
// FinishRegistration processes the last half of the Webauthn Registration flow. It is the handler for the
// "PUT /webauthn/register" endpoint, initiated by the client with information encrypted by the new private key.
func (a *App) FinishRegistration(w http.ResponseWriter, r *http.Request) {
user, err := getWebauthnUser(r)
if err != nil {
log.Printf("failed to get user for FinishRegistration: %s", err)
jsonResponse(w, internalServerError, http.StatusInternalServerError)
return
}
keyHandleHash, err := user.FinishRegistration(r)
if err != nil {
log.Printf("failed to finish registration: %s", err)
jsonResponse(w, invalidRequest, http.StatusBadRequest)
return
}
response := finishRegistrationResponse{
KeyHandleHash: keyHandleHash,
}
jsonResponse(w, response, http.StatusOK) // Handle next steps
}
// BeginLogin processes the first half of the Webauthn Authentication flow. It is the handler for the
// "POST /webauthn/login" endpoint, initiated by the client at the beginning of a login request.
func (a *App) BeginLogin(w http.ResponseWriter, r *http.Request) {
user, err := getWebauthnUser(r)
if err != nil {
log.Printf("failed to get user for BeginLogin: %s", err)
jsonResponse(w, internalServerError, http.StatusInternalServerError)
return
}
options, err := user.BeginLogin()
if err != nil {
log.Printf("error beginning user login: %s", err)
jsonResponse(w, invalidRequest, http.StatusBadRequest)
return
}
jsonResponse(w, options, http.StatusOK)
}
// FinishLogin processes the second half of the Webauthn Authentication flow. It is the handler for the
// "PUT /webauthn/login" endpoint, initiated by the client with login data signed with the private key.
func (a *App) FinishLogin(w http.ResponseWriter, r *http.Request) {
user, err := getWebauthnUser(r)
if err != nil {
log.Printf("failed to get user for FinishLogin: %s", err)
jsonResponse(w, internalServerError, http.StatusInternalServerError)
return
}
credential, err := user.FinishLogin(r)
if err != nil {
// SonarQube flagged this as vulnerable to injection attacks. Rather than exhaustively search for places
// where user input is inserted into the error message, I'll just sanitize it as recommended.
sanitizedError := strings.ReplaceAll(strings.ReplaceAll(err.Error(), "\n", "_"), "\r", "_")
log.Printf("error finishing user login: %s", sanitizedError)
jsonResponse(w, invalidRequest, http.StatusBadRequest)
return
}
resp := finishLoginResponse{
KeyHandleHash: hashAndEncodeKeyHandle(credential.ID),
}
jsonResponse(w, resp, http.StatusOK)
}
// DeleteUser is the handler for the "DELETE /webauthn/user" endpoint. It removes a user and any stored passkeys owned
// by the user.
func (a *App) DeleteUser(w http.ResponseWriter, r *http.Request) {
user, err := getWebauthnUser(r)
if err != nil {
log.Printf("failed to get user for DeleteUser: %s", err)
jsonResponse(w, internalServerError, http.StatusInternalServerError)
return
}
if err := user.Delete(); err != nil {
log.Printf("error deleting user: %s", err)
jsonResponse(w, internalServerError, http.StatusInternalServerError)
return
}
jsonResponse(w, nil, http.StatusNoContent)
}
// DeleteCredential is the handler for the "DELETE /webauthn/credential/{credID}" endpoint. It removes a single
// passkey identified by "credID", which is the key_handle_hash returned by the FinishRegistration endpoint, or "u2f"
// if it is a legacy U2F credential, in which case that user is saved with all of its legacy u2f fields blanked out.
func (a *App) DeleteCredential(w http.ResponseWriter, r *http.Request) {
user, err := getWebauthnUser(r)
if err != nil {
log.Printf("failed to get user for DeleteCredential: %s", err)
jsonResponse(w, internalServerError, http.StatusInternalServerError)
return
}
credID := r.PathValue(IDParam)
if credID == "" {
err := fmt.Errorf("%s path parameter not provided to DeleteCredential, path: %s", IDParam, r.URL.Path)
log.Printf("%s", err)
jsonResponse(w, invalidRequest, http.StatusBadRequest)
return
}
status, err := user.DeleteCredential(credID)
if err != nil {
log.Printf("error deleting user credential (%d): %s", status, err)
}
switch status {
case http.StatusNoContent:
jsonResponse(w, nil, status)
case http.StatusNotFound:
jsonResponse(w, "Not found", status)
case http.StatusInternalServerError:
jsonResponse(w, internalServerError, status)
default:
log.Printf("unexpected status code (%d)", status)
jsonResponse(w, internalServerError, http.StatusInternalServerError)
}
}
// fixStringEncoding converts a string from standard Base64 to Base64-URL
func fixStringEncoding(content string) string {
content = strings.ReplaceAll(content, "+", "-")
content = strings.ReplaceAll(content, "/", "_")
content = strings.ReplaceAll(content, "=", "")
return content
}
// fixEncoding converts a string from standard Base64 to Base64-URL as an io.Reader
func fixEncoding(content []byte) io.Reader {
allStr := string(content)
return bytes.NewReader([]byte(fixStringEncoding(allStr)))
}
// getWebAuthnFromApiMeta creates a new WebAuthn object from the metadata provided in a web request. Typically used in
// the API authentication phase, early in the handler or in a middleware.
func getWebAuthnFromApiMeta(meta WebauthnMeta) (*webauthn.WebAuthn, error) {
web, err := webauthn.New(&webauthn.Config{
RPDisplayName: meta.RPDisplayName, // Display Name for your site
RPID: meta.RPID, // Generally the FQDN for your site
RPOrigins: []string{meta.RPOrigin}, // The origin URL for WebAuthn requests
Debug: true,
})
if err != nil {
log.Printf("failed to get new webauthn: %s", err)
}
return web, nil
}
// getWebauthnMetaFromRequest creates an WebauthnMeta object from request headers, including basic validation checks. Used during
// API authentication.
func getWebauthnMetaFromRequest(r *http.Request) (WebauthnMeta, error) {
meta := WebauthnMeta{
RPDisplayName: r.Header.Get("x-mfa-RPDisplayName"),
RPID: r.Header.Get("x-mfa-RPID"),
RPOrigin: r.Header.Get("x-mfa-RPOrigin"),
RPIcon: r.Header.Get("x-mfa-RPIcon"),
UserUUID: r.Header.Get("x-mfa-UserUUID"),
Username: r.Header.Get("x-mfa-Username"),
UserDisplayName: r.Header.Get("x-mfa-UserDisplayName"),
UserIcon: r.Header.Get("x-mfa-UserIcon"),
}
// check that required fields are provided
if meta.RPDisplayName == "" {
msg := "missing required header: x-mfa-RPDisplayName"
return WebauthnMeta{}, errors.New(msg)
}
if meta.RPID == "" {
msg := "missing required header: x-mfa-RPID"
return WebauthnMeta{}, errors.New(msg)
}
if meta.Username == "" {
msg := "missing required header: x-mfa-Username"
return WebauthnMeta{}, errors.New(msg)
}
if meta.UserDisplayName == "" {
msg := "missing required header: x-mfa-UserDisplayName"
return WebauthnMeta{}, errors.New(msg)
}
return meta, nil
}
// getWebauthnUser returns the authenticated WebauthnUser from the request context. The authentication middleware or
// early handler processing inserts the authenticated user into the context for retrieval by this function.
func getWebauthnUser(r *http.Request) (WebauthnUser, error) {
user, ok := r.Context().Value(UserContextKey).(WebauthnUser)
if !ok {
return WebauthnUser{}, fmt.Errorf("unable to get user from request context")
}
return user, nil
}
func authWebauthnUser(r *http.Request, storage *Storage, apiKey ApiKey) (User, error) {
apiMeta, err := getWebauthnMetaFromRequest(r)
if err != nil {
log.Printf("unable to retrieve API meta information from request: %s", err)
return nil, fmt.Errorf("unable to retrieve API meta information from request: %w", err)
}
webAuthnClient, err := getWebAuthnFromApiMeta(apiMeta)
if err != nil {
return nil, fmt.Errorf("unable to create webauthn client from api meta config: %w", err)
}
user := NewWebauthnUser(apiMeta, storage, apiKey, webAuthnClient)
// If this user exists (api key value is not empty), make sure the calling API Key owns the user and is allowed to operate on it
if user.ApiKeyValue != "" && user.ApiKeyValue != apiKey.Key {
log.Printf("api key %s tried to access user %s but that user does not belong to that api key", apiKey.Key, user.ID)
return nil, fmt.Errorf("user does not exist")
}
return user, nil
}