blob: 92ca88536987576900844b43834dd8fe03d03fdf [file] [log] [blame]
giolekva603e73a2021-10-22 14:46:45 +04001package main
2
3import (
4 "bytes"
giolekvaeb590282021-10-22 17:31:40 +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"
giolekvaeb590282021-10-22 17:31:40 +040018 "strings"
giolekva603e73a2021-10-22 14:46:45 +040019
20 "github.com/gorilla/mux"
21 "github.com/itaysk/regogo"
22)
23
24var port = flag.Int("port", 8080, "Port to listen on")
25var kratos = flag.String("kratos", "https://accounts.lekva.me", "Kratos URL")
26
27var ErrNotLoggedIn = errors.New("Not logged in")
28
29//go:embed templates/*
30var tmpls embed.FS
31
32type Templates struct {
33 WhoAmI *template.Template
34 Registration *template.Template
35 Login *template.Template
36}
37
38func ParseTemplates(fs embed.FS) (*Templates, error) {
39 registration, err := template.ParseFS(fs, "templates/registration.html")
40 if err != nil {
41 return nil, err
42 }
43 login, err := template.ParseFS(fs, "templates/login.html")
44 if err != nil {
45 return nil, err
46 }
47 whoami, err := template.ParseFS(fs, "templates/whoami.html")
48 if err != nil {
49 return nil, err
50 }
51 return &Templates{whoami, registration, login}, nil
52}
53
54type Server struct {
55 kratos string
56 tmpls *Templates
57}
58
59func (s *Server) Start(port int) error {
60 r := mux.NewRouter()
61 http.Handle("/", r)
62 r.Path("/registration").Methods(http.MethodGet).HandlerFunc(s.registrationInitiate)
63 r.Path("/registration").Methods(http.MethodPost).HandlerFunc(s.registration)
64 r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
65 r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
66 r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
67 r.Path("/").HandlerFunc(s.whoami)
68 fmt.Printf("Starting HTTP server on port: %d\n", port)
69 return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
70}
71
72func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
73 jar, err := cookiejar.New(nil)
74 if err != nil {
75 return "", err
76 }
77 client := &http.Client{
78 Jar: jar,
79 }
80 b, err := url.Parse("https://accounts.lekva.me/self-service/" + flowType + "/browser")
81 if err != nil {
82 return "", err
83 }
84 client.Jar.SetCookies(b, cookies)
85 resp, err := client.Get(fmt.Sprintf("https://accounts.lekva.me/self-service/"+flowType+"/flows?id=%s", flow))
86 if err != nil {
87 return "", err
88 }
89 respBody, err := ioutil.ReadAll(resp.Body)
90 if err != nil {
91 return "", err
92 }
93 token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
94 if err != nil {
95 return "", err
96 }
97 return token.String(), nil
98}
99
100func (s *Server) registrationInitiate(w http.ResponseWriter, r *http.Request) {
101 if err := r.ParseForm(); err != nil {
102 http.Error(w, err.Error(), http.StatusInternalServerError)
103 return
104 }
105 flow, ok := r.Form["flow"]
106 if !ok {
107 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
108 return
109 }
110 csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
111 if err != nil {
112 http.Error(w, err.Error(), http.StatusInternalServerError)
113 return
114 }
115 log.Println(csrfToken)
116 w.Header().Set("Content-Type", "text/html")
117 if err := s.tmpls.Registration.Execute(w, csrfToken); err != nil {
118 http.Error(w, err.Error(), http.StatusInternalServerError)
119 return
120 }
121}
122
123type regReq struct {
124 CSRFToken string `json:"csrf_token"`
125 Method string `json:"method"`
126 Password string `json:"password"`
127 Traits regReqTraits `json:"traits"`
128}
129
130type regReqTraits struct {
131 Username string `json:"username"`
132}
133
134func (s *Server) registration(w http.ResponseWriter, r *http.Request) {
135 if err := r.ParseForm(); err != nil {
136 http.Error(w, err.Error(), http.StatusInternalServerError)
137 return
138 }
139 flow, ok := r.Form["flow"]
140 if !ok {
141 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
142 return
143 }
144 req := regReq{
145 CSRFToken: r.FormValue("csrf_token"),
146 Method: "password",
147 Password: r.FormValue("password"),
148 Traits: regReqTraits{
149 Username: r.FormValue("username"),
150 },
151 }
152 var reqBody bytes.Buffer
153 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
154 http.Error(w, err.Error(), http.StatusInternalServerError)
155 return
156 }
157 if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
158 http.Error(w, err.Error(), http.StatusInternalServerError)
159 return
160 } else {
161 for _, c := range resp.Cookies() {
162 http.SetCookie(w, c)
163 }
164 http.Redirect(w, r, "/", http.StatusSeeOther)
165 }
166}
167
168// Login flow
169
170func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
171 if err := r.ParseForm(); err != nil {
172 http.Error(w, err.Error(), http.StatusInternalServerError)
173 return
174 }
175 flow, ok := r.Form["flow"]
176 if !ok {
giolekvaeb590282021-10-22 17:31:40 +0400177 challenge, ok := r.Form["login_challenge"]
178 if ok {
179 // TODO(giolekva): encrypt
180 http.SetCookie(w, &http.Cookie{
181 Name: "login_challenge",
182 Value: challenge[0],
183 HttpOnly: true,
184 })
185 }
giolekva603e73a2021-10-22 14:46:45 +0400186 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
187 return
188 }
189 csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
190 if err != nil {
191 http.Error(w, err.Error(), http.StatusInternalServerError)
192 return
193 }
194 log.Println(csrfToken)
195 w.Header().Set("Content-Type", "text/html")
196 if err := s.tmpls.Login.Execute(w, csrfToken); err != nil {
197 http.Error(w, err.Error(), http.StatusInternalServerError)
198 return
199 }
200}
201
202type loginReq struct {
203 CSRFToken string `json:"csrf_token"`
204 Method string `json:"method"`
205 Password string `json:"password"`
206 Username string `json:"password_identifier"`
207}
208
209func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
210 jar, err := cookiejar.New(nil)
211 if err != nil {
212 return nil, err
213 }
214 client := &http.Client{
215 Jar: jar,
216 }
217 b, err := url.Parse("https://accounts.lekva.me/self-service/" + flowType + "/browser")
218 if err != nil {
219 return nil, err
220 }
221 client.Jar.SetCookies(b, cookies)
222 resp, err := client.Post(fmt.Sprintf("https://accounts.lekva.me/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
223 if err != nil {
224 return nil, err
225 }
226 return resp, nil
227}
228
229type logoutResp struct {
230 LogoutURL string `json:"logout_url"`
231}
232
233func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
234 jar, err := cookiejar.New(nil)
235 if err != nil {
236 return "", err
237 }
238 client := &http.Client{
239 Jar: jar,
240 }
241 b, err := url.Parse("https://accounts.lekva.me/self-service/logout/browser")
242 if err != nil {
243 return "", err
244 }
245 client.Jar.SetCookies(b, cookies)
246 resp, err := client.Get("https://accounts.lekva.me/self-service/logout/browser")
247 if err != nil {
248 return "", err
249 }
250 var lr logoutResp
251 if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
252 return "", err
253 }
254 return lr.LogoutURL, nil
255}
256
257func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
258 jar, err := cookiejar.New(nil)
259 if err != nil {
260 return "", err
261 }
262 client := &http.Client{
263 Jar: jar,
264 }
265 b, err := url.Parse("https://accounts.lekva.me/sessions/whoami")
266 if err != nil {
267 return "", err
268 }
269 client.Jar.SetCookies(b, cookies)
270 resp, err := client.Get("https://accounts.lekva.me/sessions/whoami")
271 if err != nil {
272 return "", err
273 }
274 respBody, err := ioutil.ReadAll(resp.Body)
275 if err != nil {
276 return "", err
277 }
278 username, err := regogo.Get(string(respBody), "input.identity.traits.username")
279 if err != nil {
280 return "", err
281 }
282 if username.String() == "" {
283 return "", ErrNotLoggedIn
284 }
285 return username.String(), nil
286
287}
288
289func (s *Server) login(w http.ResponseWriter, r *http.Request) {
290 if err := r.ParseForm(); err != nil {
291 http.Error(w, err.Error(), http.StatusInternalServerError)
292 return
293 }
294 flow, ok := r.Form["flow"]
295 if !ok {
296 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
297 return
298 }
299 req := loginReq{
300 CSRFToken: r.FormValue("csrf_token"),
301 Method: "password",
302 Password: r.FormValue("password"),
303 Username: r.FormValue("username"),
304 }
305 var reqBody bytes.Buffer
306 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
307 http.Error(w, err.Error(), http.StatusInternalServerError)
308 return
309 }
310 if resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody); err != nil {
311 http.Error(w, err.Error(), http.StatusInternalServerError)
312 return
313 } else {
314 for _, c := range resp.Cookies() {
315 http.SetCookie(w, c)
316 }
giolekvaeb590282021-10-22 17:31:40 +0400317 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
318 username, err := getWhoAmIFromKratos(resp.Cookies())
319 if err != nil {
320 http.Error(w, err.Error(), http.StatusInternalServerError)
321 return
322 }
323 req := &http.Request{
324 Method: http.MethodPut,
325 URL: &url.URL{
326 Scheme: "https",
327 Host: "hydra.pcloud",
328 Path: "/oauth2/auth/requests/login/accept",
329 RawQuery: fmt.Sprintf("login_challenge=%s", challenge.Value),
330 },
331 Header: map[string][]string{
332 "Content-Type": []string{"text/html"},
333 },
334 // TODO(giolekva): user stable userid instead
335 Body: io.NopCloser(strings.NewReader(fmt.Sprintf(`
336{
337 "subject": "%s",
338 "remember": true,
339 "remember_for": 3600
340}`, username))),
341 }
342 client := &http.Client{
343 Transport: &http.Transport{
344 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
345 },
346 }
347 resp, err := client.Do(req)
348 if err != nil {
349 http.Error(w, err.Error(), http.StatusInternalServerError)
350 } else {
351 io.Copy(w, resp.Body)
352 }
353 }
354 // http.Redirect(w, r, "/", http.StatusSeeOther)
giolekva603e73a2021-10-22 14:46:45 +0400355 }
356}
357
358func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
359 if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
360 http.Error(w, err.Error(), http.StatusInternalServerError)
361 return
362 } else {
363 http.Redirect(w, r, logoutURL, http.StatusSeeOther)
364 }
365}
366
367func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
368 if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
369 if errors.Is(err, ErrNotLoggedIn) {
370 http.Redirect(w, r, "/login", http.StatusSeeOther)
371 return
372 }
373 http.Error(w, err.Error(), http.StatusInternalServerError)
374 } else {
375 if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
376 http.Error(w, err.Error(), http.StatusInternalServerError)
377 }
378 }
379}
380
381func main() {
382 flag.Parse()
383 t, err := ParseTemplates(tmpls)
384 if err != nil {
385 log.Fatal(err)
386 }
387 s := &Server{
388 kratos: *kratos,
389 tmpls: t,
390 }
391 log.Fatal(s.Start(*port))
392}