blob: c3add0270f4b31a0d6bfd3230c7d43f46c104618 [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
33type Templates struct {
34 WhoAmI *template.Template
35 Registration *template.Template
36 Login *template.Template
giolekva788dc6e2021-10-25 20:40:53 +040037 Consent *template.Template
giolekva603e73a2021-10-22 14:46:45 +040038}
39
40func ParseTemplates(fs embed.FS) (*Templates, error) {
giolekva788dc6e2021-10-25 20:40:53 +040041 whoami, err := template.ParseFS(fs, "templates/whoami.html")
42 if err != nil {
43 return nil, err
44 }
giolekva603e73a2021-10-22 14:46:45 +040045 registration, err := template.ParseFS(fs, "templates/registration.html")
46 if err != nil {
47 return nil, err
48 }
49 login, err := template.ParseFS(fs, "templates/login.html")
50 if err != nil {
51 return nil, err
52 }
giolekva788dc6e2021-10-25 20:40:53 +040053 consent, err := template.ParseFS(fs, "templates/consent.html")
giolekva603e73a2021-10-22 14:46:45 +040054 if err != nil {
55 return nil, err
56 }
giolekva788dc6e2021-10-25 20:40:53 +040057 return &Templates{whoami, registration, login, consent}, nil
giolekva603e73a2021-10-22 14:46:45 +040058}
59
60type Server struct {
61 kratos string
giolekva788dc6e2021-10-25 20:40:53 +040062 hydra *HydraClient
giolekva603e73a2021-10-22 14:46:45 +040063 tmpls *Templates
64}
65
66func (s *Server) Start(port int) error {
67 r := mux.NewRouter()
68 http.Handle("/", r)
69 r.Path("/registration").Methods(http.MethodGet).HandlerFunc(s.registrationInitiate)
70 r.Path("/registration").Methods(http.MethodPost).HandlerFunc(s.registration)
71 r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
72 r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
giolekva788dc6e2021-10-25 20:40:53 +040073 r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
74 r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
giolekva603e73a2021-10-22 14:46:45 +040075 r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
76 r.Path("/").HandlerFunc(s.whoami)
77 fmt.Printf("Starting HTTP server on port: %d\n", port)
78 return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
79}
80
81func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
82 jar, err := cookiejar.New(nil)
83 if err != nil {
84 return "", err
85 }
86 client := &http.Client{
87 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +040088 Transport: &http.Transport{
89 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
90 },
giolekva603e73a2021-10-22 14:46:45 +040091 }
giolekvadd750802021-11-07 13:24:21 +040092 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +040093 if err != nil {
94 return "", err
95 }
96 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +040097 resp, err := client.Get(fmt.Sprintf(*kratos+"/self-service/"+flowType+"/flows?id=%s", flow))
giolekva603e73a2021-10-22 14:46:45 +040098 if err != nil {
99 return "", err
100 }
101 respBody, err := ioutil.ReadAll(resp.Body)
102 if err != nil {
103 return "", err
104 }
105 token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
106 if err != nil {
107 return "", err
108 }
109 return token.String(), nil
110}
111
112func (s *Server) registrationInitiate(w http.ResponseWriter, r *http.Request) {
113 if err := r.ParseForm(); err != nil {
114 http.Error(w, err.Error(), http.StatusInternalServerError)
115 return
116 }
117 flow, ok := r.Form["flow"]
118 if !ok {
119 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
120 return
121 }
122 csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
123 if err != nil {
124 http.Error(w, err.Error(), http.StatusInternalServerError)
125 return
126 }
giolekva603e73a2021-10-22 14:46:45 +0400127 w.Header().Set("Content-Type", "text/html")
128 if err := s.tmpls.Registration.Execute(w, csrfToken); err != nil {
129 http.Error(w, err.Error(), http.StatusInternalServerError)
130 return
131 }
132}
133
134type regReq struct {
135 CSRFToken string `json:"csrf_token"`
136 Method string `json:"method"`
137 Password string `json:"password"`
138 Traits regReqTraits `json:"traits"`
139}
140
141type regReqTraits struct {
142 Username string `json:"username"`
143}
144
145func (s *Server) registration(w http.ResponseWriter, r *http.Request) {
146 if err := r.ParseForm(); err != nil {
147 http.Error(w, err.Error(), http.StatusInternalServerError)
148 return
149 }
150 flow, ok := r.Form["flow"]
151 if !ok {
152 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
153 return
154 }
155 req := regReq{
156 CSRFToken: r.FormValue("csrf_token"),
157 Method: "password",
158 Password: r.FormValue("password"),
159 Traits: regReqTraits{
160 Username: r.FormValue("username"),
161 },
162 }
163 var reqBody bytes.Buffer
164 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
165 http.Error(w, err.Error(), http.StatusInternalServerError)
166 return
167 }
168 if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
169 http.Error(w, err.Error(), http.StatusInternalServerError)
170 return
171 } else {
172 for _, c := range resp.Cookies() {
173 http.SetCookie(w, c)
174 }
175 http.Redirect(w, r, "/", http.StatusSeeOther)
176 }
177}
178
179// Login flow
180
181func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
182 if err := r.ParseForm(); err != nil {
183 http.Error(w, err.Error(), http.StatusInternalServerError)
184 return
185 }
giolekva788dc6e2021-10-25 20:40:53 +0400186 if challenge, ok := r.Form["login_challenge"]; ok {
187 // TODO(giolekva): encrypt
188 http.SetCookie(w, &http.Cookie{
189 Name: "login_challenge",
190 Value: challenge[0],
191 HttpOnly: true,
192 })
193 } else {
194 // http.SetCookie(w, &http.Cookie{
195 // Name: "login_challenge",
196 // Value: "",
197 // Expires: time.Unix(0, 0),
198 // HttpOnly: true,
199 // })
200 }
giolekva603e73a2021-10-22 14:46:45 +0400201 flow, ok := r.Form["flow"]
202 if !ok {
203 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
204 return
205 }
206 csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
207 if err != nil {
208 http.Error(w, err.Error(), http.StatusInternalServerError)
209 return
210 }
giolekva603e73a2021-10-22 14:46:45 +0400211 w.Header().Set("Content-Type", "text/html")
212 if err := s.tmpls.Login.Execute(w, csrfToken); err != nil {
213 http.Error(w, err.Error(), http.StatusInternalServerError)
214 return
215 }
216}
217
218type loginReq struct {
219 CSRFToken string `json:"csrf_token"`
220 Method string `json:"method"`
221 Password string `json:"password"`
222 Username string `json:"password_identifier"`
223}
224
225func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
226 jar, err := cookiejar.New(nil)
227 if err != nil {
228 return nil, err
229 }
230 client := &http.Client{
231 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400232 Transport: &http.Transport{
233 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
234 },
giolekva603e73a2021-10-22 14:46:45 +0400235 }
giolekvadd750802021-11-07 13:24:21 +0400236 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400237 if err != nil {
238 return nil, err
239 }
240 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400241 resp, err := client.Post(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
giolekva603e73a2021-10-22 14:46:45 +0400242 if err != nil {
243 return nil, err
244 }
245 return resp, nil
246}
247
248type logoutResp struct {
249 LogoutURL string `json:"logout_url"`
250}
251
252func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
253 jar, err := cookiejar.New(nil)
254 if err != nil {
255 return "", err
256 }
257 client := &http.Client{
258 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400259 Transport: &http.Transport{
260 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
261 },
giolekva603e73a2021-10-22 14:46:45 +0400262 }
giolekvadd750802021-11-07 13:24:21 +0400263 b, err := url.Parse(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400264 if err != nil {
265 return "", err
266 }
267 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400268 resp, err := client.Get(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400269 if err != nil {
270 return "", err
271 }
272 var lr logoutResp
273 if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
274 return "", err
275 }
276 return lr.LogoutURL, nil
277}
278
279func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
280 jar, err := cookiejar.New(nil)
281 if err != nil {
282 return "", err
283 }
284 client := &http.Client{
285 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400286 Transport: &http.Transport{
287 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
288 },
giolekva603e73a2021-10-22 14:46:45 +0400289 }
giolekvadd750802021-11-07 13:24:21 +0400290 b, err := url.Parse(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400291 if err != nil {
292 return "", err
293 }
294 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400295 resp, err := client.Get(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400296 if err != nil {
297 return "", err
298 }
299 respBody, err := ioutil.ReadAll(resp.Body)
300 if err != nil {
301 return "", err
302 }
303 username, err := regogo.Get(string(respBody), "input.identity.traits.username")
304 if err != nil {
305 return "", err
306 }
307 if username.String() == "" {
308 return "", ErrNotLoggedIn
309 }
310 return username.String(), nil
311
312}
313
giolekva788dc6e2021-10-25 20:40:53 +0400314func extractError(r io.Reader) error {
315 respBody, err := ioutil.ReadAll(r)
316 if err != nil {
317 return err
318 }
319 t, err := regogo.Get(string(respBody), "input.ui.messages[0].type")
320 if err != nil {
321 return err
322 }
323 if t.String() == "error" {
324 message, err := regogo.Get(string(respBody), "input.ui.messages[0].text")
325 if err != nil {
326 return err
327 }
328 return errors.New(message.String())
329 }
330 return nil
331}
332
giolekva603e73a2021-10-22 14:46:45 +0400333func (s *Server) login(w http.ResponseWriter, r *http.Request) {
334 if err := r.ParseForm(); err != nil {
335 http.Error(w, err.Error(), http.StatusInternalServerError)
336 return
337 }
338 flow, ok := r.Form["flow"]
339 if !ok {
340 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
341 return
342 }
343 req := loginReq{
344 CSRFToken: r.FormValue("csrf_token"),
345 Method: "password",
346 Password: r.FormValue("password"),
347 Username: r.FormValue("username"),
348 }
349 var reqBody bytes.Buffer
350 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
351 http.Error(w, err.Error(), http.StatusInternalServerError)
352 return
353 }
giolekva788dc6e2021-10-25 20:40:53 +0400354 resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody)
355 if err == nil {
356 err = extractError(resp.Body)
357 }
358 if err != nil {
giolekvaeb590282021-10-22 17:31:40 +0400359 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
giolekva788dc6e2021-10-25 20:40:53 +0400360 redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
giolekvaeb590282021-10-22 17:31:40 +0400361 if err != nil {
362 http.Error(w, err.Error(), http.StatusInternalServerError)
363 return
364 }
giolekva788dc6e2021-10-25 20:40:53 +0400365 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
366 return
giolekvaeb590282021-10-22 17:31:40 +0400367 }
giolekva788dc6e2021-10-25 20:40:53 +0400368 http.Error(w, err.Error(), http.StatusInternalServerError)
369 return
giolekva603e73a2021-10-22 14:46:45 +0400370 }
giolekva788dc6e2021-10-25 20:40:53 +0400371 for _, c := range resp.Cookies() {
372 http.SetCookie(w, c)
373 }
374 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
375 username, err := getWhoAmIFromKratos(resp.Cookies())
376 if err != nil {
377 http.Error(w, err.Error(), http.StatusInternalServerError)
378 return
379 }
380 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
381 if err != nil {
382 http.Error(w, err.Error(), http.StatusInternalServerError)
383 return
384 }
385 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
386 return
387 }
388 http.Redirect(w, r, "/", http.StatusSeeOther)
giolekva603e73a2021-10-22 14:46:45 +0400389}
390
391func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
392 if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
393 http.Error(w, err.Error(), http.StatusInternalServerError)
394 return
395 } else {
396 http.Redirect(w, r, logoutURL, http.StatusSeeOther)
397 }
398}
399
400func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
401 if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
402 if errors.Is(err, ErrNotLoggedIn) {
403 http.Redirect(w, r, "/login", http.StatusSeeOther)
404 return
405 }
406 http.Error(w, err.Error(), http.StatusInternalServerError)
407 } else {
408 if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
409 http.Error(w, err.Error(), http.StatusInternalServerError)
410 }
411 }
412}
413
giolekva788dc6e2021-10-25 20:40:53 +0400414// TODO(giolekva): verify if logged in
415func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
416 if err := r.ParseForm(); err != nil {
417 http.Error(w, err.Error(), http.StatusBadRequest)
418 return
419 }
420 challenge, ok := r.Form["consent_challenge"]
421 if !ok {
422 http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
423 return
424 }
425 consent, err := s.hydra.GetConsentChallenge(challenge[0])
426 if err != nil {
427 http.Error(w, err.Error(), http.StatusInternalServerError)
428 return
429 }
430 w.Header().Set("Content-Type", "text/html")
431 if err := s.tmpls.Consent.Execute(w, consent.RequestedScopes); err != nil {
432 http.Error(w, err.Error(), http.StatusInternalServerError)
433 return
434 }
435}
436
437func (s *Server) processConsent(w http.ResponseWriter, r *http.Request) {
438 if err := r.ParseForm(); err != nil {
439 http.Error(w, err.Error(), http.StatusBadRequest)
440 return
441 }
442 username, err := getWhoAmIFromKratos(r.Cookies())
443 if err != nil {
444 http.Error(w, err.Error(), http.StatusInternalServerError)
445 return
446 }
447 if _, accepted := r.Form["allow"]; accepted {
448 acceptedScopes, _ := r.Form["scope"]
449 idToken := map[string]string{
450 "username": username,
giolekvadd750802021-11-07 13:24:21 +0400451 "email": username + "@" + *emailDomain,
giolekva788dc6e2021-10-25 20:40:53 +0400452 }
453 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
454 http.Error(w, err.Error(), http.StatusInternalServerError)
455 } else {
456 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
457 }
458 return
459 } else {
460 // TODO(giolekva): implement rejection logic
461 }
462}
463
giolekva603e73a2021-10-22 14:46:45 +0400464func main() {
465 flag.Parse()
466 t, err := ParseTemplates(tmpls)
467 if err != nil {
468 log.Fatal(err)
469 }
470 s := &Server{
471 kratos: *kratos,
giolekva788dc6e2021-10-25 20:40:53 +0400472 hydra: NewHydraClient(*hydra),
giolekva603e73a2021-10-22 14:46:45 +0400473 tmpls: t,
474 }
475 log.Fatal(s.Start(*port))
476}