-
Notifications
You must be signed in to change notification settings - Fork 654
Expand file tree
/
Copy pathpasskey_manage.go
More file actions
160 lines (134 loc) · 5.16 KB
/
passkey_manage.go
File metadata and controls
160 lines (134 loc) · 5.16 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
package api
import (
"encoding/json"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/gofrs/uuid"
"github.com/supabase/auth/internal/api/apierrors"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
"github.com/supabase/auth/internal/utilities"
)
// PasskeyListItem is the response shape for a single passkey in the list and management endpoints.
type PasskeyListItem struct {
ID string `json:"id"`
FriendlyName string `json:"friendly_name,omitempty"`
AAGUID *uuid.UUID `json:"aaguid,omitempty"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
}
// PasskeyUpdateParams is the request body for PATCH /passkeys/{passkey_id}.
type PasskeyUpdateParams struct {
FriendlyName string `json:"friendly_name"`
}
// TODO(fm): we should not allow any of the following operations on credentials used for
// MFA webauthn factors — in particular, the deletion operation.
// PasskeyList handles GET /passkeys/.
// Requires authentication. Returns all passkeys for the authenticated user.
func (a *API) PasskeyList(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
user := getUser(ctx)
db := a.db.WithContext(ctx)
creds, err := models.FindWebAuthnCredentialsByUserID(db, user.ID)
if err != nil {
return apierrors.NewInternalServerError("Database error loading passkeys").WithInternalError(err)
}
items := make([]PasskeyListItem, len(creds))
for i, cred := range creds {
items[i] = toPasskeyListItem(cred)
}
return sendJSON(w, http.StatusOK, items)
}
// PasskeyUpdate handles PATCH /passkeys/{passkey_id}.
// Requires authentication. Updates the friendly_name of a passkey owned by the authenticated user.
func (a *API) PasskeyUpdate(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
config := a.config
user := getUser(ctx)
db := a.db.WithContext(ctx)
passkeyID, err := uuid.FromString(chi.URLParam(r, "passkey_id"))
if err != nil {
return apierrors.NewNotFoundError(apierrors.ErrorCodeValidationFailed, "Passkey not found")
}
params := &PasskeyUpdateParams{}
body, err := utilities.GetBodyBytes(r)
if err != nil {
return apierrors.NewBadRequestError(apierrors.ErrorCodeBadJSON, "Could not read request body")
}
if err := json.Unmarshal(body, params); err != nil {
return apierrors.NewBadRequestError(apierrors.ErrorCodeBadJSON, "Could not parse request body as JSON: %v", err)
}
if params.FriendlyName == "" {
return apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "friendly_name is required")
}
if len(params.FriendlyName) > 120 {
return apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "friendly_name must be 120 characters or less")
}
cred, err := models.FindWebAuthnCredentialByIDAndUserID(db, passkeyID, user.ID)
if err != nil {
if models.IsNotFoundError(err) {
return apierrors.NewNotFoundError(apierrors.ErrorCodeValidationFailed, "Passkey not found")
}
return apierrors.NewInternalServerError("Database error loading passkey").WithInternalError(err)
}
err = db.Transaction(func(tx *storage.Connection) error {
if terr := cred.UpdateFriendlyName(tx, params.FriendlyName); terr != nil {
return terr
}
if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, models.PasskeyUpdatedAction, utilities.GetIPAddress(r), map[string]any{
"passkey_id": cred.ID,
}); terr != nil {
return terr
}
return nil
})
if err != nil {
return apierrors.NewInternalServerError("Database error updating passkey").WithInternalError(err)
}
return sendJSON(w, http.StatusOK, toPasskeyListItem(cred))
}
// PasskeyDelete handles DELETE /passkeys/{passkey_id}.
// Requires authentication. Deletes a passkey owned by the authenticated user.
func (a *API) PasskeyDelete(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
config := a.config
user := getUser(ctx)
db := a.db.WithContext(ctx)
passkeyID, err := uuid.FromString(chi.URLParam(r, "passkey_id"))
if err != nil {
return apierrors.NewNotFoundError(apierrors.ErrorCodeValidationFailed, "Passkey not found")
}
cred, err := models.FindWebAuthnCredentialByIDAndUserID(db, passkeyID, user.ID)
if err != nil {
if models.IsNotFoundError(err) {
return apierrors.NewNotFoundError(apierrors.ErrorCodeValidationFailed, "Passkey not found")
}
return apierrors.NewInternalServerError("Database error loading passkey").WithInternalError(err)
}
err = db.Transaction(func(tx *storage.Connection) error {
if terr := cred.Delete(tx); terr != nil {
return terr
}
if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, models.PasskeyDeletedAction, utilities.GetIPAddress(r), map[string]any{
"passkey_id": cred.ID,
}); terr != nil {
return terr
}
return nil
})
if err != nil {
return apierrors.NewInternalServerError("Database error deleting passkey").WithInternalError(err)
}
w.WriteHeader(http.StatusNoContent)
return nil
}
func toPasskeyListItem(cred *models.WebAuthnCredential) PasskeyListItem {
return PasskeyListItem{
ID: cred.ID.String(),
FriendlyName: cred.FriendlyName,
AAGUID: cred.AAGUID,
CreatedAt: cred.CreatedAt,
LastUsedAt: cred.LastUsedAt,
}
}