blob: a9b349d93120ac6919064a2d6cc890e2390d15dd [file] [log] [blame]
giolekva603e73a2021-10-22 14:46:45 +04001package main
2
3import (
4 "bytes"
5 "embed"
6 "encoding/json"
7 "errors"
8 "flag"
9 "fmt"
10 "html/template"
11 "io"
12 "io/ioutil"
13 "log"
14 "net/http"
15 "net/http/cookiejar"
16 "net/url"
17
18 "github.com/gorilla/mux"
19 "github.com/itaysk/regogo"
20)
21
22var port = flag.Int("port", 8080, "Port to listen on")
23var kratos = flag.String("kratos", "https://accounts.lekva.me", "Kratos URL")
giolekva788dc6e2021-10-25 20:40:53 +040024var hydra = flag.String("hydra", "hydra.pcloud", "Hydra admin server address")
giolekva603e73a2021-10-22 14:46:45 +040025
26var ErrNotLoggedIn = errors.New("Not logged in")
27
28//go:embed templates/*
29var tmpls embed.FS
30
31type Templates struct {
32 WhoAmI *template.Template
33 Registration *template.Template
34 Login *template.Template
giolekva788dc6e2021-10-25 20:40:53 +040035 Consent *template.Template
giolekva603e73a2021-10-22 14:46:45 +040036}
37
38func ParseTemplates(fs embed.FS) (*Templates, error) {
giolekva788dc6e2021-10-25 20:40:53 +040039 whoami, err := template.ParseFS(fs, "templates/whoami.html")
40 if err != nil {
41 return nil, err
42 }
giolekva603e73a2021-10-22 14:46:45 +040043 registration, err := template.ParseFS(fs, "templates/registration.html")
44 if err != nil {
45 return nil, err
46 }
47 login, err := template.ParseFS(fs, "templates/login.html")
48 if err != nil {
49 return nil, err
50 }
giolekva788dc6e2021-10-25 20:40:53 +040051 consent, err := template.ParseFS(fs, "templates/consent.html")
giolekva603e73a2021-10-22 14:46:45 +040052 if err != nil {
53 return nil, err
54 }
giolekva788dc6e2021-10-25 20:40:53 +040055 return &Templates{whoami, registration, login, consent}, nil
giolekva603e73a2021-10-22 14:46:45 +040056}
57
58type Server struct {
59 kratos string
giolekva788dc6e2021-10-25 20:40:53 +040060 hydra *HydraClient
giolekva603e73a2021-10-22 14:46:45 +040061 tmpls *Templates
62}
63
64func (s *Server) Start(port int) error {
65 r := mux.NewRouter()
66 http.Handle("/", r)
67 r.Path("/registration").Methods(http.MethodGet).HandlerFunc(s.registrationInitiate)
68 r.Path("/registration").Methods(http.MethodPost).HandlerFunc(s.registration)
69 r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
70 r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
giolekva788dc6e2021-10-25 20:40:53 +040071 r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
72 r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
giolekva603e73a2021-10-22 14:46:45 +040073 r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
74 r.Path("/").HandlerFunc(s.whoami)
75 fmt.Printf("Starting HTTP server on port: %d\n", port)
76 return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
77}
78
79func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
80 jar, err := cookiejar.New(nil)
81 if err != nil {
82 return "", err
83 }
84 client := &http.Client{
85 Jar: jar,
86 }
87 b, err := url.Parse("https://accounts.lekva.me/self-service/" + flowType + "/browser")
88 if err != nil {
89 return "", err
90 }
91 client.Jar.SetCookies(b, cookies)
92 resp, err := client.Get(fmt.Sprintf("https://accounts.lekva.me/self-service/"+flowType+"/flows?id=%s", flow))
93 if err != nil {
94 return "", err
95 }
96 respBody, err := ioutil.ReadAll(resp.Body)
97 if err != nil {
98 return "", err
99 }
100 token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
101 if err != nil {
102 return "", err
103 }
104 return token.String(), nil
105}
106
107func (s *Server) registrationInitiate(w http.ResponseWriter, r *http.Request) {
108 if err := r.ParseForm(); err != nil {
109 http.Error(w, err.Error(), http.StatusInternalServerError)
110 return
111 }
112 flow, ok := r.Form["flow"]
113 if !ok {
114 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
115 return
116 }
117 csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
118 if err != nil {
119 http.Error(w, err.Error(), http.StatusInternalServerError)
120 return
121 }
giolekva603e73a2021-10-22 14:46:45 +0400122 w.Header().Set("Content-Type", "text/html")
123 if err := s.tmpls.Registration.Execute(w, csrfToken); err != nil {
124 http.Error(w, err.Error(), http.StatusInternalServerError)
125 return
126 }
127}
128
129type regReq struct {
130 CSRFToken string `json:"csrf_token"`
131 Method string `json:"method"`
132 Password string `json:"password"`
133 Traits regReqTraits `json:"traits"`
134}
135
136type regReqTraits struct {
137 Username string `json:"username"`
138}
139
140func (s *Server) registration(w http.ResponseWriter, r *http.Request) {
141 if err := r.ParseForm(); err != nil {
142 http.Error(w, err.Error(), http.StatusInternalServerError)
143 return
144 }
145 flow, ok := r.Form["flow"]
146 if !ok {
147 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
148 return
149 }
150 req := regReq{
151 CSRFToken: r.FormValue("csrf_token"),
152 Method: "password",
153 Password: r.FormValue("password"),
154 Traits: regReqTraits{
155 Username: r.FormValue("username"),
156 },
157 }
158 var reqBody bytes.Buffer
159 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
160 http.Error(w, err.Error(), http.StatusInternalServerError)
161 return
162 }
163 if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
164 http.Error(w, err.Error(), http.StatusInternalServerError)
165 return
166 } else {
167 for _, c := range resp.Cookies() {
168 http.SetCookie(w, c)
169 }
170 http.Redirect(w, r, "/", http.StatusSeeOther)
171 }
172}
173
174// Login flow
175
176func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
177 if err := r.ParseForm(); err != nil {
178 http.Error(w, err.Error(), http.StatusInternalServerError)
179 return
180 }
giolekva788dc6e2021-10-25 20:40:53 +0400181 if challenge, ok := r.Form["login_challenge"]; ok {
182 // TODO(giolekva): encrypt
183 http.SetCookie(w, &http.Cookie{
184 Name: "login_challenge",
185 Value: challenge[0],
186 HttpOnly: true,
187 })
188 } else {
189 // http.SetCookie(w, &http.Cookie{
190 // Name: "login_challenge",
191 // Value: "",
192 // Expires: time.Unix(0, 0),
193 // HttpOnly: true,
194 // })
195 }
giolekva603e73a2021-10-22 14:46:45 +0400196 flow, ok := r.Form["flow"]
197 if !ok {
198 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
199 return
200 }
201 csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
202 if err != nil {
203 http.Error(w, err.Error(), http.StatusInternalServerError)
204 return
205 }
giolekva603e73a2021-10-22 14:46:45 +0400206 w.Header().Set("Content-Type", "text/html")
207 if err := s.tmpls.Login.Execute(w, csrfToken); err != nil {
208 http.Error(w, err.Error(), http.StatusInternalServerError)
209 return
210 }
211}
212
213type loginReq struct {
214 CSRFToken string `json:"csrf_token"`
215 Method string `json:"method"`
216 Password string `json:"password"`
217 Username string `json:"password_identifier"`
218}
219
220func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
221 jar, err := cookiejar.New(nil)
222 if err != nil {
223 return nil, err
224 }
225 client := &http.Client{
226 Jar: jar,
227 }
228 b, err := url.Parse("https://accounts.lekva.me/self-service/" + flowType + "/browser")
229 if err != nil {
230 return nil, err
231 }
232 client.Jar.SetCookies(b, cookies)
233 resp, err := client.Post(fmt.Sprintf("https://accounts.lekva.me/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
234 if err != nil {
235 return nil, err
236 }
237 return resp, nil
238}
239
240type logoutResp struct {
241 LogoutURL string `json:"logout_url"`
242}
243
244func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
245 jar, err := cookiejar.New(nil)
246 if err != nil {
247 return "", err
248 }
249 client := &http.Client{
250 Jar: jar,
251 }
252 b, err := url.Parse("https://accounts.lekva.me/self-service/logout/browser")
253 if err != nil {
254 return "", err
255 }
256 client.Jar.SetCookies(b, cookies)
257 resp, err := client.Get("https://accounts.lekva.me/self-service/logout/browser")
258 if err != nil {
259 return "", err
260 }
261 var lr logoutResp
262 if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
263 return "", err
264 }
265 return lr.LogoutURL, nil
266}
267
268func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
269 jar, err := cookiejar.New(nil)
270 if err != nil {
271 return "", err
272 }
273 client := &http.Client{
274 Jar: jar,
275 }
276 b, err := url.Parse("https://accounts.lekva.me/sessions/whoami")
277 if err != nil {
278 return "", err
279 }
280 client.Jar.SetCookies(b, cookies)
281 resp, err := client.Get("https://accounts.lekva.me/sessions/whoami")
282 if err != nil {
283 return "", err
284 }
285 respBody, err := ioutil.ReadAll(resp.Body)
286 if err != nil {
287 return "", err
288 }
289 username, err := regogo.Get(string(respBody), "input.identity.traits.username")
290 if err != nil {
291 return "", err
292 }
293 if username.String() == "" {
294 return "", ErrNotLoggedIn
295 }
296 return username.String(), nil
297
298}
299
giolekva788dc6e2021-10-25 20:40:53 +0400300func extractError(r io.Reader) error {
301 respBody, err := ioutil.ReadAll(r)
302 if err != nil {
303 return err
304 }
305 t, err := regogo.Get(string(respBody), "input.ui.messages[0].type")
306 if err != nil {
307 return err
308 }
309 if t.String() == "error" {
310 message, err := regogo.Get(string(respBody), "input.ui.messages[0].text")
311 if err != nil {
312 return err
313 }
314 return errors.New(message.String())
315 }
316 return nil
317}
318
giolekva603e73a2021-10-22 14:46:45 +0400319func (s *Server) login(w http.ResponseWriter, r *http.Request) {
320 if err := r.ParseForm(); err != nil {
321 http.Error(w, err.Error(), http.StatusInternalServerError)
322 return
323 }
324 flow, ok := r.Form["flow"]
325 if !ok {
326 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
327 return
328 }
329 req := loginReq{
330 CSRFToken: r.FormValue("csrf_token"),
331 Method: "password",
332 Password: r.FormValue("password"),
333 Username: r.FormValue("username"),
334 }
335 var reqBody bytes.Buffer
336 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
337 http.Error(w, err.Error(), http.StatusInternalServerError)
338 return
339 }
giolekva788dc6e2021-10-25 20:40:53 +0400340 resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody)
341 if err == nil {
342 err = extractError(resp.Body)
343 }
344 if err != nil {
giolekvaeb590282021-10-22 17:31:40 +0400345 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
giolekva788dc6e2021-10-25 20:40:53 +0400346 redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
giolekvaeb590282021-10-22 17:31:40 +0400347 if err != nil {
348 http.Error(w, err.Error(), http.StatusInternalServerError)
349 return
350 }
giolekva788dc6e2021-10-25 20:40:53 +0400351 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
352 return
giolekvaeb590282021-10-22 17:31:40 +0400353 }
giolekva788dc6e2021-10-25 20:40:53 +0400354 http.Error(w, err.Error(), http.StatusInternalServerError)
355 return
giolekva603e73a2021-10-22 14:46:45 +0400356 }
giolekva788dc6e2021-10-25 20:40:53 +0400357 for _, c := range resp.Cookies() {
358 http.SetCookie(w, c)
359 }
360 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
361 username, err := getWhoAmIFromKratos(resp.Cookies())
362 if err != nil {
363 http.Error(w, err.Error(), http.StatusInternalServerError)
364 return
365 }
366 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
367 if err != nil {
368 http.Error(w, err.Error(), http.StatusInternalServerError)
369 return
370 }
371 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
372 return
373 }
374 http.Redirect(w, r, "/", http.StatusSeeOther)
giolekva603e73a2021-10-22 14:46:45 +0400375}
376
377func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
378 if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
379 http.Error(w, err.Error(), http.StatusInternalServerError)
380 return
381 } else {
382 http.Redirect(w, r, logoutURL, http.StatusSeeOther)
383 }
384}
385
386func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
387 if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
388 if errors.Is(err, ErrNotLoggedIn) {
389 http.Redirect(w, r, "/login", http.StatusSeeOther)
390 return
391 }
392 http.Error(w, err.Error(), http.StatusInternalServerError)
393 } else {
394 if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
395 http.Error(w, err.Error(), http.StatusInternalServerError)
396 }
397 }
398}
399
giolekva788dc6e2021-10-25 20:40:53 +0400400// TODO(giolekva): verify if logged in
401func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
402 if err := r.ParseForm(); err != nil {
403 http.Error(w, err.Error(), http.StatusBadRequest)
404 return
405 }
406 challenge, ok := r.Form["consent_challenge"]
407 if !ok {
408 http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
409 return
410 }
411 consent, err := s.hydra.GetConsentChallenge(challenge[0])
412 if err != nil {
413 http.Error(w, err.Error(), http.StatusInternalServerError)
414 return
415 }
416 w.Header().Set("Content-Type", "text/html")
417 if err := s.tmpls.Consent.Execute(w, consent.RequestedScopes); err != nil {
418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
420 }
421}
422
423func (s *Server) processConsent(w http.ResponseWriter, r *http.Request) {
424 if err := r.ParseForm(); err != nil {
425 http.Error(w, err.Error(), http.StatusBadRequest)
426 return
427 }
428 username, err := getWhoAmIFromKratos(r.Cookies())
429 if err != nil {
430 http.Error(w, err.Error(), http.StatusInternalServerError)
431 return
432 }
433 if _, accepted := r.Form["allow"]; accepted {
434 acceptedScopes, _ := r.Form["scope"]
435 idToken := map[string]string{
436 "username": username,
437 "email": username + "@lekva.me",
438 }
439 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
440 http.Error(w, err.Error(), http.StatusInternalServerError)
441 } else {
442 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
443 }
444 return
445 } else {
446 // TODO(giolekva): implement rejection logic
447 }
448}
449
giolekva603e73a2021-10-22 14:46:45 +0400450func main() {
451 flag.Parse()
452 t, err := ParseTemplates(tmpls)
453 if err != nil {
454 log.Fatal(err)
455 }
456 s := &Server{
457 kratos: *kratos,
giolekva788dc6e2021-10-25 20:40:53 +0400458 hydra: NewHydraClient(*hydra),
giolekva603e73a2021-10-22 14:46:45 +0400459 tmpls: t,
460 }
461 log.Fatal(s.Start(*port))
462}