blob: 1b5f41a333f9255b6743e39d49bb297bd2ce0810 [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
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040028var apiPort = flag.Int("api-port", 8081, "API Port to listen on")
29var kratosAPI = flag.String("kratos-api", "", "Kratos API address")
30
giolekva603e73a2021-10-22 14:46:45 +040031var ErrNotLoggedIn = errors.New("Not logged in")
32
33//go:embed templates/*
34var tmpls embed.FS
35
giolekva47031752021-11-12 14:34:33 +040036//go:embed static
37var static embed.FS
38
giolekva603e73a2021-10-22 14:46:45 +040039type Templates struct {
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040040 WhoAmI *template.Template
41 Register *template.Template
42 Login *template.Template
43 Consent *template.Template
giolekva603e73a2021-10-22 14:46:45 +040044}
45
46func ParseTemplates(fs embed.FS) (*Templates, error) {
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040047 base, err := template.ParseFS(fs, "templates/base.html")
giolekva788dc6e2021-10-25 20:40:53 +040048 if err != nil {
49 return nil, err
50 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040051 parse := func(path string) (*template.Template, error) {
52 if b, err := base.Clone(); err != nil {
53 return nil, err
54 } else {
55 return b.ParseFS(fs, path)
56 }
57 }
58 whoami, err := parse("templates/whoami.html")
giolekva603e73a2021-10-22 14:46:45 +040059 if err != nil {
60 return nil, err
61 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040062 register, err := parse("templates/register.html")
giolekva603e73a2021-10-22 14:46:45 +040063 if err != nil {
64 return nil, err
65 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040066 login, err := parse("templates/login.html")
giolekva603e73a2021-10-22 14:46:45 +040067 if err != nil {
68 return nil, err
69 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040070 consent, err := parse("templates/consent.html")
71 if err != nil {
72 return nil, err
73 }
74 return &Templates{whoami, register, login, consent}, nil
giolekva603e73a2021-10-22 14:46:45 +040075}
76
77type Server struct {
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040078 r *mux.Router
79 serv *http.Server
giolekva603e73a2021-10-22 14:46:45 +040080 kratos string
giolekva788dc6e2021-10-25 20:40:53 +040081 hydra *HydraClient
giolekva603e73a2021-10-22 14:46:45 +040082 tmpls *Templates
83}
84
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040085func NewServer(port int, kratos string, hydra *HydraClient, tmpls *Templates) *Server {
86 r := mux.NewRouter()
87 serv := &http.Server{
88 Addr: fmt.Sprintf(":%d", port),
89 Handler: r,
90 }
91 return &Server{r, serv, kratos, hydra, tmpls}
92}
93
giolekva47031752021-11-12 14:34:33 +040094func cacheControlWrapper(h http.Handler) http.Handler {
95 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
96 // TODO(giolekva): enable caching
97 // w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
98 h.ServeHTTP(w, r)
99 })
100}
101
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400102func (s *Server) Start() error {
giolekva47031752021-11-12 14:34:33 +0400103 var staticFS = http.FS(static)
104 fs := http.FileServer(staticFS)
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400105 s.r.PathPrefix("/static/").Handler(cacheControlWrapper(fs))
106 s.r.Path("/register").Methods(http.MethodGet).HandlerFunc(s.registerInitiate)
107 s.r.Path("/register").Methods(http.MethodPost).HandlerFunc(s.register)
108 s.r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
109 s.r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
110 s.r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
111 s.r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
112 s.r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
113 s.r.Path("/").HandlerFunc(s.whoami)
114 return s.serv.ListenAndServe()
giolekva603e73a2021-10-22 14:46:45 +0400115}
116
117func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
118 jar, err := cookiejar.New(nil)
119 if err != nil {
120 return "", err
121 }
122 client := &http.Client{
123 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400124 Transport: &http.Transport{
125 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
126 },
giolekva603e73a2021-10-22 14:46:45 +0400127 }
giolekvadd750802021-11-07 13:24:21 +0400128 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400129 if err != nil {
130 return "", err
131 }
132 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400133 resp, err := client.Get(fmt.Sprintf(*kratos+"/self-service/"+flowType+"/flows?id=%s", flow))
giolekva603e73a2021-10-22 14:46:45 +0400134 if err != nil {
135 return "", err
136 }
137 respBody, err := ioutil.ReadAll(resp.Body)
138 if err != nil {
139 return "", err
140 }
141 token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
142 if err != nil {
143 return "", err
144 }
145 return token.String(), nil
146}
147
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400148func (s *Server) registerInitiate(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400149 if err := r.ParseForm(); err != nil {
150 http.Error(w, err.Error(), http.StatusInternalServerError)
151 return
152 }
153 flow, ok := r.Form["flow"]
154 if !ok {
155 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
156 return
157 }
158 csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
159 if err != nil {
160 http.Error(w, err.Error(), http.StatusInternalServerError)
161 return
162 }
giolekva603e73a2021-10-22 14:46:45 +0400163 w.Header().Set("Content-Type", "text/html")
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400164 if err := s.tmpls.Register.Execute(w, csrfToken); err != nil {
giolekva603e73a2021-10-22 14:46:45 +0400165 http.Error(w, err.Error(), http.StatusInternalServerError)
166 return
167 }
168}
169
170type regReq struct {
171 CSRFToken string `json:"csrf_token"`
172 Method string `json:"method"`
173 Password string `json:"password"`
174 Traits regReqTraits `json:"traits"`
175}
176
177type regReqTraits struct {
178 Username string `json:"username"`
179}
180
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400181func (s *Server) register(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400182 if err := r.ParseForm(); err != nil {
183 http.Error(w, err.Error(), http.StatusInternalServerError)
184 return
185 }
186 flow, ok := r.Form["flow"]
187 if !ok {
188 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
189 return
190 }
191 req := regReq{
192 CSRFToken: r.FormValue("csrf_token"),
193 Method: "password",
194 Password: r.FormValue("password"),
195 Traits: regReqTraits{
196 Username: r.FormValue("username"),
197 },
198 }
199 var reqBody bytes.Buffer
200 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
201 http.Error(w, err.Error(), http.StatusInternalServerError)
202 return
203 }
204 if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
205 http.Error(w, err.Error(), http.StatusInternalServerError)
206 return
207 } else {
208 for _, c := range resp.Cookies() {
209 http.SetCookie(w, c)
210 }
211 http.Redirect(w, r, "/", http.StatusSeeOther)
212 }
213}
214
215// Login flow
216
217func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
218 if err := r.ParseForm(); err != nil {
219 http.Error(w, err.Error(), http.StatusInternalServerError)
220 return
221 }
giolekva788dc6e2021-10-25 20:40:53 +0400222 if challenge, ok := r.Form["login_challenge"]; ok {
223 // TODO(giolekva): encrypt
224 http.SetCookie(w, &http.Cookie{
225 Name: "login_challenge",
226 Value: challenge[0],
227 HttpOnly: true,
228 })
229 } else {
230 // http.SetCookie(w, &http.Cookie{
231 // Name: "login_challenge",
232 // Value: "",
233 // Expires: time.Unix(0, 0),
234 // HttpOnly: true,
235 // })
236 }
giolekva603e73a2021-10-22 14:46:45 +0400237 flow, ok := r.Form["flow"]
238 if !ok {
239 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
240 return
241 }
242 csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
243 if err != nil {
244 http.Error(w, err.Error(), http.StatusInternalServerError)
245 return
246 }
giolekva603e73a2021-10-22 14:46:45 +0400247 w.Header().Set("Content-Type", "text/html")
248 if err := s.tmpls.Login.Execute(w, csrfToken); err != nil {
249 http.Error(w, err.Error(), http.StatusInternalServerError)
250 return
251 }
252}
253
254type loginReq struct {
255 CSRFToken string `json:"csrf_token"`
256 Method string `json:"method"`
257 Password string `json:"password"`
258 Username string `json:"password_identifier"`
259}
260
261func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
262 jar, err := cookiejar.New(nil)
263 if err != nil {
264 return nil, err
265 }
266 client := &http.Client{
267 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400268 Transport: &http.Transport{
269 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
270 },
giolekva603e73a2021-10-22 14:46:45 +0400271 }
giolekvadd750802021-11-07 13:24:21 +0400272 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400273 if err != nil {
274 return nil, err
275 }
276 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400277 resp, err := client.Post(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
giolekva603e73a2021-10-22 14:46:45 +0400278 if err != nil {
279 return nil, err
280 }
281 return resp, nil
282}
283
284type logoutResp struct {
285 LogoutURL string `json:"logout_url"`
286}
287
288func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
289 jar, err := cookiejar.New(nil)
290 if err != nil {
291 return "", err
292 }
293 client := &http.Client{
294 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400295 Transport: &http.Transport{
296 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
297 },
giolekva603e73a2021-10-22 14:46:45 +0400298 }
giolekvadd750802021-11-07 13:24:21 +0400299 b, err := url.Parse(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400300 if err != nil {
301 return "", err
302 }
303 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400304 resp, err := client.Get(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400305 if err != nil {
306 return "", err
307 }
308 var lr logoutResp
309 if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
310 return "", err
311 }
312 return lr.LogoutURL, nil
313}
314
315func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
316 jar, err := cookiejar.New(nil)
317 if err != nil {
318 return "", err
319 }
320 client := &http.Client{
321 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400322 Transport: &http.Transport{
323 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
324 },
giolekva603e73a2021-10-22 14:46:45 +0400325 }
giolekvadd750802021-11-07 13:24:21 +0400326 b, err := url.Parse(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400327 if err != nil {
328 return "", err
329 }
330 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400331 resp, err := client.Get(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400332 if err != nil {
333 return "", err
334 }
335 respBody, err := ioutil.ReadAll(resp.Body)
336 if err != nil {
337 return "", err
338 }
339 username, err := regogo.Get(string(respBody), "input.identity.traits.username")
340 if err != nil {
341 return "", err
342 }
343 if username.String() == "" {
344 return "", ErrNotLoggedIn
345 }
346 return username.String(), nil
347
348}
349
giolekva788dc6e2021-10-25 20:40:53 +0400350func extractError(r io.Reader) error {
351 respBody, err := ioutil.ReadAll(r)
352 if err != nil {
353 return err
354 }
355 t, err := regogo.Get(string(respBody), "input.ui.messages[0].type")
356 if err != nil {
357 return err
358 }
359 if t.String() == "error" {
360 message, err := regogo.Get(string(respBody), "input.ui.messages[0].text")
361 if err != nil {
362 return err
363 }
364 return errors.New(message.String())
365 }
366 return nil
367}
368
giolekva603e73a2021-10-22 14:46:45 +0400369func (s *Server) login(w http.ResponseWriter, r *http.Request) {
370 if err := r.ParseForm(); err != nil {
371 http.Error(w, err.Error(), http.StatusInternalServerError)
372 return
373 }
374 flow, ok := r.Form["flow"]
375 if !ok {
376 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
377 return
378 }
379 req := loginReq{
380 CSRFToken: r.FormValue("csrf_token"),
381 Method: "password",
382 Password: r.FormValue("password"),
383 Username: r.FormValue("username"),
384 }
385 var reqBody bytes.Buffer
386 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
387 http.Error(w, err.Error(), http.StatusInternalServerError)
388 return
389 }
giolekva788dc6e2021-10-25 20:40:53 +0400390 resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody)
391 if err == nil {
392 err = extractError(resp.Body)
393 }
394 if err != nil {
giolekvaeb590282021-10-22 17:31:40 +0400395 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
giolekva788dc6e2021-10-25 20:40:53 +0400396 redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
giolekvaeb590282021-10-22 17:31:40 +0400397 if err != nil {
398 http.Error(w, err.Error(), http.StatusInternalServerError)
399 return
400 }
giolekva788dc6e2021-10-25 20:40:53 +0400401 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
402 return
giolekvaeb590282021-10-22 17:31:40 +0400403 }
giolekva788dc6e2021-10-25 20:40:53 +0400404 http.Error(w, err.Error(), http.StatusInternalServerError)
405 return
giolekva603e73a2021-10-22 14:46:45 +0400406 }
giolekva788dc6e2021-10-25 20:40:53 +0400407 for _, c := range resp.Cookies() {
408 http.SetCookie(w, c)
409 }
410 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
411 username, err := getWhoAmIFromKratos(resp.Cookies())
412 if err != nil {
413 http.Error(w, err.Error(), http.StatusInternalServerError)
414 return
415 }
416 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
417 if err != nil {
418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
420 }
421 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
422 return
423 }
424 http.Redirect(w, r, "/", http.StatusSeeOther)
giolekva603e73a2021-10-22 14:46:45 +0400425}
426
427func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
428 if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
429 http.Error(w, err.Error(), http.StatusInternalServerError)
430 return
431 } else {
432 http.Redirect(w, r, logoutURL, http.StatusSeeOther)
433 }
434}
435
436func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
437 if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
438 if errors.Is(err, ErrNotLoggedIn) {
439 http.Redirect(w, r, "/login", http.StatusSeeOther)
440 return
441 }
442 http.Error(w, err.Error(), http.StatusInternalServerError)
443 } else {
444 if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
445 http.Error(w, err.Error(), http.StatusInternalServerError)
446 }
447 }
448}
449
giolekva788dc6e2021-10-25 20:40:53 +0400450// TODO(giolekva): verify if logged in
451func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
452 if err := r.ParseForm(); err != nil {
453 http.Error(w, err.Error(), http.StatusBadRequest)
454 return
455 }
456 challenge, ok := r.Form["consent_challenge"]
457 if !ok {
458 http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
459 return
460 }
461 consent, err := s.hydra.GetConsentChallenge(challenge[0])
462 if err != nil {
463 http.Error(w, err.Error(), http.StatusInternalServerError)
464 return
465 }
466 w.Header().Set("Content-Type", "text/html")
467 if err := s.tmpls.Consent.Execute(w, consent.RequestedScopes); err != nil {
468 http.Error(w, err.Error(), http.StatusInternalServerError)
469 return
470 }
471}
472
473func (s *Server) processConsent(w http.ResponseWriter, r *http.Request) {
474 if err := r.ParseForm(); err != nil {
475 http.Error(w, err.Error(), http.StatusBadRequest)
476 return
477 }
478 username, err := getWhoAmIFromKratos(r.Cookies())
479 if err != nil {
480 http.Error(w, err.Error(), http.StatusInternalServerError)
481 return
482 }
483 if _, accepted := r.Form["allow"]; accepted {
484 acceptedScopes, _ := r.Form["scope"]
485 idToken := map[string]string{
486 "username": username,
giolekvadd750802021-11-07 13:24:21 +0400487 "email": username + "@" + *emailDomain,
giolekva788dc6e2021-10-25 20:40:53 +0400488 }
489 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
490 http.Error(w, err.Error(), http.StatusInternalServerError)
491 } else {
492 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
493 }
494 return
495 } else {
496 // TODO(giolekva): implement rejection logic
497 }
498}
499
giolekva603e73a2021-10-22 14:46:45 +0400500func main() {
501 flag.Parse()
502 t, err := ParseTemplates(tmpls)
503 if err != nil {
504 log.Fatal(err)
505 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400506 go func() {
507 s := NewAPIServer(*apiPort, *kratosAPI)
508 log.Fatal(s.Start())
509 }()
510 func() {
511 s := NewServer(
512 *port,
513 *kratos,
514 NewHydraClient(*hydra),
515 t,
516 )
517 log.Fatal(s.Start())
518 }()
giolekva603e73a2021-10-22 14:46:45 +0400519}