-
Notifications
You must be signed in to change notification settings - Fork 2
/
db.go
338 lines (301 loc) · 8.64 KB
/
db.go
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
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/fs"
"time"
"crawshaw.dev/jsonfile"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/lstoll/oidc/core"
)
// schemaVersion is the current version of the database schema. It may be used
// to handle schema changes in the future.
const schemaVersion uint = 1
var (
ErrUserNotFound = errors.New("user not found")
ErrUserNotActivated = errors.New("user not activated")
ErrUserEmailTaken = errors.New("user email taken")
ErrUnauthenticatedUser = errors.New("unauthenticated user")
ErrCredentialNotFound = errors.New("credential not found")
)
// schema is the database schema, serializable in JSON.
type schema struct {
// Version is the schemaVersion of the on-disk database.
Version uint `json:"version"`
// Users stores all users in the system, along with their WebAuthn credentials.
// The key is the user's ID.
Users map[string]User `json:"users"`
// OIDCSession is a map of session ID to core.Session serialized in JSON.
OIDCSessions map[string]json.RawMessage `json:"oidcSessions"`
// AuthenticatedUsers tracks authenticated users.
// The key is the session ID from OIDCSessions.
AuthenticatedUsers map[string]AuthenticatedUser `json:"authenticatedUsers"`
// Keysets is a map of the keyset name to the keyset data.
Keysets map[string]DBKeyset `json:"keysets"`
}
// DBKeyset represents a rotating keyset in the database.
type DBKeyset struct {
// LastRotated is the time the last rotation was performed on the set
LastRotated time.Time `json:"lastRotated,omitempty"`
// UpcomingKeyID is the keyset key ID for the newly-provisioned key, that is
// waiting to get rotated in to being active.
UpcomingKeyID uint32 `json:"upcomingKeyID"`
// Keyset is the JSON formatted representation of the tink keyset.
Keyset json.RawMessage `json:"keyset,omitempty"`
}
// User implements webauthn.User.
type User struct {
// ID uniquely identifies the user, and is assigned automatically.
// This is stable for the lifetime of the user.
ID string `json:"id"`
// Email address for the user. This is changeable, however it must be unique as
// it's the "exposed" ID for a user for login purposes.
Email string `json:"email"`
// FullName to refer to the user as.
FullName string `json:"fullName"`
// EnrollmentKey used for enrolling tokens for a user. It is removed when
// the token is enrolled.
EnrollmentKey string `json:"enrollmentKey"`
// Credentials is the user's WebAuthn credentials keyed by a user-provided identifier.
Credentials map[string]WebauthnCredential `json:"credentials"`
}
// WebauthnCredential wraps the webauthn.Credential with some more metadata.
type WebauthnCredential struct {
webauthn.Credential
Name string `json:"name"`
AddedAt time.Time `json:"addedAt"`
}
func (u User) WebAuthnID() []byte {
return []byte(u.ID)
}
func (u User) WebAuthnName() string {
return u.Email
}
func (u User) WebAuthnDisplayName() string {
return u.FullName
}
func (u User) WebAuthnIcon() string {
return ""
}
func (u User) WebAuthnCredentials() []webauthn.Credential {
var ret []webauthn.Credential
for _, v := range u.Credentials {
ret = append(ret, v.Credential)
}
return ret
}
// AuthenticatedUser are the details flagged for an authenticated user of the
// system.
type AuthenticatedUser struct {
// Subject (required) is the unique identifier for the authenticated user.
// This should be stable over time.
Subject string `json:"subject"`
// Email (optional), for when the email/profile scope is requested
Email string `json:"email,omitempty"`
// FullName (optional), for when the profile scope is requested
FullName string `json:"fullName,omitempty"`
// Groups (optional), for when the groups scope is requested
Groups []string `json:"groups,omitempty"`
// ExtraClaims (optional) fields to add to the returned ID token claims
ExtraClaims map[string]interface{} `json:"extraClaims,omitempty"`
// PolicyContext is internal data, that is passed to the policies that are
// evaluated downstream. This data is not presented to the user.
PolicyContext map[string]interface{} `json:"policyContext,omitempty"`
}
// openDB opens the database at path, creating the file if it does not exist.
func openDB(path string) (*DB, error) {
f, err := jsonfile.Load[schema](path)
if errors.Is(err, fs.ErrNotExist) {
f, err = jsonfile.New[schema](path)
if err != nil {
return nil, err
}
err = f.Write(func(db *schema) error {
db.Version = schemaVersion
return nil
})
}
if err != nil {
return nil, err
}
var ver uint
f.Read(func(db *schema) { ver = db.Version })
if ver != schemaVersion {
return nil, fmt.Errorf("unsupported database version: %d", ver)
}
return &DB{f: f}, nil
}
// DB is the IDP database.
// The database consists of a single JSON file stored on disk.
// It contains unencrypted private key material.
type DB struct {
f *jsonfile.JSONFile[schema]
}
func (db *DB) Reload() error {
return db.f.Reload(func(db *schema) error {
if db.Version != schemaVersion {
return fmt.Errorf("unsupported database version: %d", db.Version)
}
return nil
})
}
func (db *DB) GetUserByID(userID string) (User, error) {
var (
v User
ok bool
)
db.f.Read(func(db *schema) {
v, ok = db.Users[userID]
})
if !ok {
return v, ErrUserNotFound
}
return v, nil
}
func (db *DB) CreateUser(user User) (User, error) {
if user.ID != "" {
return User{}, fmt.Errorf("user ID already assigned")
}
if user.EnrollmentKey != "" {
return User{}, fmt.Errorf("user enrollment key already assigned")
}
user.ID = uuid.NewString()
user.EnrollmentKey = uuid.NewString()
user.Credentials = make(map[string]WebauthnCredential)
err := db.f.Write(func(db *schema) error {
if _, ok := db.Users[user.ID]; ok {
panic("generated UUID already in use")
}
var dupe bool
for _, u := range db.Users {
if u.Email == user.Email {
dupe = true
break
}
}
if dupe {
return ErrUserEmailTaken
}
if len(db.Users) == 0 {
db.Users = make(map[string]User)
}
db.Users[user.ID] = user
return nil
})
if err != nil {
return User{}, err
}
return user, nil
}
func (db *DB) UpdateUser(user User) error {
if user.ID == "" {
return errors.New("user ID missing")
}
err := db.f.Write(func(db *schema) error {
if _, ok := db.Users[user.ID]; !ok {
return ErrUserNotFound
}
for _, u := range db.Users {
if u.ID != user.ID && u.Email == user.Email {
return ErrUserEmailTaken
}
}
db.Users[user.ID] = user
return nil
})
return err
}
func (db *DB) UpdateUserCredential(userID string, cred webauthn.Credential) error {
err := db.f.Write(func(db *schema) error {
user, ok := db.Users[userID]
if !ok {
return ErrUserNotFound
}
for k, v := range user.Credentials {
if bytes.Equal(cred.ID, v.ID) {
c := db.Users[userID].Credentials[k]
c.Credential = cred
db.Users[userID].Credentials[k] = c
return nil
}
}
return ErrCredentialNotFound
})
return err
}
func (db *DB) CreateUserCredential(userID, name string, cred WebauthnCredential) error {
err := db.f.Write(func(db *schema) error {
if _, ok := db.Users[userID]; !ok {
return ErrUserNotFound
}
u := db.Users[userID]
u.EnrollmentKey = ""
u.Credentials[name] = cred
db.Users[userID] = u
return nil
})
return err
}
func (db *DB) DeleteUserCredential(userID string, name string) error {
err := db.f.Write(func(db *schema) error {
if _, ok := db.Users[userID]; !ok {
return ErrUserNotFound
}
delete(db.Users[userID].Credentials, name)
return nil
})
return err
}
func (db *DB) ListUsers() []User {
var users []User
db.f.Read(func(db *schema) {
for _, v := range db.Users {
users = append(users, v)
}
})
return users
}
func (db *DB) Authenticate(sessionID string, auth AuthenticatedUser) error {
return db.f.Write(func(db *schema) error {
if len(db.AuthenticatedUsers) == 0 {
db.AuthenticatedUsers = make(map[string]AuthenticatedUser)
}
db.AuthenticatedUsers[sessionID] = auth
return nil
})
}
func (db *DB) GetAuthenticatedUser(sessionID string) (AuthenticatedUser, error) {
var (
v AuthenticatedUser
ok bool
)
db.f.Read(func(db *schema) {
v, ok = db.AuthenticatedUsers[sessionID]
})
if !ok {
return AuthenticatedUser{}, ErrUnauthenticatedUser
}
return v, nil
}
func (db *DB) SessionManager() core.SessionManager {
return &sessionManager{f: db.f}
}
func (db *DB) GetKeyset(ks Keyset) DBKeyset {
var ret DBKeyset
db.f.Read(func(data *schema) {
ret = data.Keysets[ks.Name]
})
return ret
}
func (db *DB) PutKeyset(ks Keyset, stored DBKeyset) error {
return db.f.Write(func(db *schema) error {
if db.Keysets == nil {
db.Keysets = map[string]DBKeyset{}
}
db.Keysets[ks.Name] = stored
return nil
})
}