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

barerepo / server / internal/httpd/accounts.go
669 lines · 18kb · 133728eaa05486504991b230f9d4c7e986b7defc
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package httpd
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"errors"
133728e barerepo 1mo
6
"fmt"
133728e barerepo 1mo
7
"net"
133728e barerepo 1mo
8
"net/http"
133728e barerepo 1mo
9
"os"
133728e barerepo 1mo
10
"path/filepath"
133728e barerepo 1mo
11
"strconv"
133728e barerepo 1mo
12
"strings"
133728e barerepo 1mo
13
"time"
133728e barerepo 1mo
14
133728e barerepo 1mo
15
"github.com/barerepo/server/internal/auth"
133728e barerepo 1mo
16
"github.com/barerepo/server/internal/gitread"
133728e barerepo 1mo
17
"github.com/barerepo/server/internal/store"
133728e barerepo 1mo
18
"github.com/barerepo/server/internal/token"
133728e barerepo 1mo
19
)
133728e barerepo 1mo
20
133728e barerepo 1mo
21
// cookieName holds an opaque token in a server table, not a JWT, because revocation matters. 10.
133728e barerepo 1mo
22
const cookieName = "barerepo_session"
133728e barerepo 1mo
23
133728e barerepo 1mo
24
// viewer is who is asking, or "" for nobody, and every private page goes through here.
133728e barerepo 1mo
25
func (s *Server) viewer(r *http.Request) string {
133728e barerepo 1mo
26
c, err := r.Cookie(cookieName)
133728e barerepo 1mo
27
if err != nil || c.Value == "" {
133728e barerepo 1mo
28
return ""
133728e barerepo 1mo
29
}
133728e barerepo 1mo
30
account, err := s.DB.SessionAccount(r.Context(), c.Value)
133728e barerepo 1mo
31
if err != nil {
133728e barerepo 1mo
32
return ""
133728e barerepo 1mo
33
}
133728e barerepo 1mo
34
return account
133728e barerepo 1mo
35
}
133728e barerepo 1mo
36
133728e barerepo 1mo
37
func (s *Server) setSession(w http.ResponseWriter, r *http.Request, value string) {
133728e barerepo 1mo
38
http.SetCookie(w, &http.Cookie{
133728e barerepo 1mo
39
Name: cookieName,
133728e barerepo 1mo
40
Value: value,
133728e barerepo 1mo
41
Path: "/",
133728e barerepo 1mo
42
HttpOnly: true,
133728e barerepo 1mo
43
// Secure over https, because a plain-http install on localhost still has to sign in.
133728e barerepo 1mo
44
Secure: strings.HasPrefix(s.Cfg.Server.ExternalURL, "https://"),
133728e barerepo 1mo
45
SameSite: http.SameSiteLaxMode,
133728e barerepo 1mo
46
MaxAge: int(store.SessionLife.Seconds()),
133728e barerepo 1mo
47
})
133728e barerepo 1mo
48
}
133728e barerepo 1mo
49
133728e barerepo 1mo
50
func (s *Server) clearSession(w http.ResponseWriter) {
133728e barerepo 1mo
51
http.SetCookie(w, &http.Cookie{
133728e barerepo 1mo
52
Name: cookieName, Value: "", Path: "/", HttpOnly: true, MaxAge: -1,
133728e barerepo 1mo
53
})
133728e barerepo 1mo
54
}
133728e barerepo 1mo
55
133728e barerepo 1mo
56
// signupPage is the form and its errors, with Nonce set once a signature is what is missing.
133728e barerepo 1mo
57
type signupPage struct {
133728e barerepo 1mo
58
chrome
133728e barerepo 1mo
59
Name string
133728e barerepo 1mo
60
PubKey string
133728e barerepo 1mo
61
Nonce string
133728e barerepo 1mo
62
Error string
133728e barerepo 1mo
63
}
133728e barerepo 1mo
64
133728e barerepo 1mo
65
func (s *Server) serveSignup(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
66
page := signupPage{chrome: newChrome("barerepo · signup",
133728e barerepo 1mo
67
"Signup page where an ssh public key is the only credential collected.")}
133728e barerepo 1mo
68
133728e barerepo 1mo
69
if isGet(r) {
133728e barerepo 1mo
70
s.render(w, r, "signup", page)
133728e barerepo 1mo
71
return
133728e barerepo 1mo
72
}
133728e barerepo 1mo
73
if err := r.ParseForm(); err != nil {
133728e barerepo 1mo
74
http.Error(w, "malformed form", http.StatusBadRequest)
133728e barerepo 1mo
75
return
133728e barerepo 1mo
76
}
133728e barerepo 1mo
77
page.Name = strings.TrimSpace(r.FormValue("name"))
133728e barerepo 1mo
78
page.PubKey = strings.TrimSpace(r.FormValue("pubkey"))
133728e barerepo 1mo
79
fail := func(msg string) {
133728e barerepo 1mo
80
// A bad key is not echoed back, so a private key pasted by mistake leaves the screen.
133728e barerepo 1mo
81
if strings.Contains(msg, "private key") {
133728e barerepo 1mo
82
page.PubKey = ""
133728e barerepo 1mo
83
}
133728e barerepo 1mo
84
page.Error = msg
133728e barerepo 1mo
85
w.WriteHeader(http.StatusBadRequest)
133728e barerepo 1mo
86
s.render(w, r, "signup", page)
133728e barerepo 1mo
87
}
133728e barerepo 1mo
88
133728e barerepo 1mo
89
// Second step: the signature over the nonce the first step handed out.
133728e barerepo 1mo
90
if nonce := strings.TrimSpace(r.FormValue("nonce")); nonce != "" {
133728e barerepo 1mo
91
s.finishSignup(w, r, nonce, strings.TrimSpace(r.FormValue("signature")))
133728e barerepo 1mo
92
return
133728e barerepo 1mo
93
}
133728e barerepo 1mo
94
133728e barerepo 1mo
95
if !s.signupAllowed(r) {
133728e barerepo 1mo
96
page.Error = "too many signups from this address in the last hour. try later."
133728e barerepo 1mo
97
w.WriteHeader(http.StatusTooManyRequests)
133728e barerepo 1mo
98
s.render(w, r, "signup", page)
133728e barerepo 1mo
99
return
133728e barerepo 1mo
100
}
133728e barerepo 1mo
101
if err := s.DB.CheckNewAccount(r.Context(), page.Name, page.PubKey); err != nil {
133728e barerepo 1mo
102
fail(err.Error())
133728e barerepo 1mo
103
return
133728e barerepo 1mo
104
}
133728e barerepo 1mo
105
133728e barerepo 1mo
106
// Proof of possession, or anyone can claim an account with a public key they found.
133728e barerepo 1mo
107
nonce, err := auth.NewNonce()
133728e barerepo 1mo
108
if err != nil {
133728e barerepo 1mo
109
s.oops(w, r, err)
133728e barerepo 1mo
110
return
133728e barerepo 1mo
111
}
133728e barerepo 1mo
112
if err := s.DB.NewSignupChallenge(r.Context(), nonce, page.Name, page.PubKey); err != nil {
133728e barerepo 1mo
113
s.oops(w, r, err)
133728e barerepo 1mo
114
return
133728e barerepo 1mo
115
}
133728e barerepo 1mo
116
page.Nonce = nonce
133728e barerepo 1mo
117
s.render(w, r, "signup", page)
133728e barerepo 1mo
118
}
133728e barerepo 1mo
119
133728e barerepo 1mo
120
// signinPage carries either the name form or the nonce form.
133728e barerepo 1mo
121
type signinPage struct {
133728e barerepo 1mo
122
chrome
133728e barerepo 1mo
123
Name string
133728e barerepo 1mo
124
Nonce string
133728e barerepo 1mo
125
Error string
133728e barerepo 1mo
126
// Unknown means no such account, so the page points at signup instead of leaving a guess.
133728e barerepo 1mo
127
Unknown bool
133728e barerepo 1mo
128
}
133728e barerepo 1mo
129
133728e barerepo 1mo
130
func (s *Server) serveSignin(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
131
s.render(w, r, "signin", signinPage{chrome: signinChrome()})
133728e barerepo 1mo
132
}
133728e barerepo 1mo
133
133728e barerepo 1mo
134
func signinChrome() chrome {
133728e barerepo 1mo
135
return newChrome("barerepo · sign in",
133728e barerepo 1mo
136
"Sign in page where the server challenges an ssh key instead of asking for a password.")
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
133728e barerepo 1mo
139
// serveChallenge is step 2 of chapter 10: generate a nonce and show it.
133728e barerepo 1mo
140
func (s *Server) serveChallenge(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
141
name := strings.TrimSpace(r.FormValue("name"))
133728e barerepo 1mo
142
page := signinPage{chrome: signinChrome(), Name: name}
133728e barerepo 1mo
143
133728e barerepo 1mo
144
// A name is a public URL namespace, so hiding it here only wastes the user's signature.
133728e barerepo 1mo
145
if _, err := s.DB.Account(r.Context(), name); err != nil {
133728e barerepo 1mo
146
page.Error = "there is no account named " + name + "."
133728e barerepo 1mo
147
page.Unknown = true
133728e barerepo 1mo
148
w.WriteHeader(http.StatusNotFound)
133728e barerepo 1mo
149
s.render(w, r, "signin", page)
133728e barerepo 1mo
150
return
133728e barerepo 1mo
151
}
133728e barerepo 1mo
152
nonce, err := auth.NewNonce()
133728e barerepo 1mo
153
if err != nil {
133728e barerepo 1mo
154
s.oops(w, r, err)
133728e barerepo 1mo
155
return
133728e barerepo 1mo
156
}
133728e barerepo 1mo
157
if err := s.DB.NewChallenge(r.Context(), name, nonce); err != nil {
133728e barerepo 1mo
158
s.oops(w, r, err)
133728e barerepo 1mo
159
return
133728e barerepo 1mo
160
}
133728e barerepo 1mo
161
if wantsText(r) {
133728e barerepo 1mo
162
plainText(w, nonce)
133728e barerepo 1mo
163
return
133728e barerepo 1mo
164
}
133728e barerepo 1mo
165
page.Nonce = nonce
133728e barerepo 1mo
166
s.render(w, r, "signin", page)
133728e barerepo 1mo
167
}
133728e barerepo 1mo
168
133728e barerepo 1mo
169
// wantsText marks the caller as a terminal, which reads a nonce and never a page.
133728e barerepo 1mo
170
func wantsText(r *http.Request) bool {
133728e barerepo 1mo
171
return strings.Contains(r.Header.Get("Accept"), "text/plain")
133728e barerepo 1mo
172
}
133728e barerepo 1mo
173
133728e barerepo 1mo
174
func plainText(w http.ResponseWriter, lines ...string) {
133728e barerepo 1mo
175
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
133728e barerepo 1mo
176
for _, line := range lines {
133728e barerepo 1mo
177
fmt.Fprintln(w, line)
133728e barerepo 1mo
178
}
133728e barerepo 1mo
179
}
133728e barerepo 1mo
180
133728e barerepo 1mo
181
// serveVerify is steps 4 to 6: check the signature, issue a session.
133728e barerepo 1mo
182
func (s *Server) serveVerify(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
183
nonce := strings.TrimSpace(r.FormValue("nonce"))
133728e barerepo 1mo
184
signature := strings.TrimSpace(r.FormValue("signature"))
133728e barerepo 1mo
185
133728e barerepo 1mo
186
// The name rides along, because a spent nonce is not the user's mistake to retype for.
133728e barerepo 1mo
187
name := strings.TrimSpace(r.FormValue("name"))
133728e barerepo 1mo
188
fail := func(msg string) {
133728e barerepo 1mo
189
w.WriteHeader(http.StatusUnauthorized)
133728e barerepo 1mo
190
if wantsText(r) {
133728e barerepo 1mo
191
plainText(w, msg)
133728e barerepo 1mo
192
return
133728e barerepo 1mo
193
}
133728e barerepo 1mo
194
s.render(w, r, "signin", signinPage{chrome: signinChrome(), Name: name, Error: msg})
133728e barerepo 1mo
195
}
133728e barerepo 1mo
196
133728e barerepo 1mo
197
// Spent whether or not the signature is good, so no answer is retried on one challenge.
133728e barerepo 1mo
198
c, err := s.DB.TakeChallenge(r.Context(), nonce)
133728e barerepo 1mo
199
if err != nil {
133728e barerepo 1mo
200
fail("that challenge is used up. press send challenge for a new one.")
133728e barerepo 1mo
201
return
133728e barerepo 1mo
202
}
133728e barerepo 1mo
203
keys, err := s.DB.Keys(r.Context(), c.Account)
133728e barerepo 1mo
204
if err != nil {
133728e barerepo 1mo
205
s.oops(w, r, err)
133728e barerepo 1mo
206
return
133728e barerepo 1mo
207
}
133728e barerepo 1mo
208
used, err := auth.Verify(r.Context(), c.Account, nonce, signature, keys)
133728e barerepo 1mo
209
if err != nil {
133728e barerepo 1mo
210
fail(err.Error())
133728e barerepo 1mo
211
return
133728e barerepo 1mo
212
}
133728e barerepo 1mo
213
if err := s.DB.TouchKey(r.Context(), used.ID); err != nil {
133728e barerepo 1mo
214
s.log(r, err)
133728e barerepo 1mo
215
}
133728e barerepo 1mo
216
133728e barerepo 1mo
217
if wantsText(r) {
133728e barerepo 1mo
218
code, err := s.DB.NewClaim(r.Context(), c.Account)
133728e barerepo 1mo
219
if err != nil {
133728e barerepo 1mo
220
s.oops(w, r, err)
133728e barerepo 1mo
221
return
221 of 669 lines · the whole file
history · rawbarerepo 0.1.0