blob: 7cb1f4deaa7c160a0bca79e22900f01e114795b0 [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
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040031var enableRegistration = flag.Bool("enable-registration", false, "If true account registration will be enabled")
32
giolekva603e73a2021-10-22 14:46:45 +040033var ErrNotLoggedIn = errors.New("Not logged in")
34
35//go:embed templates/*
36var tmpls embed.FS
37
giolekva47031752021-11-12 14:34:33 +040038//go:embed static
39var static embed.FS
40
giolekva603e73a2021-10-22 14:46:45 +040041type Templates struct {
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040042 WhoAmI *template.Template
43 Register *template.Template
44 Login *template.Template
45 Consent *template.Template
giolekva603e73a2021-10-22 14:46:45 +040046}
47
48func ParseTemplates(fs embed.FS) (*Templates, error) {
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040049 base, err := template.ParseFS(fs, "templates/base.html")
giolekva788dc6e2021-10-25 20:40:53 +040050 if err != nil {
51 return nil, err
52 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040053 parse := func(path string) (*template.Template, error) {
54 if b, err := base.Clone(); err != nil {
55 return nil, err
56 } else {
57 return b.ParseFS(fs, path)
58 }
59 }
60 whoami, err := parse("templates/whoami.html")
giolekva603e73a2021-10-22 14:46:45 +040061 if err != nil {
62 return nil, err
63 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040064 register, err := parse("templates/register.html")
giolekva603e73a2021-10-22 14:46:45 +040065 if err != nil {
66 return nil, err
67 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040068 login, err := parse("templates/login.html")
giolekva603e73a2021-10-22 14:46:45 +040069 if err != nil {
70 return nil, err
71 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040072 consent, err := parse("templates/consent.html")
73 if err != nil {
74 return nil, err
75 }
76 return &Templates{whoami, register, login, consent}, nil
giolekva603e73a2021-10-22 14:46:45 +040077}
78
79type Server struct {
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040080 r *mux.Router
81 serv *http.Server
82 kratos string
83 hydra *HydraClient
84 tmpls *Templates
85 enableRegistration bool
giolekva603e73a2021-10-22 14:46:45 +040086}
87
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040088func NewServer(port int, kratos string, hydra *HydraClient, tmpls *Templates, enableRegistration bool) *Server {
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040089 r := mux.NewRouter()
90 serv := &http.Server{
91 Addr: fmt.Sprintf(":%d", port),
92 Handler: r,
93 }
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040094 return &Server{r, serv, kratos, hydra, tmpls, enableRegistration}
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040095}
96
giolekva47031752021-11-12 14:34:33 +040097func cacheControlWrapper(h http.Handler) http.Handler {
98 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99 // TODO(giolekva): enable caching
100 // w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
101 h.ServeHTTP(w, r)
102 })
103}
104
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400105func (s *Server) Start() error {
giolekva47031752021-11-12 14:34:33 +0400106 var staticFS = http.FS(static)
107 fs := http.FileServer(staticFS)
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400108 s.r.PathPrefix("/static/").Handler(cacheControlWrapper(fs))
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400109 if s.enableRegistration {
110 s.r.Path("/register").Methods(http.MethodGet).HandlerFunc(s.registerInitiate)
111 s.r.Path("/register").Methods(http.MethodPost).HandlerFunc(s.register)
112 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400113 s.r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
114 s.r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
115 s.r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
116 s.r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
117 s.r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
118 s.r.Path("/").HandlerFunc(s.whoami)
119 return s.serv.ListenAndServe()
giolekva603e73a2021-10-22 14:46:45 +0400120}
121
122func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
123 jar, err := cookiejar.New(nil)
124 if err != nil {
125 return "", err
126 }
127 client := &http.Client{
128 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400129 Transport: &http.Transport{
130 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
131 },
giolekva603e73a2021-10-22 14:46:45 +0400132 }
giolekvadd750802021-11-07 13:24:21 +0400133 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400134 if err != nil {
135 return "", err
136 }
137 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400138 resp, err := client.Get(fmt.Sprintf(*kratos+"/self-service/"+flowType+"/flows?id=%s", flow))
giolekva603e73a2021-10-22 14:46:45 +0400139 if err != nil {
140 return "", err
141 }
142 respBody, err := ioutil.ReadAll(resp.Body)
143 if err != nil {
144 return "", err
145 }
146 token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
147 if err != nil {
148 return "", err
149 }
150 return token.String(), nil
151}
152
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400153func (s *Server) registerInitiate(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400154 if err := r.ParseForm(); err != nil {
155 http.Error(w, err.Error(), http.StatusInternalServerError)
156 return
157 }
158 flow, ok := r.Form["flow"]
159 if !ok {
160 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
161 return
162 }
163 csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
164 if err != nil {
165 http.Error(w, err.Error(), http.StatusInternalServerError)
166 return
167 }
giolekva603e73a2021-10-22 14:46:45 +0400168 w.Header().Set("Content-Type", "text/html")
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400169 if err := s.tmpls.Register.Execute(w, csrfToken); err != nil {
giolekva603e73a2021-10-22 14:46:45 +0400170 http.Error(w, err.Error(), http.StatusInternalServerError)
171 return
172 }
173}
174
175type regReq struct {
176 CSRFToken string `json:"csrf_token"`
177 Method string `json:"method"`
178 Password string `json:"password"`
179 Traits regReqTraits `json:"traits"`
180}
181
182type regReqTraits struct {
183 Username string `json:"username"`
184}
185
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400186func (s *Server) register(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400187 if err := r.ParseForm(); err != nil {
188 http.Error(w, err.Error(), http.StatusInternalServerError)
189 return
190 }
191 flow, ok := r.Form["flow"]
192 if !ok {
193 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
194 return
195 }
196 req := regReq{
197 CSRFToken: r.FormValue("csrf_token"),
198 Method: "password",
199 Password: r.FormValue("password"),
200 Traits: regReqTraits{
201 Username: r.FormValue("username"),
202 },
203 }
204 var reqBody bytes.Buffer
205 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
206 http.Error(w, err.Error(), http.StatusInternalServerError)
207 return
208 }
209 if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
210 http.Error(w, err.Error(), http.StatusInternalServerError)
211 return
212 } else {
213 for _, c := range resp.Cookies() {
214 http.SetCookie(w, c)
215 }
216 http.Redirect(w, r, "/", http.StatusSeeOther)
217 }
218}
219
220// Login flow
221
222func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
223 if err := r.ParseForm(); err != nil {
224 http.Error(w, err.Error(), http.StatusInternalServerError)
225 return
226 }
giolekva788dc6e2021-10-25 20:40:53 +0400227 if challenge, ok := r.Form["login_challenge"]; ok {
228 // TODO(giolekva): encrypt
229 http.SetCookie(w, &http.Cookie{
230 Name: "login_challenge",
231 Value: challenge[0],
232 HttpOnly: true,
233 })
234 } else {
235 // http.SetCookie(w, &http.Cookie{
236 // Name: "login_challenge",
237 // Value: "",
238 // Expires: time.Unix(0, 0),
239 // HttpOnly: true,
240 // })
241 }
giolekva603e73a2021-10-22 14:46:45 +0400242 flow, ok := r.Form["flow"]
243 if !ok {
244 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
245 return
246 }
247 csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
248 if err != nil {
249 http.Error(w, err.Error(), http.StatusInternalServerError)
250 return
251 }
giolekva603e73a2021-10-22 14:46:45 +0400252 w.Header().Set("Content-Type", "text/html")
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400253 if err := s.tmpls.Login.Execute(w, map[string]any{
254 "csrfToken": csrfToken,
255 "enableRegistration": s.enableRegistration,
256 }); err != nil {
giolekva603e73a2021-10-22 14:46:45 +0400257 http.Error(w, err.Error(), http.StatusInternalServerError)
258 return
259 }
260}
261
262type loginReq struct {
263 CSRFToken string `json:"csrf_token"`
264 Method string `json:"method"`
265 Password string `json:"password"`
266 Username string `json:"password_identifier"`
267}
268
269func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
270 jar, err := cookiejar.New(nil)
271 if err != nil {
272 return nil, err
273 }
274 client := &http.Client{
275 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400276 Transport: &http.Transport{
277 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
278 },
giolekva603e73a2021-10-22 14:46:45 +0400279 }
giolekvadd750802021-11-07 13:24:21 +0400280 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400281 if err != nil {
282 return nil, err
283 }
284 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400285 resp, err := client.Post(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
giolekva603e73a2021-10-22 14:46:45 +0400286 if err != nil {
287 return nil, err
288 }
289 return resp, nil
290}
291
292type logoutResp struct {
293 LogoutURL string `json:"logout_url"`
294}
295
296func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
297 jar, err := cookiejar.New(nil)
298 if err != nil {
299 return "", err
300 }
301 client := &http.Client{
302 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400303 Transport: &http.Transport{
304 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
305 },
giolekva603e73a2021-10-22 14:46:45 +0400306 }
giolekvadd750802021-11-07 13:24:21 +0400307 b, err := url.Parse(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400308 if err != nil {
309 return "", err
310 }
311 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400312 resp, err := client.Get(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400313 if err != nil {
314 return "", err
315 }
316 var lr logoutResp
317 if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
318 return "", err
319 }
320 return lr.LogoutURL, nil
321}
322
323func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
324 jar, err := cookiejar.New(nil)
325 if err != nil {
326 return "", err
327 }
328 client := &http.Client{
329 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400330 Transport: &http.Transport{
331 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
332 },
giolekva603e73a2021-10-22 14:46:45 +0400333 }
giolekvadd750802021-11-07 13:24:21 +0400334 b, err := url.Parse(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400335 if err != nil {
336 return "", err
337 }
338 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400339 resp, err := client.Get(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400340 if err != nil {
341 return "", err
342 }
343 respBody, err := ioutil.ReadAll(resp.Body)
344 if err != nil {
345 return "", err
346 }
347 username, err := regogo.Get(string(respBody), "input.identity.traits.username")
348 if err != nil {
349 return "", err
350 }
351 if username.String() == "" {
352 return "", ErrNotLoggedIn
353 }
354 return username.String(), nil
355
356}
357
giolekva788dc6e2021-10-25 20:40:53 +0400358func extractError(r io.Reader) error {
359 respBody, err := ioutil.ReadAll(r)
360 if err != nil {
361 return err
362 }
363 t, err := regogo.Get(string(respBody), "input.ui.messages[0].type")
364 if err != nil {
365 return err
366 }
367 if t.String() == "error" {
368 message, err := regogo.Get(string(respBody), "input.ui.messages[0].text")
369 if err != nil {
370 return err
371 }
372 return errors.New(message.String())
373 }
374 return nil
375}
376
giolekva603e73a2021-10-22 14:46:45 +0400377func (s *Server) login(w http.ResponseWriter, r *http.Request) {
378 if err := r.ParseForm(); err != nil {
379 http.Error(w, err.Error(), http.StatusInternalServerError)
380 return
381 }
382 flow, ok := r.Form["flow"]
383 if !ok {
384 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
385 return
386 }
387 req := loginReq{
388 CSRFToken: r.FormValue("csrf_token"),
389 Method: "password",
390 Password: r.FormValue("password"),
391 Username: r.FormValue("username"),
392 }
393 var reqBody bytes.Buffer
394 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
395 http.Error(w, err.Error(), http.StatusInternalServerError)
396 return
397 }
giolekva788dc6e2021-10-25 20:40:53 +0400398 resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody)
399 if err == nil {
400 err = extractError(resp.Body)
401 }
402 if err != nil {
giolekvaeb590282021-10-22 17:31:40 +0400403 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
giolekva788dc6e2021-10-25 20:40:53 +0400404 redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
giolekvaeb590282021-10-22 17:31:40 +0400405 if err != nil {
406 http.Error(w, err.Error(), http.StatusInternalServerError)
407 return
408 }
giolekva788dc6e2021-10-25 20:40:53 +0400409 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
410 return
giolekvaeb590282021-10-22 17:31:40 +0400411 }
giolekva788dc6e2021-10-25 20:40:53 +0400412 http.Error(w, err.Error(), http.StatusInternalServerError)
413 return
giolekva603e73a2021-10-22 14:46:45 +0400414 }
giolekva788dc6e2021-10-25 20:40:53 +0400415 for _, c := range resp.Cookies() {
416 http.SetCookie(w, c)
417 }
418 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
419 username, err := getWhoAmIFromKratos(resp.Cookies())
420 if err != nil {
421 http.Error(w, err.Error(), http.StatusInternalServerError)
422 return
423 }
424 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
425 if err != nil {
426 http.Error(w, err.Error(), http.StatusInternalServerError)
427 return
428 }
429 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
430 return
431 }
432 http.Redirect(w, r, "/", http.StatusSeeOther)
giolekva603e73a2021-10-22 14:46:45 +0400433}
434
435func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
436 if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
437 http.Error(w, err.Error(), http.StatusInternalServerError)
438 return
439 } else {
440 http.Redirect(w, r, logoutURL, http.StatusSeeOther)
441 }
442}
443
444func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
445 if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
446 if errors.Is(err, ErrNotLoggedIn) {
447 http.Redirect(w, r, "/login", http.StatusSeeOther)
448 return
449 }
450 http.Error(w, err.Error(), http.StatusInternalServerError)
451 } else {
452 if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
453 http.Error(w, err.Error(), http.StatusInternalServerError)
454 }
455 }
456}
457
giolekva788dc6e2021-10-25 20:40:53 +0400458// TODO(giolekva): verify if logged in
459func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
460 if err := r.ParseForm(); err != nil {
461 http.Error(w, err.Error(), http.StatusBadRequest)
462 return
463 }
464 challenge, ok := r.Form["consent_challenge"]
465 if !ok {
466 http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
467 return
468 }
469 consent, err := s.hydra.GetConsentChallenge(challenge[0])
470 if err != nil {
471 http.Error(w, err.Error(), http.StatusInternalServerError)
472 return
473 }
474 w.Header().Set("Content-Type", "text/html")
475 if err := s.tmpls.Consent.Execute(w, consent.RequestedScopes); err != nil {
476 http.Error(w, err.Error(), http.StatusInternalServerError)
477 return
478 }
479}
480
481func (s *Server) processConsent(w http.ResponseWriter, r *http.Request) {
482 if err := r.ParseForm(); err != nil {
483 http.Error(w, err.Error(), http.StatusBadRequest)
484 return
485 }
486 username, err := getWhoAmIFromKratos(r.Cookies())
487 if err != nil {
488 http.Error(w, err.Error(), http.StatusInternalServerError)
489 return
490 }
491 if _, accepted := r.Form["allow"]; accepted {
492 acceptedScopes, _ := r.Form["scope"]
493 idToken := map[string]string{
494 "username": username,
giolekvadd750802021-11-07 13:24:21 +0400495 "email": username + "@" + *emailDomain,
giolekva788dc6e2021-10-25 20:40:53 +0400496 }
497 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
498 http.Error(w, err.Error(), http.StatusInternalServerError)
499 } else {
500 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
501 }
502 return
503 } else {
504 // TODO(giolekva): implement rejection logic
505 }
506}
507
giolekva603e73a2021-10-22 14:46:45 +0400508func main() {
509 flag.Parse()
510 t, err := ParseTemplates(tmpls)
511 if err != nil {
512 log.Fatal(err)
513 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400514 go func() {
515 s := NewAPIServer(*apiPort, *kratosAPI)
516 log.Fatal(s.Start())
517 }()
518 func() {
519 s := NewServer(
520 *port,
521 *kratos,
522 NewHydraClient(*hydra),
523 t,
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400524 *enableRegistration,
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400525 )
526 log.Fatal(s.Start())
527 }()
giolekva603e73a2021-10-22 14:46:45 +0400528}