-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
orm.go
281 lines (239 loc) · 9.42 KB
/
orm.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
package sessions
import (
"crypto/subtle"
"database/sql"
"encoding/json"
"strings"
"time"
"github.com/pkg/errors"
"github.com/smartcontractkit/sqlx"
"github.com/smartcontractkit/chainlink/core/auth"
"github.com/smartcontractkit/chainlink/core/bridges"
"github.com/smartcontractkit/chainlink/core/logger"
"github.com/smartcontractkit/chainlink/core/services/pg"
"github.com/smartcontractkit/chainlink/core/utils"
"github.com/smartcontractkit/chainlink/core/utils/mathutil"
)
//go:generate mockery --name ORM --output ./mocks/ --case=underscore
type ORM interface {
FindUser() (User, error)
AuthorizedUserWithSession(sessionID string) (User, error)
DeleteUser() error
DeleteUserSession(sessionID string) error
CreateSession(sr SessionRequest) (string, error)
ClearNonCurrentSessions(sessionID string) error
CreateUser(user *User) error
SetAuthToken(user *User, token *auth.Token) error
CreateAndSetAuthToken(user *User) (*auth.Token, error)
DeleteAuthToken(user *User) error
SetPassword(user *User, newPassword string) error
Sessions(offset, limit int) ([]Session, error)
GetUserWebAuthn(email string) ([]WebAuthn, error)
SaveWebAuthn(token *WebAuthn) error
FindExternalInitiator(eia *auth.Token) (initiator *bridges.ExternalInitiator, err error)
}
type orm struct {
db *sqlx.DB
sessionDuration time.Duration
lggr logger.Logger
}
var _ ORM = (*orm)(nil)
func NewORM(db *sqlx.DB, sessionDuration time.Duration, lggr logger.Logger) ORM {
return &orm{db, sessionDuration, lggr.Named("SessionsORM")}
}
// FindUser will return the one API user, or an error.
func (o *orm) FindUser() (User, error) {
return o.findUser()
}
func (o *orm) findUser() (user User, err error) {
sql := "SELECT * FROM users ORDER BY created_at desc LIMIT 1"
err = o.db.Get(&user, sql)
return
}
// AuthorizedUserWithSession will return the one API user if the Session ID exists
// and hasn't expired, and update session's LastUsed field.
func (o *orm) AuthorizedUserWithSession(sessionID string) (User, error) {
if len(sessionID) == 0 {
return User{}, errors.New("Session ID cannot be empty")
}
result, err := o.db.Exec("UPDATE sessions SET last_used = now() WHERE id = $1 AND last_used + $2 >= now()", sessionID, o.sessionDuration)
if err != nil {
return User{}, err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return User{}, err
}
if rowsAffected == 0 {
return User{}, sql.ErrNoRows
}
return o.FindUser()
}
// DeleteUser will delete the API User in the db.
func (o *orm) DeleteUser() error {
ctx, cancel := pg.DefaultQueryCtx()
defer cancel()
return pg.SqlxTransaction(ctx, o.db, o.lggr, func(tx pg.Queryer) error {
if _, err := tx.Exec("DELETE FROM users"); err != nil {
return err
}
_, err := tx.Exec("DELETE FROM sessions")
return err
})
}
// DeleteUserSession will erase the session ID for the sole API User.
func (o *orm) DeleteUserSession(sessionID string) error {
_, err := o.db.Exec("DELETE FROM sessions WHERE id = $1", sessionID)
return err
}
// GetUserWebAuthn will return a list of structures representing all enrolled WebAuthn
// tokens for the user. This list must be used when logging in (for obvious reasons) but
// must also be used for registration to prevent the user from enrolling the same hardware
// token multiple times.
func (o *orm) GetUserWebAuthn(email string) ([]WebAuthn, error) {
var uwas []WebAuthn
err := o.db.Select(&uwas, "SELECT email, public_key_data FROM web_authns WHERE LOWER(email) = $1", strings.ToLower(email))
if err != nil {
return uwas, err
}
// In the event of not found, there is no MFA on this account and it is not an error
// so this returns either an empty list or list of WebAuthn rows
return uwas, nil
}
// CreateSession will check the password in the SessionRequest against
// the hashed API User password in the db. Also will check WebAuthn if it's
// enabled for that user.
func (o *orm) CreateSession(sr SessionRequest) (string, error) {
user, err := o.FindUser()
if err != nil {
return "", err
}
lggr := o.lggr.With("user", user.Email)
lggr.Debugw("Found user")
// Do email and password check first to prevent extra database look up
// for MFA tokens leaking if an account has MFA tokens or not.
if !constantTimeEmailCompare(sr.Email, user.Email) {
return "", errors.New("Invalid email")
}
if !utils.CheckPasswordHash(sr.Password, user.HashedPassword) {
return "", errors.New("Invalid password")
}
// Load all valid MFA tokens associated with user's email
uwas, err := o.GetUserWebAuthn(user.Email)
if err != nil {
// There was an error with the database query
lggr.Errorf("Could not fetch user's MFA data: %v", err)
return "", errors.New("MFA Error")
}
// No webauthn tokens registered for the current user, so normal authentication is now complete
if len(uwas) == 0 {
lggr.Infof("No MFA for user. Creating Session")
session := NewSession()
_, err = o.db.Exec("INSERT INTO sessions (id, last_used, created_at) VALUES ($1, now(), now())", session.ID)
return session.ID, err
}
// Next check if this session request includes the required WebAuthn challenge data
// if not, return a 401 error for the frontend to prompt the user to provide this
// data in the next round trip request (tap key to include webauthn data on the login page)
if sr.WebAuthnData == "" {
lggr.Warnf("Attempted login to MFA user. Generating challenge for user.")
options, webauthnError := BeginWebAuthnLogin(user, uwas, sr)
if webauthnError != nil {
lggr.Errorf("Could not begin WebAuthn verification: %v", webauthnError)
return "", errors.New("MFA Error")
}
j, jsonError := json.Marshal(options)
if jsonError != nil {
lggr.Errorf("Could not serialize WebAuthn challenge: %v", jsonError)
return "", errors.New("MFA Error")
}
return "", errors.New(string(j))
}
// The user is at the final stage of logging in with MFA. We have an
// attestation back from the user, we now need to verify that it is
// correct.
err = FinishWebAuthnLogin(user, uwas, sr)
if err != nil {
// The user does have WebAuthn enabled but failed the check
lggr.Errorf("User sent an invalid attestation: %v", err)
return "", errors.New("MFA Error")
}
lggr.Infof("User passed MFA authentication and login will proceed")
// This is a success so we can create the sessions
session := NewSession()
_, err = o.db.Exec("INSERT INTO sessions (id, last_used, created_at) VALUES ($1, now(), now())", session.ID)
return session.ID, err
}
const constantTimeEmailLength = 256
func constantTimeEmailCompare(left, right string) bool {
length := mathutil.Max(constantTimeEmailLength, len(left), len(right))
leftBytes := make([]byte, length)
rightBytes := make([]byte, length)
copy(leftBytes, left)
copy(rightBytes, right)
return subtle.ConstantTimeCompare(leftBytes, rightBytes) == 1
}
// ClearNonCurrentSessions removes all sessions but the id passed in.
func (o *orm) ClearNonCurrentSessions(sessionID string) error {
_, err := o.db.Exec("DELETE FROM sessions where id != $1", sessionID)
return err
}
// Creates creates the user.
func (o *orm) CreateUser(user *User) error {
sql := "INSERT INTO users (email, hashed_password, created_at, updated_at) VALUES ($1, $2, now(), now()) RETURNING *"
return o.db.Get(user, sql, user.Email, user.HashedPassword)
}
// SetAuthToken updates the user to use the given Authentication Token.
func (o *orm) SetPassword(user *User, newPassword string) error {
hashedPassword, err := utils.HashPassword(newPassword)
if err != nil {
return err
}
sql := "UPDATE users SET hashed_password = $1, updated_at = now() WHERE email = $2 RETURNING *"
return o.db.Get(user, sql, hashedPassword, user.Email)
}
func (o *orm) CreateAndSetAuthToken(user *User) (*auth.Token, error) {
newToken := auth.NewToken()
err := o.SetAuthToken(user, newToken)
if err != nil {
return nil, err
}
return newToken, nil
}
// SetAuthToken updates the user to use the given Authentication Token.
func (o *orm) SetAuthToken(user *User, token *auth.Token) error {
salt := utils.NewSecret(utils.DefaultSecretSize)
hashedSecret, err := auth.HashedSecret(token, salt)
if err != nil {
return errors.Wrap(err, "user")
}
sql := "UPDATE users SET token_salt = $1, token_key = $2, token_hashed_secret = $3, updated_at = now() WHERE email = $4 RETURNING *"
return o.db.Get(user, sql, salt, token.AccessKey, hashedSecret, user.Email)
}
// DeleteAuthToken clears and disables the users Authentication Token.
func (o *orm) DeleteAuthToken(user *User) error {
sql := "UPDATE users SET token_salt = '', token_key = '', token_hashed_secret = '', updated_at = now() WHERE email = $1 RETURNING *"
return o.db.Get(user, sql, user.Email)
}
// SaveWebAuthn saves new WebAuthn token information.
func (o *orm) SaveWebAuthn(token *WebAuthn) error {
sql := "INSERT INTO web_authns (email, public_key_data) VALUES ($1, $2)"
_, err := o.db.Exec(sql, token.Email, token.PublicKeyData)
return err
}
// Sessions returns all sessions limited by the parameters.
func (o *orm) Sessions(offset, limit int) (sessions []Session, err error) {
sql := `SELECT * FROM sessions ORDER BY created_at, id LIMIT $1 OFFSET $2;`
if err = o.db.Select(&sessions, sql, limit, offset); err != nil {
return
}
return
}
// NOTE: this is duplicated from the bridges ORM to appease the AuthStorer interface
func (o *orm) FindExternalInitiator(
eia *auth.Token,
) (*bridges.ExternalInitiator, error) {
exi := &bridges.ExternalInitiator{}
err := o.db.Get(exi, `SELECT * FROM external_initiators WHERE access_key = $1`, eia.AccessKey)
return exi, err
}