This repository has been archived by the owner on Mar 1, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
email.go
162 lines (142 loc) · 4.96 KB
/
email.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
package oauth2
import (
"context"
"encoding/base64"
"errors"
uuid "github.com/hashicorp/go-uuid"
yall "yall.in"
"lockbox.dev/accounts"
"lockbox.dev/grants"
)
type emailer interface {
SendMail(ctx context.Context, email, code string) error
}
// MemoryEmailer is an in-memory implementation of the `emailer` interface that
// simply tracks the last code sent and the email it was "sent" to.
//
// Its intended use is in testing.
type MemoryEmailer struct {
LastCode string
LastEmail string
}
// SendMail records the passed code and email in the `MemoryEmailer` for later
// retrieval. It never returns an error.
func (m *MemoryEmailer) SendMail(_ context.Context, email, code string) error {
m.LastCode = code
m.LastEmail = email
return nil
}
// emailGranter fills the granter interface for handling a Grant passed
// by its ID. The expectation is that the user will request a Grant be
// emailed to them as a link, they'll click the link, and end that link
// will exchange the Grant for a session.
type emailGranter struct {
code string // the code presented as proof of grant
clientID string // the clientID using the Grant
grants grants.Storer
// populated in Validate
grant grants.Grant // the Grant being traded for a session
}
// Validate retrieves the Grant and stores it for later reference,
// ensuring that it is valid and authorized.
func (g *emailGranter) Validate(ctx context.Context) APIError {
log := yall.FromContext(ctx)
log = log.WithField("passed_code", g.code)
grant, err := g.grants.GetGrantBySource(ctx, "email", g.code)
if err != nil {
if errors.Is(err, grants.ErrGrantNotFound) {
log.Debug("grant not found")
return invalidRequestError
}
log.WithError(err).Error("error retrieving grant")
return serverError
}
g.grant = grant
return APIError{}
}
// ProfileID returns the ID of the profile the grant is for. It must be called
// after Validate.
func (g *emailGranter) ProfileID(_ context.Context) string {
return g.grant.ProfileID
}
// AccountID returns the ID of the account the grant is for. It must be called
// after Validate.
func (g *emailGranter) AccountID(_ context.Context) string {
return g.grant.AccountID
}
// Grant returns the grant we retrieved in Validate.
func (g *emailGranter) Grant(_ context.Context, _ []string) grants.Grant {
return g.grant
}
// Granted does nothing, the Grant will automatically be marked as used
// when it is exchanged for a session.
func (*emailGranter) Granted(_ context.Context) error {
return nil
}
// Redirects returns false, indicating we want to use the JSON request/response
// flow, not the URL querystring redirect flow.
func (*emailGranter) Redirects() bool {
return false
}
// CreatesGrantsInline returns false, indicating we don't want the access token
// exchange to generate a new Grant, we just want to use a previously generated
// Grant.
func (*emailGranter) CreatesGrantsInline() bool {
return false
}
type emailGrantCreator struct {
email string
client string
accounts accounts.Storer
emailer emailer
}
// GetAccount returns the `accounts.Account` associated with `g.email`. The
// APIError returned is intended to be rendered, not inspected.
func (g *emailGrantCreator) GetAccount(ctx context.Context) (accounts.Account, APIError) {
log := yall.FromContext(ctx)
account, err := g.accounts.Get(ctx, g.email)
if err != nil {
if errors.Is(err, accounts.ErrAccountNotFound) {
log.WithField("email", g.email).Debug("account not found")
return accounts.Account{}, invalidRequestError
}
log.WithError(err).Error("error retrieving account")
return accounts.Account{}, serverError
}
return account, APIError{}
}
// FillGrant creates a new `grants.Grant` with a `SourceType` of "email". The
// `SourceID` is a randomly generated URL-safe-base64-encoded string.
func (g *emailGrantCreator) FillGrant(ctx context.Context, account accounts.Account, scopes []string) (grants.Grant, APIError) {
log := yall.FromContext(ctx)
numCodeBytes := 32
codeBytes, err := uuid.GenerateRandomBytes(numCodeBytes)
if err != nil {
log.WithError(err).Error("error generating random bytes")
return grants.Grant{}, serverError
}
return grants.Grant{
SourceType: "email",
SourceID: base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(codeBytes),
Scopes: scopes,
AccountID: account.ID,
ProfileID: account.ProfileID,
ClientID: g.client,
}, APIError{}
}
// ResponseMethod reports that emailGrantCreator responses should be sent out
// of band, not through redirect or returning them in the response.
func (*emailGrantCreator) ResponseMethod() responseMethod {
return rmOOB
}
// HandleOOBGrant sends the email containing the `grants.Grant` code that can
// be exchanged at the token endpoint for an access token.
func (g *emailGrantCreator) HandleOOBGrant(ctx context.Context, grant grants.Grant) error {
log := yall.FromContext(ctx)
err := g.emailer.SendMail(ctx, g.email, grant.SourceID)
if err != nil {
log.WithError(err).Debug("Error sending mail")
return err
}
return nil
}