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