blob: 518277d4f652011182b2f8a90625bb9c693923d5 [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 {
37 WhoAmI *template.Template
38 Registration *template.Template
39 Login *template.Template
giolekva788dc6e2021-10-25 20:40:53 +040040 Consent *template.Template
giolekva603e73a2021-10-22 14:46:45 +040041}
42
43func ParseTemplates(fs embed.FS) (*Templates, error) {
giolekva788dc6e2021-10-25 20:40:53 +040044 whoami, err := template.ParseFS(fs, "templates/whoami.html")
45 if err != nil {
46 return nil, err
47 }
giolekva603e73a2021-10-22 14:46:45 +040048 registration, err := template.ParseFS(fs, "templates/registration.html")
49 if err != nil {
50 return nil, err
51 }
52 login, err := template.ParseFS(fs, "templates/login.html")
53 if err != nil {
54 return nil, err
55 }
giolekva788dc6e2021-10-25 20:40:53 +040056 consent, err := template.ParseFS(fs, "templates/consent.html")
giolekva603e73a2021-10-22 14:46:45 +040057 if err != nil {
58 return nil, err
59 }
giolekva788dc6e2021-10-25 20:40:53 +040060 return &Templates{whoami, registration, login, consent}, nil
giolekva603e73a2021-10-22 14:46:45 +040061}
62
63type Server struct {
64 kratos string
giolekva788dc6e2021-10-25 20:40:53 +040065 hydra *HydraClient
giolekva603e73a2021-10-22 14:46:45 +040066 tmpls *Templates
67}
68
giolekva47031752021-11-12 14:34:33 +040069func cacheControlWrapper(h http.Handler) http.Handler {
70 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
71 // TODO(giolekva): enable caching
72 // w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
73 h.ServeHTTP(w, r)
74 })
75}
76
giolekva603e73a2021-10-22 14:46:45 +040077func (s *Server) Start(port int) error {
78 r := mux.NewRouter()
79 http.Handle("/", r)
giolekva47031752021-11-12 14:34:33 +040080 var staticFS = http.FS(static)
81 fs := http.FileServer(staticFS)
82 r.PathPrefix("/static/").Handler(cacheControlWrapper(fs))
giolekva603e73a2021-10-22 14:46:45 +040083 r.Path("/registration").Methods(http.MethodGet).HandlerFunc(s.registrationInitiate)
84 r.Path("/registration").Methods(http.MethodPost).HandlerFunc(s.registration)
85 r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
86 r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
giolekva788dc6e2021-10-25 20:40:53 +040087 r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
88 r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
giolekva603e73a2021-10-22 14:46:45 +040089 r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
90 r.Path("/").HandlerFunc(s.whoami)
91 fmt.Printf("Starting HTTP server on port: %d\n", port)
92 return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
93}
94
95func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
96 jar, err := cookiejar.New(nil)
97 if err != nil {
98 return "", err
99 }
100 client := &http.Client{
101 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400102 Transport: &http.Transport{
103 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
104 },
giolekva603e73a2021-10-22 14:46:45 +0400105 }
giolekvadd750802021-11-07 13:24:21 +0400106 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400107 if err != nil {
108 return "", err
109 }
110 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400111 resp, err := client.Get(fmt.Sprintf(*kratos+"/self-service/"+flowType+"/flows?id=%s", flow))
giolekva603e73a2021-10-22 14:46:45 +0400112 if err != nil {
113 return "", err
114 }
115 respBody, err := ioutil.ReadAll(resp.Body)
116 if err != nil {
117 return "", err
118 }
119 token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
120 if err != nil {
121 return "", err
122 }
123 return token.String(), nil
124}
125
126func (s *Server) registrationInitiate(w http.ResponseWriter, r *http.Request) {
127 if err := r.ParseForm(); err != nil {
128 http.Error(w, err.Error(), http.StatusInternalServerError)
129 return
130 }
131 flow, ok := r.Form["flow"]
132 if !ok {
133 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
134 return
135 }
136 csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
137 if err != nil {
138 http.Error(w, err.Error(), http.StatusInternalServerError)
139 return
140 }
giolekva603e73a2021-10-22 14:46:45 +0400141 w.Header().Set("Content-Type", "text/html")
142 if err := s.tmpls.Registration.Execute(w, csrfToken); err != nil {
143 http.Error(w, err.Error(), http.StatusInternalServerError)
144 return
145 }
146}
147
148type regReq struct {
149 CSRFToken string `json:"csrf_token"`
150 Method string `json:"method"`
151 Password string `json:"password"`
152 Traits regReqTraits `json:"traits"`
153}
154
155type regReqTraits struct {
156 Username string `json:"username"`
157}
158
159func (s *Server) registration(w http.ResponseWriter, r *http.Request) {
160 if err := r.ParseForm(); err != nil {
161 http.Error(w, err.Error(), http.StatusInternalServerError)
162 return
163 }
164 flow, ok := r.Form["flow"]
165 if !ok {
166 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
167 return
168 }
169 req := regReq{
170 CSRFToken: r.FormValue("csrf_token"),
171 Method: "password",
172 Password: r.FormValue("password"),
173 Traits: regReqTraits{
174 Username: r.FormValue("username"),
175 },
176 }
177 var reqBody bytes.Buffer
178 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
179 http.Error(w, err.Error(), http.StatusInternalServerError)
180 return
181 }
182 if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
183 http.Error(w, err.Error(), http.StatusInternalServerError)
184 return
185 } else {
186 for _, c := range resp.Cookies() {
187 http.SetCookie(w, c)
188 }
189 http.Redirect(w, r, "/", http.StatusSeeOther)
190 }
191}
192
193// Login flow
194
195func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
196 if err := r.ParseForm(); err != nil {
197 http.Error(w, err.Error(), http.StatusInternalServerError)
198 return
199 }
giolekva788dc6e2021-10-25 20:40:53 +0400200 if challenge, ok := r.Form["login_challenge"]; ok {
201 // TODO(giolekva): encrypt
202 http.SetCookie(w, &http.Cookie{
203 Name: "login_challenge",
204 Value: challenge[0],
205 HttpOnly: true,
206 })
207 } else {
208 // http.SetCookie(w, &http.Cookie{
209 // Name: "login_challenge",
210 // Value: "",
211 // Expires: time.Unix(0, 0),
212 // HttpOnly: true,
213 // })
214 }
giolekva603e73a2021-10-22 14:46:45 +0400215 flow, ok := r.Form["flow"]
216 if !ok {
217 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
218 return
219 }
220 csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
221 if err != nil {
222 http.Error(w, err.Error(), http.StatusInternalServerError)
223 return
224 }
giolekva603e73a2021-10-22 14:46:45 +0400225 w.Header().Set("Content-Type", "text/html")
226 if err := s.tmpls.Login.Execute(w, csrfToken); err != nil {
227 http.Error(w, err.Error(), http.StatusInternalServerError)
228 return
229 }
230}
231
232type loginReq struct {
233 CSRFToken string `json:"csrf_token"`
234 Method string `json:"method"`
235 Password string `json:"password"`
236 Username string `json:"password_identifier"`
237}
238
239func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
240 jar, err := cookiejar.New(nil)
241 if err != nil {
242 return nil, err
243 }
244 client := &http.Client{
245 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400246 Transport: &http.Transport{
247 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
248 },
giolekva603e73a2021-10-22 14:46:45 +0400249 }
giolekvadd750802021-11-07 13:24:21 +0400250 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400251 if err != nil {
252 return nil, err
253 }
254 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400255 resp, err := client.Post(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
giolekva603e73a2021-10-22 14:46:45 +0400256 if err != nil {
257 return nil, err
258 }
259 return resp, nil
260}
261
262type logoutResp struct {
263 LogoutURL string `json:"logout_url"`
264}
265
266func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
267 jar, err := cookiejar.New(nil)
268 if err != nil {
269 return "", err
270 }
271 client := &http.Client{
272 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400273 Transport: &http.Transport{
274 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
275 },
giolekva603e73a2021-10-22 14:46:45 +0400276 }
giolekvadd750802021-11-07 13:24:21 +0400277 b, err := url.Parse(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400278 if err != nil {
279 return "", err
280 }
281 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400282 resp, err := client.Get(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400283 if err != nil {
284 return "", err
285 }
286 var lr logoutResp
287 if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
288 return "", err
289 }
290 return lr.LogoutURL, nil
291}
292
293func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
294 jar, err := cookiejar.New(nil)
295 if err != nil {
296 return "", err
297 }
298 client := &http.Client{
299 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400300 Transport: &http.Transport{
301 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
302 },
giolekva603e73a2021-10-22 14:46:45 +0400303 }
giolekvadd750802021-11-07 13:24:21 +0400304 b, err := url.Parse(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400305 if err != nil {
306 return "", err
307 }
308 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400309 resp, err := client.Get(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400310 if err != nil {
311 return "", err
312 }
313 respBody, err := ioutil.ReadAll(resp.Body)
314 if err != nil {
315 return "", err
316 }
317 username, err := regogo.Get(string(respBody), "input.identity.traits.username")
318 if err != nil {
319 return "", err
320 }
321 if username.String() == "" {
322 return "", ErrNotLoggedIn
323 }
324 return username.String(), nil
325
326}
327
giolekva788dc6e2021-10-25 20:40:53 +0400328func extractError(r io.Reader) error {
329 respBody, err := ioutil.ReadAll(r)
330 if err != nil {
331 return err
332 }
333 t, err := regogo.Get(string(respBody), "input.ui.messages[0].type")
334 if err != nil {
335 return err
336 }
337 if t.String() == "error" {
338 message, err := regogo.Get(string(respBody), "input.ui.messages[0].text")
339 if err != nil {
340 return err
341 }
342 return errors.New(message.String())
343 }
344 return nil
345}
346
giolekva603e73a2021-10-22 14:46:45 +0400347func (s *Server) login(w http.ResponseWriter, r *http.Request) {
348 if err := r.ParseForm(); err != nil {
349 http.Error(w, err.Error(), http.StatusInternalServerError)
350 return
351 }
352 flow, ok := r.Form["flow"]
353 if !ok {
354 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
355 return
356 }
357 req := loginReq{
358 CSRFToken: r.FormValue("csrf_token"),
359 Method: "password",
360 Password: r.FormValue("password"),
361 Username: r.FormValue("username"),
362 }
363 var reqBody bytes.Buffer
364 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
365 http.Error(w, err.Error(), http.StatusInternalServerError)
366 return
367 }
giolekva788dc6e2021-10-25 20:40:53 +0400368 resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody)
369 if err == nil {
370 err = extractError(resp.Body)
371 }
372 if err != nil {
giolekvaeb590282021-10-22 17:31:40 +0400373 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
giolekva788dc6e2021-10-25 20:40:53 +0400374 redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
giolekvaeb590282021-10-22 17:31:40 +0400375 if err != nil {
376 http.Error(w, err.Error(), http.StatusInternalServerError)
377 return
378 }
giolekva788dc6e2021-10-25 20:40:53 +0400379 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
380 return
giolekvaeb590282021-10-22 17:31:40 +0400381 }
giolekva788dc6e2021-10-25 20:40:53 +0400382 http.Error(w, err.Error(), http.StatusInternalServerError)
383 return
giolekva603e73a2021-10-22 14:46:45 +0400384 }
giolekva788dc6e2021-10-25 20:40:53 +0400385 for _, c := range resp.Cookies() {
386 http.SetCookie(w, c)
387 }
388 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
389 username, err := getWhoAmIFromKratos(resp.Cookies())
390 if err != nil {
391 http.Error(w, err.Error(), http.StatusInternalServerError)
392 return
393 }
394 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
395 if err != nil {
396 http.Error(w, err.Error(), http.StatusInternalServerError)
397 return
398 }
399 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
400 return
401 }
402 http.Redirect(w, r, "/", http.StatusSeeOther)
giolekva603e73a2021-10-22 14:46:45 +0400403}
404
405func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
406 if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
407 http.Error(w, err.Error(), http.StatusInternalServerError)
408 return
409 } else {
410 http.Redirect(w, r, logoutURL, http.StatusSeeOther)
411 }
412}
413
414func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
415 if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
416 if errors.Is(err, ErrNotLoggedIn) {
417 http.Redirect(w, r, "/login", http.StatusSeeOther)
418 return
419 }
420 http.Error(w, err.Error(), http.StatusInternalServerError)
421 } else {
422 if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
423 http.Error(w, err.Error(), http.StatusInternalServerError)
424 }
425 }
426}
427
giolekva788dc6e2021-10-25 20:40:53 +0400428// TODO(giolekva): verify if logged in
429func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
430 if err := r.ParseForm(); err != nil {
431 http.Error(w, err.Error(), http.StatusBadRequest)
432 return
433 }
434 challenge, ok := r.Form["consent_challenge"]
435 if !ok {
436 http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
437 return
438 }
439 consent, err := s.hydra.GetConsentChallenge(challenge[0])
440 if err != nil {
441 http.Error(w, err.Error(), http.StatusInternalServerError)
442 return
443 }
444 w.Header().Set("Content-Type", "text/html")
445 if err := s.tmpls.Consent.Execute(w, consent.RequestedScopes); err != nil {
446 http.Error(w, err.Error(), http.StatusInternalServerError)
447 return
448 }
449}
450
451func (s *Server) processConsent(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 username, err := getWhoAmIFromKratos(r.Cookies())
457 if err != nil {
458 http.Error(w, err.Error(), http.StatusInternalServerError)
459 return
460 }
461 if _, accepted := r.Form["allow"]; accepted {
462 acceptedScopes, _ := r.Form["scope"]
463 idToken := map[string]string{
464 "username": username,
giolekvadd750802021-11-07 13:24:21 +0400465 "email": username + "@" + *emailDomain,
giolekva788dc6e2021-10-25 20:40:53 +0400466 }
467 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
468 http.Error(w, err.Error(), http.StatusInternalServerError)
469 } else {
470 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
471 }
472 return
473 } else {
474 // TODO(giolekva): implement rejection logic
475 }
476}
477
giolekva603e73a2021-10-22 14:46:45 +0400478func main() {
479 flag.Parse()
480 t, err := ParseTemplates(tmpls)
481 if err != nil {
482 log.Fatal(err)
483 }
484 s := &Server{
485 kratos: *kratos,
giolekva788dc6e2021-10-25 20:40:53 +0400486 hydra: NewHydraClient(*hydra),
giolekva603e73a2021-10-22 14:46:45 +0400487 tmpls: t,
488 }
489 log.Fatal(s.Start(*port))
490}