blob: e615ea88005c0483a3743d03b142285d9650170f [file] [log] [blame]
giolekva603e73a2021-10-22 14:46:45 +04001package main
2
3import (
4 "bytes"
giolekvadd750802021-11-07 13:24:21 +04005 "crypto/tls"
giolekva603e73a2021-10-22 14:46:45 +04006 "embed"
7 "encoding/json"
8 "errors"
9 "flag"
10 "fmt"
11 "html/template"
12 "io"
13 "io/ioutil"
14 "log"
15 "net/http"
16 "net/http/cookiejar"
17 "net/url"
18
19 "github.com/gorilla/mux"
20 "github.com/itaysk/regogo"
21)
22
23var port = flag.Int("port", 8080, "Port to listen on")
24var kratos = flag.String("kratos", "https://accounts.lekva.me", "Kratos URL")
giolekva788dc6e2021-10-25 20:40:53 +040025var hydra = flag.String("hydra", "hydra.pcloud", "Hydra admin server address")
giolekvadd750802021-11-07 13:24:21 +040026var emailDomain = flag.String("email-domain", "lekva.me", "Email domain")
giolekva603e73a2021-10-22 14:46:45 +040027
28var ErrNotLoggedIn = errors.New("Not logged in")
29
30//go:embed templates/*
31var tmpls embed.FS
32
giolekva47031752021-11-12 14:34:33 +040033//go:embed static
34var static embed.FS
35
giolekva603e73a2021-10-22 14:46:45 +040036type Templates struct {
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040037 WhoAmI *template.Template
38 Register *template.Template
39 Login *template.Template
40 Consent *template.Template
giolekva603e73a2021-10-22 14:46:45 +040041}
42
43func ParseTemplates(fs embed.FS) (*Templates, error) {
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040044 base, err := template.ParseFS(fs, "templates/base.html")
giolekva788dc6e2021-10-25 20:40:53 +040045 if err != nil {
46 return nil, err
47 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040048 parse := func(path string) (*template.Template, error) {
49 if b, err := base.Clone(); err != nil {
50 return nil, err
51 } else {
52 return b.ParseFS(fs, path)
53 }
54 }
55 whoami, err := parse("templates/whoami.html")
giolekva603e73a2021-10-22 14:46:45 +040056 if err != nil {
57 return nil, err
58 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040059 register, err := parse("templates/register.html")
giolekva603e73a2021-10-22 14:46:45 +040060 if err != nil {
61 return nil, err
62 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040063 login, err := parse("templates/login.html")
giolekva603e73a2021-10-22 14:46:45 +040064 if err != nil {
65 return nil, err
66 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040067 consent, err := parse("templates/consent.html")
68 if err != nil {
69 return nil, err
70 }
71 return &Templates{whoami, register, login, consent}, nil
giolekva603e73a2021-10-22 14:46:45 +040072}
73
74type Server struct {
75 kratos string
giolekva788dc6e2021-10-25 20:40:53 +040076 hydra *HydraClient
giolekva603e73a2021-10-22 14:46:45 +040077 tmpls *Templates
78}
79
giolekva47031752021-11-12 14:34:33 +040080func cacheControlWrapper(h http.Handler) http.Handler {
81 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82 // TODO(giolekva): enable caching
83 // w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
84 h.ServeHTTP(w, r)
85 })
86}
87
giolekva603e73a2021-10-22 14:46:45 +040088func (s *Server) Start(port int) error {
89 r := mux.NewRouter()
90 http.Handle("/", r)
giolekva47031752021-11-12 14:34:33 +040091 var staticFS = http.FS(static)
92 fs := http.FileServer(staticFS)
93 r.PathPrefix("/static/").Handler(cacheControlWrapper(fs))
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040094 r.Path("/register").Methods(http.MethodGet).HandlerFunc(s.registerInitiate)
95 r.Path("/register").Methods(http.MethodPost).HandlerFunc(s.register)
giolekva603e73a2021-10-22 14:46:45 +040096 r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
97 r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
giolekva788dc6e2021-10-25 20:40:53 +040098 r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
99 r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
giolekva603e73a2021-10-22 14:46:45 +0400100 r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
101 r.Path("/").HandlerFunc(s.whoami)
102 fmt.Printf("Starting HTTP server on port: %d\n", port)
103 return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
104}
105
106func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
107 jar, err := cookiejar.New(nil)
108 if err != nil {
109 return "", err
110 }
111 client := &http.Client{
112 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400113 Transport: &http.Transport{
114 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
115 },
giolekva603e73a2021-10-22 14:46:45 +0400116 }
giolekvadd750802021-11-07 13:24:21 +0400117 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400118 if err != nil {
119 return "", err
120 }
121 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400122 resp, err := client.Get(fmt.Sprintf(*kratos+"/self-service/"+flowType+"/flows?id=%s", flow))
giolekva603e73a2021-10-22 14:46:45 +0400123 if err != nil {
124 return "", err
125 }
126 respBody, err := ioutil.ReadAll(resp.Body)
127 if err != nil {
128 return "", err
129 }
130 token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
131 if err != nil {
132 return "", err
133 }
134 return token.String(), nil
135}
136
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400137func (s *Server) registerInitiate(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400138 if err := r.ParseForm(); err != nil {
139 http.Error(w, err.Error(), http.StatusInternalServerError)
140 return
141 }
142 flow, ok := r.Form["flow"]
143 if !ok {
144 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
145 return
146 }
147 csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
148 if err != nil {
149 http.Error(w, err.Error(), http.StatusInternalServerError)
150 return
151 }
giolekva603e73a2021-10-22 14:46:45 +0400152 w.Header().Set("Content-Type", "text/html")
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400153 if err := s.tmpls.Register.Execute(w, csrfToken); err != nil {
giolekva603e73a2021-10-22 14:46:45 +0400154 http.Error(w, err.Error(), http.StatusInternalServerError)
155 return
156 }
157}
158
159type regReq struct {
160 CSRFToken string `json:"csrf_token"`
161 Method string `json:"method"`
162 Password string `json:"password"`
163 Traits regReqTraits `json:"traits"`
164}
165
166type regReqTraits struct {
167 Username string `json:"username"`
168}
169
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400170func (s *Server) register(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400171 if err := r.ParseForm(); err != nil {
172 http.Error(w, err.Error(), http.StatusInternalServerError)
173 return
174 }
175 flow, ok := r.Form["flow"]
176 if !ok {
177 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
178 return
179 }
180 req := regReq{
181 CSRFToken: r.FormValue("csrf_token"),
182 Method: "password",
183 Password: r.FormValue("password"),
184 Traits: regReqTraits{
185 Username: r.FormValue("username"),
186 },
187 }
188 var reqBody bytes.Buffer
189 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
190 http.Error(w, err.Error(), http.StatusInternalServerError)
191 return
192 }
193 if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
194 http.Error(w, err.Error(), http.StatusInternalServerError)
195 return
196 } else {
197 for _, c := range resp.Cookies() {
198 http.SetCookie(w, c)
199 }
200 http.Redirect(w, r, "/", http.StatusSeeOther)
201 }
202}
203
204// Login flow
205
206func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
207 if err := r.ParseForm(); err != nil {
208 http.Error(w, err.Error(), http.StatusInternalServerError)
209 return
210 }
giolekva788dc6e2021-10-25 20:40:53 +0400211 if challenge, ok := r.Form["login_challenge"]; ok {
212 // TODO(giolekva): encrypt
213 http.SetCookie(w, &http.Cookie{
214 Name: "login_challenge",
215 Value: challenge[0],
216 HttpOnly: true,
217 })
218 } else {
219 // http.SetCookie(w, &http.Cookie{
220 // Name: "login_challenge",
221 // Value: "",
222 // Expires: time.Unix(0, 0),
223 // HttpOnly: true,
224 // })
225 }
giolekva603e73a2021-10-22 14:46:45 +0400226 flow, ok := r.Form["flow"]
227 if !ok {
228 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
229 return
230 }
231 csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
232 if err != nil {
233 http.Error(w, err.Error(), http.StatusInternalServerError)
234 return
235 }
giolekva603e73a2021-10-22 14:46:45 +0400236 w.Header().Set("Content-Type", "text/html")
237 if err := s.tmpls.Login.Execute(w, csrfToken); err != nil {
238 http.Error(w, err.Error(), http.StatusInternalServerError)
239 return
240 }
241}
242
243type loginReq struct {
244 CSRFToken string `json:"csrf_token"`
245 Method string `json:"method"`
246 Password string `json:"password"`
247 Username string `json:"password_identifier"`
248}
249
250func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
251 jar, err := cookiejar.New(nil)
252 if err != nil {
253 return nil, err
254 }
255 client := &http.Client{
256 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400257 Transport: &http.Transport{
258 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
259 },
giolekva603e73a2021-10-22 14:46:45 +0400260 }
giolekvadd750802021-11-07 13:24:21 +0400261 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400262 if err != nil {
263 return nil, err
264 }
265 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400266 resp, err := client.Post(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
giolekva603e73a2021-10-22 14:46:45 +0400267 if err != nil {
268 return nil, err
269 }
270 return resp, nil
271}
272
273type logoutResp struct {
274 LogoutURL string `json:"logout_url"`
275}
276
277func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
278 jar, err := cookiejar.New(nil)
279 if err != nil {
280 return "", err
281 }
282 client := &http.Client{
283 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400284 Transport: &http.Transport{
285 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
286 },
giolekva603e73a2021-10-22 14:46:45 +0400287 }
giolekvadd750802021-11-07 13:24:21 +0400288 b, err := url.Parse(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400289 if err != nil {
290 return "", err
291 }
292 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400293 resp, err := client.Get(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400294 if err != nil {
295 return "", err
296 }
297 var lr logoutResp
298 if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
299 return "", err
300 }
301 return lr.LogoutURL, nil
302}
303
304func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
305 jar, err := cookiejar.New(nil)
306 if err != nil {
307 return "", err
308 }
309 client := &http.Client{
310 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400311 Transport: &http.Transport{
312 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
313 },
giolekva603e73a2021-10-22 14:46:45 +0400314 }
giolekvadd750802021-11-07 13:24:21 +0400315 b, err := url.Parse(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400316 if err != nil {
317 return "", err
318 }
319 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400320 resp, err := client.Get(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400321 if err != nil {
322 return "", err
323 }
324 respBody, err := ioutil.ReadAll(resp.Body)
325 if err != nil {
326 return "", err
327 }
328 username, err := regogo.Get(string(respBody), "input.identity.traits.username")
329 if err != nil {
330 return "", err
331 }
332 if username.String() == "" {
333 return "", ErrNotLoggedIn
334 }
335 return username.String(), nil
336
337}
338
giolekva788dc6e2021-10-25 20:40:53 +0400339func extractError(r io.Reader) error {
340 respBody, err := ioutil.ReadAll(r)
341 if err != nil {
342 return err
343 }
344 t, err := regogo.Get(string(respBody), "input.ui.messages[0].type")
345 if err != nil {
346 return err
347 }
348 if t.String() == "error" {
349 message, err := regogo.Get(string(respBody), "input.ui.messages[0].text")
350 if err != nil {
351 return err
352 }
353 return errors.New(message.String())
354 }
355 return nil
356}
357
giolekva603e73a2021-10-22 14:46:45 +0400358func (s *Server) login(w http.ResponseWriter, r *http.Request) {
359 if err := r.ParseForm(); err != nil {
360 http.Error(w, err.Error(), http.StatusInternalServerError)
361 return
362 }
363 flow, ok := r.Form["flow"]
364 if !ok {
365 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
366 return
367 }
368 req := loginReq{
369 CSRFToken: r.FormValue("csrf_token"),
370 Method: "password",
371 Password: r.FormValue("password"),
372 Username: r.FormValue("username"),
373 }
374 var reqBody bytes.Buffer
375 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
376 http.Error(w, err.Error(), http.StatusInternalServerError)
377 return
378 }
giolekva788dc6e2021-10-25 20:40:53 +0400379 resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody)
380 if err == nil {
381 err = extractError(resp.Body)
382 }
383 if err != nil {
giolekvaeb590282021-10-22 17:31:40 +0400384 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
giolekva788dc6e2021-10-25 20:40:53 +0400385 redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
giolekvaeb590282021-10-22 17:31:40 +0400386 if err != nil {
387 http.Error(w, err.Error(), http.StatusInternalServerError)
388 return
389 }
giolekva788dc6e2021-10-25 20:40:53 +0400390 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
391 return
giolekvaeb590282021-10-22 17:31:40 +0400392 }
giolekva788dc6e2021-10-25 20:40:53 +0400393 http.Error(w, err.Error(), http.StatusInternalServerError)
394 return
giolekva603e73a2021-10-22 14:46:45 +0400395 }
giolekva788dc6e2021-10-25 20:40:53 +0400396 for _, c := range resp.Cookies() {
397 http.SetCookie(w, c)
398 }
399 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
400 username, err := getWhoAmIFromKratos(resp.Cookies())
401 if err != nil {
402 http.Error(w, err.Error(), http.StatusInternalServerError)
403 return
404 }
405 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
406 if err != nil {
407 http.Error(w, err.Error(), http.StatusInternalServerError)
408 return
409 }
410 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
411 return
412 }
413 http.Redirect(w, r, "/", http.StatusSeeOther)
giolekva603e73a2021-10-22 14:46:45 +0400414}
415
416func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
417 if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
420 } else {
421 http.Redirect(w, r, logoutURL, http.StatusSeeOther)
422 }
423}
424
425func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
426 if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
427 if errors.Is(err, ErrNotLoggedIn) {
428 http.Redirect(w, r, "/login", http.StatusSeeOther)
429 return
430 }
431 http.Error(w, err.Error(), http.StatusInternalServerError)
432 } else {
433 if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
434 http.Error(w, err.Error(), http.StatusInternalServerError)
435 }
436 }
437}
438
giolekva788dc6e2021-10-25 20:40:53 +0400439// TODO(giolekva): verify if logged in
440func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
441 if err := r.ParseForm(); err != nil {
442 http.Error(w, err.Error(), http.StatusBadRequest)
443 return
444 }
445 challenge, ok := r.Form["consent_challenge"]
446 if !ok {
447 http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
448 return
449 }
450 consent, err := s.hydra.GetConsentChallenge(challenge[0])
451 if err != nil {
452 http.Error(w, err.Error(), http.StatusInternalServerError)
453 return
454 }
455 w.Header().Set("Content-Type", "text/html")
456 if err := s.tmpls.Consent.Execute(w, consent.RequestedScopes); err != nil {
457 http.Error(w, err.Error(), http.StatusInternalServerError)
458 return
459 }
460}
461
462func (s *Server) processConsent(w http.ResponseWriter, r *http.Request) {
463 if err := r.ParseForm(); err != nil {
464 http.Error(w, err.Error(), http.StatusBadRequest)
465 return
466 }
467 username, err := getWhoAmIFromKratos(r.Cookies())
468 if err != nil {
469 http.Error(w, err.Error(), http.StatusInternalServerError)
470 return
471 }
472 if _, accepted := r.Form["allow"]; accepted {
473 acceptedScopes, _ := r.Form["scope"]
474 idToken := map[string]string{
475 "username": username,
giolekvadd750802021-11-07 13:24:21 +0400476 "email": username + "@" + *emailDomain,
giolekva788dc6e2021-10-25 20:40:53 +0400477 }
478 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
479 http.Error(w, err.Error(), http.StatusInternalServerError)
480 } else {
481 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
482 }
483 return
484 } else {
485 // TODO(giolekva): implement rejection logic
486 }
487}
488
giolekva603e73a2021-10-22 14:46:45 +0400489func main() {
490 flag.Parse()
491 t, err := ParseTemplates(tmpls)
492 if err != nil {
493 log.Fatal(err)
494 }
495 s := &Server{
496 kratos: *kratos,
giolekva788dc6e2021-10-25 20:40:53 +0400497 hydra: NewHydraClient(*hydra),
giolekva603e73a2021-10-22 14:46:45 +0400498 tmpls: t,
499 }
500 log.Fatal(s.Start(*port))
501}