-
Notifications
You must be signed in to change notification settings - Fork 3
/
authenticator.go
536 lines (490 loc) · 21 KB
/
authenticator.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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
// Package authenticator defines the domain model for user authentication.
package authenticator
import (
"context"
"database/sql"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
)
// TokenState represents a state of a JWT token.
// A token may represent an intermediary state prior
// to authorization (ex. TOTP code is required)
type TokenState string
// DeliveryMethod represents a mechanism to send messages
// to users.
type DeliveryMethod string
// TFAOptions represents options a user may use to complete
// 2FA.
type TFAOptions string
// MessageType describes a classification of a Message
type MessageType string
const (
// OTPEmail allows a user to complete TFA with an OTP
// code delivered via email.
OTPEmail TFAOptions = "otp_email"
// OTPPhone allows a user to complete TFA with an OTP
// code delivered via phone.
OTPPhone = "otp_phone"
// TOTP allows a user to complete TFA with a TOTP
// device or application.
TOTP = "totp"
// FIDODevice allows a user to complete TFA with a Webauthn
// compliant device.
FIDODevice = "device"
)
const (
// Phone is a delivery method for text messages.
Phone DeliveryMethod = "phone"
// Email is a delivery method for email.
Email = "email"
)
const (
// JWTPreAuthorized represents the state of a user before completing
// the TFA step of signup or login.
JWTPreAuthorized TokenState = "pre_authorized"
// JWTAuthorized represents a the state of a user after completing
// the final step of login or signup.
JWTAuthorized TokenState = "authorized"
)
const (
// OTPAddress is a message containing an OTP code for contact verification
OTPAddress MessageType = "otp_address"
// OTPResend is a message containing an OTP code
OTPResend MessageType = "otp_resend"
// OTPLogin is a message containing an OTP code for login.
OTPLogin MessageType = "otp_login"
// OTPSignup is a message containing an OTP code for signup.
OTPSignup MessageType = "otp_signup"
)
// User represents a user who is registered with the service.
type User struct {
// ID is a unique ID for the user.
ID string
// Phone is a phone number associated with the account.
Phone sql.NullString
// Email is an email address associated with the account.
Email sql.NullString
// Password is the current User provided password.
Password string
// TFASecret is a a secret string used to generate 2FA TOTP codes.
TFASecret string
// IsPhoneAllowed specifies a user may complete authentication
// by verifying an OTP code delivered through SMS.
IsPhoneOTPAllowed bool
// IsEmailOTPAllowed specifies a user may complete authentication
// by verifying an OTP code delivered through email.
IsEmailOTPAllowed bool
// IsTOTPAllowed specifies a user may complete authentication
// by verifying a TOTP code.
IsTOTPAllowed bool
// IsDeviceAllowed specifies a user may complete authentication
// by verifying a WebAuthn capable device.
IsDeviceAllowed bool
// IsVerified tells us if a user confirmed ownership of
// an email or phone number by validating a one time code
// after registration.
IsVerified bool
CreatedAt time.Time
UpdatedAt time.Time
}
// DefaultTFA is the recommended enabled TFA option clients should
// offer a user.
func (u *User) DefaultTFA() TFAOptions {
if u.IsDeviceAllowed {
return FIDODevice
}
if u.IsTOTPAllowed {
return TOTP
}
if u.IsEmailOTPAllowed {
return OTPEmail
}
return OTPPhone
}
// DefaultOTPDelivery returns the default OTP delivery method.
func (u *User) DefaultOTPDelivery() DeliveryMethod {
if u.Email.String != "" {
return Email
}
return Phone
}
// CanSendDefaultOTP determines if an OTP code should be sent out
// to a user immediately as a 2FA option.
func (u *User) CanSendDefaultOTP() bool {
if u.IsDeviceAllowed || u.IsTOTPAllowed {
return false
}
isOTPEnabled := u.IsPhoneOTPAllowed || u.IsEmailOTPAllowed
return isOTPEnabled
}
// DefaultName returns the default name for a user (email or phone).
func (u *User) DefaultName() string {
if u.Email.String != "" {
return u.Email.String
}
return u.Phone.String
}
// Device represents a device capable of attesting to a User's
// identity. Examples options include a FIDO U2F key or
// fingerprint sensor.
type Device struct {
// ID is a unique service ID for the device.
ID string
// UserID is the User's ID associated with the device
UserID string
// ClientID is a non unique ID generated by the client
// during device registration.
ClientID []byte
// PublicKey is the public key of a device used for signing
// purposes.
PublicKey []byte
// Name is a User supplied human readable name for a device.
Name string
// AAGUID is the globally unique identifier of the authentication
// device.
AAGUID []byte
// SignCount is the stored signature counter of the device.
// This value is increment to match the device counter on
// each successive authentication. If the our value is larger
// than or equal to the device value, it is indicative that the
// device may be cloned or malfunctioning.
SignCount uint32
CreatedAt time.Time
UpdatedAt time.Time
}
// LoginHistory represents a login associated with a user.
type LoginHistory struct {
// TokenID is the ID of a JWT token.
TokenID string `json:"tokenId"`
// UserID is the User's ID associated with the login record.
UserID string `json:"-"`
// IsRevoked is a boolean indicating the token has
// been revoked. Tokens are invalidated through
// expiry or revocation.
IsRevoked bool `json:"isRevoked"`
// IPAddress is the most recent address associated
// with the login. This value may change
// if a token was refreshed.
IPAddress sql.NullString `json:"ipAddress"`
// ExpiresAt is the expiry time of the JWT token.
ExpiresAt time.Time `json:"expiresAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// Token is a token that provides proof of User authentication.
type Token struct {
// jwt.StandardClaims provides standard JWT fields
// such as Audience, ExpiresAt, Id, Issuer.
jwt.StandardClaims
// ClientID is the unhashed ID stored securely on the client and used
// to validate the token request source. It is not embedded in the
// the JWT token body.
ClientID string `json:"-"`
// ClientIDHash is hash of an ID stored in the client for which
// the token was delivered to. A token is only valid when the
// hash's corresponding ClientID is delivered alongside the JWT token.
ClientIDHash string `json:"client_id"`
// UserID is the User's ID.
UserID string `json:"user_id"`
// Email is a User's email.
Email string `json:"email"`
// Phone is a User's phone number.
Phone string `json:"phone_number"`
// State is the current state of the user at the time
// the token was issued.
State TokenState `json:"state"`
// CodeHash is the hash of a randomly generated code used
// to validate an OTP code and escalate the token to an
// authorized token.
CodeHash string `json:"code,omitempty"`
// Code is the unhashed value of CodeHash. This value is
// not persisted and returned to the client outside of the JWT
// response through an alternative mechanism (e.g. Email). It is
// validated by ensuring the SHA512 hash of the value matches the
// CodeHash embedded in the token.
Code string `json:"-"`
// RefreshToken is the unhashed refresh token stored securely on
// the client and used to refresh an expired JWT token. RefreshTokens
// have separate expiry times compared to the JWT token.
RefreshToken string `json:"-"`
// RefreshTokenHash is the hash of a RefreshToken stored on the client
// for which the JWT token was delivered to. A RefreshToken is validated
// by ensuring the SHA512 hash of the value matches the RefreshTokenHash.
RefreshTokenHash string `json:"refresh_token"`
// TFAOptions represents available options a user may use to complete
// 2FA.
TFAOptions []TFAOptions `json:"tfa_options"`
// DefaultTFA is the develop TFA method clients should offer a user.
DefaultTFA TFAOptions `json:"default_tfa"`
}
// Message is a message to be delivered to a user.
type Message struct {
// Type describes the classification of a Message.
Type MessageType
// Subject is a human readable subject describe the Message.
Subject string
// Delivery type of the message (e.g. phone or email).
Delivery DeliveryMethod
// Vars contains key/value variables to populate
// templated content.
Vars map[string]string
// Content of the message.
Content string
// Delivery address of the user (e.g. phone or email).
Address string
// ExpiresAt is the latest time we can attempt delivery.
ExpiresAt time.Time
// DeliveryAttempts is the total amount of delivery attempts made.
DeliveryAttempts int
}
// MessageRepository represents a local storage for outgoing messages.
// This service will deliver OTP codes via email or SMS if enabled for the user.
type MessageRepository interface {
// Publish prepares a message for a user. Behind the scenes we write the
// message into a channel to be processed by a consumer.
Publish(ctx context.Context, msg *Message) error
// Recent retrieves a list of messages to be delivered.
Recent(ctx context.Context) (<-chan *Message, <-chan error)
}
// LoginHistoryRepository represents a local storage for LoginHistory.
type LoginHistoryRepository interface {
// ByTokenID retrieves a LoginHistory record by a JWT token ID.
ByTokenID(ctx context.Context, tokenID string) (*LoginHistory, error)
// ByUserID retrieves recent LoginHistory associated with a User's ID.
// It supports pagination through a limit or offset value.
ByUserID(ctx context.Context, userID string, limit, offset int) ([]*LoginHistory, error)
// Create creates a new LoginHistory.
Create(ctx context.Context, login *LoginHistory) error
// GetForUpdate retrieves a LoginHistory by TokenID for updating.
GetForUpdate(ctx context.Context, tokenID string) (*LoginHistory, error)
// Update updates a LoginHistory.
Update(ctx context.Context, login *LoginHistory) error
}
// DeviceRepository represents a local storage for Device.
type DeviceRepository interface {
// ByID returns a Device by it's ID.
ByID(ctx context.Context, deviceID string) (*Device, error)
// ByClientID retrieves a Device associated with a User
// by a ClientID.
ByClientID(ctx context.Context, userID string, clientID []byte) (*Device, error)
// ByUserID retreives all Devices associated with a User.
ByUserID(ctx context.Context, userID string) ([]*Device, error)
// Create creates a new Device record.
Create(ctx context.Context, device *Device) error
// GetForUpdate retrieves a Device by ID for updating.
GetForUpdate(ctx context.Context, deviceID string) (*Device, error)
// Update updates a Device.
Update(ctx context.Context, device *Device) error
// Removes a Devie associated with a User.
Remove(ctx context.Context, deviceID, userID string) error
}
// UserRepository represents a local storage for User.
type UserRepository interface {
// ByIdentity retrieves a User by some whitelisted identity
// value such as email, phone, username, ID.
ByIdentity(ctx context.Context, attribute, value string) (*User, error)
// GetForUpdate retrieves a User by ID for updating.
GetForUpdate(ctx context.Context, userID string) (*User, error)
// Create creates a new User Record.
Create(ctx context.Context, u *User) error
// ReCreate updates an existing, unverified User record,
// to treat the entry as a new unverified User registration.
// User's are considered unverified until completing OTP
// verification to prove ownership of a phone or email address.
ReCreate(ctx context.Context, u *User) error
// Update updates a User.
Update(ctx context.Context, u *User) error
// DisableOTP disables an OTP method for a User.
DisableOTP(ctx context.Context, userID string, method DeliveryMethod) (*User, error)
// RemoveDeliveryMethod removes a phone or email from a User.
RemoveDeliveryMethod(ctx context.Context, userID string, method DeliveryMethod) (*User, error)
}
// RepositoryManager manages repositories stored in storages
// with atomic properties.
type RepositoryManager interface {
// NewWithTransaction returns a new manager to with a transaction
// enabled.
NewWithTransaction(ctx context.Context) (RepositoryManager, error)
// WithAtomic performs an operation inside of a transaction.
// On success, it will return an entity.
WithAtomic(operation func() (interface{}, error)) (interface{}, error)
// LoginHistory returns a LoginHistoryRepository.
LoginHistory() LoginHistoryRepository
// Device returns a DeviceRepository.
Device() DeviceRepository
// User returns a UserRepository.
User() UserRepository
}
// TokenConfiguration provides configurable settings for a JWT token.
type TokenConfiguration struct {
DeliveryMethod DeliveryMethod
DeliveryAddress string
RefreshableToken *Token
}
// TokenOption configures a new JWT token.
type TokenOption func(*TokenConfiguration)
// TokenService represents a service to manage JWT tokens.
type TokenService interface {
// Create creates a new JWT token with optional configuration settings.
Create(ctx context.Context, user *User, state TokenState, options ...TokenOption) (*Token, error)
// Sign creates a signed JWT token string from a token struct.
Sign(ctx context.Context, token *Token) (string, error)
// Validate checks that a JWT token is signed by us, unexpired,
// unrevoked, and from the a valid client. On success it will return the unpacked
// Token struct.
Validate(ctx context.Context, signedToken string, clientID string) (*Token, error)
// Revoke Revokes a token by it's ID.
Revoke(ctx context.Context, tokenID string) error
// Cookies returns secure cookies to accompany a token.
Cookies(ctx context.Context, token *Token) []*http.Cookie
// Refreshable checks if a provided token can be refreshed.
Refreshable(ctx context.Context, token *Token, refreshToken string) error
// RefreshableTill returns the latest validity time for a token's accompanying refresh token.
RefreshableTill(ctx context.Context, token *Token, refreshToken string) time.Time
}
// WebAuthnService manages the protocol for WebAuthn authentication.
type WebAuthnService interface {
// BeginSignUp attempts to register a new WebAuthn device.
BeginSignUp(ctx context.Context, user *User) ([]byte, error)
// FinishSignUp confirms a challenge signature for registration.
FinishSignUp(ctx context.Context, user *User, r *http.Request) (*Device, error)
// BeginLogin starts the authentication flow to validate a device.
BeginLogin(ctx context.Context, user *User) ([]byte, error)
// FinishLogin confirms that a device successfully signed a challenge.
FinishLogin(ctx context.Context, user *User, r *http.Request) error
}
// PasswordService manages the protocol for password management and validation.
type PasswordService interface {
// Hash hashes a password for storage.
Hash(password string) ([]byte, error)
// Validate determines if a submitted pasword is valid for a stored
// password hash.
Validate(user *User, password string) error
// OKForUser checks if a password may be used for a user.
OKForUser(password string) error
}
// OTPService manages the protocol for SMS/Email 2FA codes and TOTP codes.
type OTPService interface {
// TOTPQRString returns a URL string used for TOTP code generation.
TOTPQRString(u *User) (string, error)
// TOTPSecret creates a TOTP secret for code generation.
TOTPSecret(u *User) (string, error)
// OTPCode creates a random OTP code and hash.
OTPCode(address string, method DeliveryMethod) (code, hash string, err error)
// ValidateOTP checks if a User email/sms delivered OTP code is valid.
ValidateOTP(code, hash string) error
// ValidateTOTP checks if a User TOTP code is valid.
ValidateTOTP(ctx context.Context, user *User, code string) error
}
// MessagingService sends messages through email or SMS.
type MessagingService interface {
// Send sends a message to a user.
Send(ctx context.Context, msg *Message) error
}
// LoginAPI provides HTTP handlers for user authentication.
type LoginAPI interface {
// Login is the initial login step to identify a User.
// On success it will return a JWT token in a pre_authorized state.
Login(w http.ResponseWriter, r *http.Request) (interface{}, error)
// DeviceChallenge retrieves a device challenge to be signed by the client.
DeviceChallenge(w http.ResponseWriter, r *http.Request) (interface{}, error)
// VerifyDevice verifies a User's authenticity by verifying
// a signing device owned by the user. On success it will return
// a JWT token in an authorized state.
VerifyDevice(w http.ResponseWriter, r *http.Request) (interface{}, error)
// VerifyCode verifies a User's authenticity by verifying
// a TOTP or randomly generated code delivered by SMS/Email.
// On success it will return a JWT token in an auhtorized state.
VerifyCode(w http.ResponseWriter, r *http.Request) (interface{}, error)
}
// SignUpAPI provides HTTP handlers for user registration.
type SignUpAPI interface {
// SignUp is the initial registration step to identify a User.
// On success it will return a JWT token in an unverified state.
SignUp(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Verify is the final registration step to validate a new
// User's authenticity. On success it will return a JWT
// token in an authozied state.
Verify(w http.ResponseWriter, r *http.Request) (interface{}, error)
}
// ContactAPI provides HTTP handlers to manage email/SMS configuration for a User.
type ContactAPI interface {
// CheckAddress requests an OTP code to be delivered to a user through
// an email address or phone number.
CheckAddress(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Disable disables a verified email or phone number on a user's profile
// from receiving OTP codes.
Disable(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Verify verifies an OTP code sent to an email or phone number. If
// delivery address is new to the user, we set it on the profile.
// By default, verified addresses are enabled for future OTP
// code delivery unless the client explicitly says otherwise.
Verify(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Remove removes a verified email or phone number from the User's profile.
// Removed email addresses and phone numbers cannot be re-added without
// requesting a new OTP.
Remove(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Send allows a user to request an OTP code to be delivered to them through
// a pre-approved channel.
Send(w http.ResponseWriter, r *http.Request) (interface{}, error)
}
// TOTPAPI provides HTTP handlers to manage TOTP configuration for a User.
type TOTPAPI interface {
// Secret requests a TOTP secret to allow a user to generate TOTP codes
// via a supported application (e.g. Google Authenticator).
Secret(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Verify enables TOTP as an TFA option for a user by accepting a
// TOTP code and validating it against the TFA secret configured
// on the user.
Verify(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Remove disables TOTP as an TFA option for a user by accepting a
// TOTP code and validating it against the TFA secret configured
// on the user.
Remove(w http.ResponseWriter, r *http.Request) (interface{}, error)
}
// DeviceAPI provides HTTP handlers to manage Devices for a User.
type DeviceAPI interface {
// Verify validates ownership of a new Device for a User.
Verify(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Create is an initial request to add a new Device for a User.
Create(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Remove removes a Device associated with a User.
Remove(w http.ResponseWriter, r *http.Request) (interface{}, error)
// List returns all active devices for a User.
List(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Rename renames a Device for a User.
Rename(w http.ResponseWriter, r *http.Request) (interface{}, error)
}
// TokenAPI provides HTTP handlers to manage a User's tokens.
type TokenAPI interface {
// Revoke revokes a User's token for a logged in session.
Revoke(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Verify verifies a User's token is authenticated and
// valid. A valid token is not expired and not revoked.
Verify(w http.ResponseWriter, r *http.Request) (interface{}, error)
// Refresh refreshes an expired token with a new expiry time.
// Refreshed tokens share a token's original ID and client ID.
Refresh(w http.ResponseWriter, r *http.Request) (interface{}, error)
}
// UserAPI proivdes HTTP handlers to configure a registered User's
// account.
type UserAPI interface {
// UpdatePassword change's a User's password.
UpdatePassword(w http.ResponseWriter, r *http.Request) (interface{}, error)
}
// LoginHistoryAPI provides HTTP handlers to expose LoginHistory.
type LoginHistoryAPI interface {
// List returns LoginHistory records
List(w http.ResponseWriter, r *http.Request) (interface{}, error)
}
// Emailer exposes an email API.
type Emailer interface {
// Email sends an email to an email address
Email(ctx context.Context, email, subject, message string) error
}
// SMSer exposes an SMS API.
type SMSer interface {
// SMS sends an SMS to an phone number
SMS(ctx context.Context, phoneNumber string, message string) error
}