File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/store/sessions.go
187 lines · 5.7kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package store
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"database/sql"
133728e barerepo 1mo
6
"errors"
133728e barerepo 1mo
7
"time"
133728e barerepo 1mo
8
133728e barerepo 1mo
9
"github.com/barerepo/server/internal/token"
133728e barerepo 1mo
10
)
133728e barerepo 1mo
11
133728e barerepo 1mo
12
// SessionLife bounds an unused session, kept in a table because revocation beats statelessness.
133728e barerepo 1mo
13
const SessionLife = 30 * 24 * time.Hour
133728e barerepo 1mo
14
133728e barerepo 1mo
15
// NewSession issues a session for an account and returns the cookie value.
133728e barerepo 1mo
16
func (db *DB) NewSession(ctx context.Context, account string) (string, error) {
133728e barerepo 1mo
17
tok, hash, err := token.New(token.Session)
133728e barerepo 1mo
18
if err != nil {
133728e barerepo 1mo
19
return "", err
133728e barerepo 1mo
20
}
133728e barerepo 1mo
21
now := now()
133728e barerepo 1mo
22
_, err = db.ExecContext(ctx,
133728e barerepo 1mo
23
`INSERT INTO tokens (kind, hash, account, scope, label, created_at, expires_at)
133728e barerepo 1mo
24
VALUES (?, ?, ?, '', '', ?, ?)`,
133728e barerepo 1mo
25
string(token.Session), hash, account, now.Unix(), now.Add(SessionLife).Unix())
133728e barerepo 1mo
26
if err != nil {
133728e barerepo 1mo
27
return "", err
133728e barerepo 1mo
28
}
133728e barerepo 1mo
29
return tok, nil
133728e barerepo 1mo
30
}
133728e barerepo 1mo
31
133728e barerepo 1mo
32
// SessionAccount returns who a session cookie belongs to, or ErrNotFound.
133728e barerepo 1mo
33
func (db *DB) SessionAccount(ctx context.Context, cookie string) (string, error) {
133728e barerepo 1mo
34
t, err := db.AccountForToken(ctx, token.Session, cookie)
133728e barerepo 1mo
35
if err != nil {
133728e barerepo 1mo
36
return "", err
133728e barerepo 1mo
37
}
133728e barerepo 1mo
38
return t.Account, nil
133728e barerepo 1mo
39
}
133728e barerepo 1mo
40
133728e barerepo 1mo
41
// EndSession revokes one session. Signing out has to actually sign out.
133728e barerepo 1mo
42
func (db *DB) EndSession(ctx context.Context, cookie string) error {
133728e barerepo 1mo
43
_, err := db.ExecContext(ctx, `DELETE FROM tokens WHERE kind = ? AND hash = ?`,
133728e barerepo 1mo
44
string(token.Session), token.Hash(cookie))
133728e barerepo 1mo
45
return err
133728e barerepo 1mo
46
}
133728e barerepo 1mo
47
133728e barerepo 1mo
48
// ClaimLife is short, because a claim is carried from one terminal to one browser and no further.
133728e barerepo 1mo
49
const ClaimLife = 10 * time.Minute
133728e barerepo 1mo
50
133728e barerepo 1mo
51
// NewClaim issues the one-use code that trades a signature made in a terminal for a session.
133728e barerepo 1mo
52
func (db *DB) NewClaim(ctx context.Context, account string) (string, error) {
133728e barerepo 1mo
53
tok, hash, err := token.New(token.Claim)
133728e barerepo 1mo
54
if err != nil {
133728e barerepo 1mo
55
return "", err
133728e barerepo 1mo
56
}
133728e barerepo 1mo
57
now := now()
133728e barerepo 1mo
58
_, err = db.ExecContext(ctx,
133728e barerepo 1mo
59
`INSERT INTO tokens (kind, hash, account, scope, label, created_at, expires_at)
133728e barerepo 1mo
60
VALUES (?, ?, ?, '', '', ?, ?)`,
133728e barerepo 1mo
61
string(token.Claim), hash, account, now.Unix(), now.Add(ClaimLife).Unix())
133728e barerepo 1mo
62
if err != nil {
133728e barerepo 1mo
63
return "", err
133728e barerepo 1mo
64
}
133728e barerepo 1mo
65
return tok, nil
133728e barerepo 1mo
66
}
133728e barerepo 1mo
67
133728e barerepo 1mo
68
// TakeClaim spends a claim and names its account, so a pasted link works once and never again.
133728e barerepo 1mo
69
func (db *DB) TakeClaim(ctx context.Context, code string) (string, error) {
133728e barerepo 1mo
70
t, err := db.AccountForToken(ctx, token.Claim, code)
133728e barerepo 1mo
71
if err != nil {
133728e barerepo 1mo
72
return "", err
133728e barerepo 1mo
73
}
133728e barerepo 1mo
74
if _, err := db.ExecContext(ctx, `DELETE FROM tokens WHERE id = ?`, t.ID); err != nil {
133728e barerepo 1mo
75
return "", err
133728e barerepo 1mo
76
}
133728e barerepo 1mo
77
return t.Account, nil
133728e barerepo 1mo
78
}
133728e barerepo 1mo
79
133728e barerepo 1mo
80
// Challenge is chapter 10's nonce, on disk so a restart costs nobody and verifying deletes it.
133728e barerepo 1mo
81
type Challenge struct {
133728e barerepo 1mo
82
Nonce string
133728e barerepo 1mo
83
Account string
133728e barerepo 1mo
84
}
133728e barerepo 1mo
85
133728e barerepo 1mo
86
// ChallengeLife is ten minutes, because signing means leaving for a terminal and coming back.
133728e barerepo 1mo
87
const ChallengeLife = 10 * time.Minute
133728e barerepo 1mo
88
133728e barerepo 1mo
89
// NewChallenge stores a nonce for an account.
133728e barerepo 1mo
90
func (db *DB) NewChallenge(ctx context.Context, account, nonce string) error {
133728e barerepo 1mo
91
now := now()
133728e barerepo 1mo
92
_, err := db.ExecContext(ctx,
133728e barerepo 1mo
93
`INSERT INTO challenges (nonce, account, expires_at) VALUES (?, ?, ?)`,
133728e barerepo 1mo
94
nonce, account, now.Add(ChallengeLife).Unix())
133728e barerepo 1mo
95
return err
133728e barerepo 1mo
96
}
133728e barerepo 1mo
97
133728e barerepo 1mo
98
// TakeChallenge consumes a nonce once, whether or not its signature was any good.
133728e barerepo 1mo
99
func (db *DB) TakeChallenge(ctx context.Context, nonce string) (*Challenge, error) {
133728e barerepo 1mo
100
var c Challenge
133728e barerepo 1mo
101
var expires int64
133728e barerepo 1mo
102
err := db.QueryRowContext(ctx,
133728e barerepo 1mo
103
`SELECT nonce, account, expires_at FROM challenges WHERE nonce = ?`, nonce).
133728e barerepo 1mo
104
Scan(&c.Nonce, &c.Account, &expires)
133728e barerepo 1mo
105
if errors.Is(err, sql.ErrNoRows) {
133728e barerepo 1mo
106
return nil, ErrNotFound
133728e barerepo 1mo
107
}
133728e barerepo 1mo
108
if err != nil {
133728e barerepo 1mo
109
return nil, err
133728e barerepo 1mo
110
}
133728e barerepo 1mo
111
// The delete is what spends it, so the caller that removed the row is the one that may use it.
133728e barerepo 1mo
112
res, err := db.ExecContext(ctx, `DELETE FROM challenges WHERE nonce = ?`, nonce)
133728e barerepo 1mo
113
if err != nil {
133728e barerepo 1mo
114
return nil, err
133728e barerepo 1mo
115
}
133728e barerepo 1mo
116
spent, err := res.RowsAffected()
133728e barerepo 1mo
117
if err != nil {
133728e barerepo 1mo
118
return nil, err
133728e barerepo 1mo
119
}
133728e barerepo 1mo
120
if spent == 0 {
133728e barerepo 1mo
121
return nil, ErrNotFound
133728e barerepo 1mo
122
}
133728e barerepo 1mo
123
if now().Unix() > expires {
133728e barerepo 1mo
124
return nil, ErrNotFound
133728e barerepo 1mo
125
}
133728e barerepo 1mo
126
return &c, nil
133728e barerepo 1mo
127
}
133728e barerepo 1mo
128
133728e barerepo 1mo
129
// SweepExpired drops spent nonces and dead sessions, which accumulate and are worth nothing.
133728e barerepo 1mo
130
func (db *DB) SweepExpired(ctx context.Context) error {
133728e barerepo 1mo
131
cutoff := now().Unix()
133728e barerepo 1mo
132
if _, err := db.ExecContext(ctx, `DELETE FROM challenges WHERE expires_at < ?`, cutoff); err != nil {
133728e barerepo 1mo
133
return err
133728e barerepo 1mo
134
}
133728e barerepo 1mo
135
if _, err := db.ExecContext(ctx, `DELETE FROM signup_challenges WHERE expires_at < ?`, cutoff); err != nil {
133728e barerepo 1mo
136
return err
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
_, err := db.ExecContext(ctx,
133728e barerepo 1mo
139
`DELETE FROM tokens WHERE expires_at IS NOT NULL AND expires_at < ?`, cutoff)
133728e barerepo 1mo
140
return err
133728e barerepo 1mo
141
}
133728e barerepo 1mo
142
133728e barerepo 1mo
143
// SignupChallenge holds a name and key while the server waits for proof of the private half.
133728e barerepo 1mo
144
type SignupChallenge struct {
133728e barerepo 1mo
145
Nonce string
133728e barerepo 1mo
146
Name string
133728e barerepo 1mo
147
PubKey string
133728e barerepo 1mo
148
}
133728e barerepo 1mo
149
133728e barerepo 1mo
150
// NewSignupChallenge parks a signup until its signature arrives.
133728e barerepo 1mo
151
func (db *DB) NewSignupChallenge(ctx context.Context, nonce, name, pubkey string) error {
133728e barerepo 1mo
152
_, err := db.ExecContext(ctx,
133728e barerepo 1mo
153
`INSERT INTO signup_challenges (nonce, name, pubkey, expires_at) VALUES (?, ?, ?, ?)`,
133728e barerepo 1mo
154
nonce, name, pubkey, now().Add(ChallengeLife).Unix())
133728e barerepo 1mo
155
return err
133728e barerepo 1mo
156
}
133728e barerepo 1mo
157
133728e barerepo 1mo
158
// TakeSignupChallenge spends a signup nonce on first use, good signature or not.
133728e barerepo 1mo
159
func (db *DB) TakeSignupChallenge(ctx context.Context, nonce string) (*SignupChallenge, error) {
133728e barerepo 1mo
160
var c SignupChallenge
133728e barerepo 1mo
161
var expires int64
133728e barerepo 1mo
162
err := db.QueryRowContext(ctx,
133728e barerepo 1mo
163
`SELECT nonce, name, pubkey, expires_at FROM signup_challenges WHERE nonce = ?`, nonce).
133728e barerepo 1mo
164
Scan(&c.Nonce, &c.Name, &c.PubKey, &expires)
133728e barerepo 1mo
165
if errors.Is(err, sql.ErrNoRows) {
133728e barerepo 1mo
166
return nil, ErrNotFound
133728e barerepo 1mo
167
}
133728e barerepo 1mo
168
if err != nil {
133728e barerepo 1mo
169
return nil, err
133728e barerepo 1mo
170
}
133728e barerepo 1mo
171
// The delete is what spends it, so two requests racing one nonce make at most one account.
133728e barerepo 1mo
172
res, err := db.ExecContext(ctx, `DELETE FROM signup_challenges WHERE nonce = ?`, nonce)
133728e barerepo 1mo
173
if err != nil {
133728e barerepo 1mo
174
return nil, err
133728e barerepo 1mo
175
}
133728e barerepo 1mo
176
spent, err := res.RowsAffected()
133728e barerepo 1mo
177
if err != nil {
133728e barerepo 1mo
178
return nil, err
133728e barerepo 1mo
179
}
133728e barerepo 1mo
180
if spent == 0 {
133728e barerepo 1mo
181
return nil, ErrNotFound
133728e barerepo 1mo
182
}
133728e barerepo 1mo
183
if now().Unix() > expires {
133728e barerepo 1mo
184
return nil, ErrNotFound
133728e barerepo 1mo
185
}
133728e barerepo 1mo
186
return &c, nil
133728e barerepo 1mo
187
}
history · rawbarerepo 0.1.0