blob: 55dc963ee603587d7d471a364cf2a696ce6b47ed [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")
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040027var apiPort = flag.Int("api-port", 8081, "API Port to listen on")
28var kratosAPI = flag.String("kratos-api", "", "Kratos API address")
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040029var enableRegistration = flag.Bool("enable-registration", false, "If true account registration will be enabled")
giodd213152024-09-27 11:26:59 +020030var defaultReturnTo = flag.String("default-return-to", "", "Default redirect address after login")
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040031
giolekva603e73a2021-10-22 14:46:45 +040032var ErrNotLoggedIn = errors.New("Not logged in")
33
34//go:embed templates/*
35var tmpls embed.FS
36
giolekva47031752021-11-12 14:34:33 +040037//go:embed static
38var static embed.FS
39
giolekva603e73a2021-10-22 14:46:45 +040040type Templates struct {
giodd213152024-09-27 11:26:59 +020041 WhoAmI *template.Template
42 Register *template.Template
43 Login *template.Template
44 Consent *template.Template
45 ChangePassword *template.Template
46 ChangePasswordSuccess *template.Template
giolekva603e73a2021-10-22 14:46:45 +040047}
48
49func ParseTemplates(fs embed.FS) (*Templates, error) {
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040050 base, err := template.ParseFS(fs, "templates/base.html")
giolekva788dc6e2021-10-25 20:40:53 +040051 if err != nil {
52 return nil, err
53 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040054 parse := func(path string) (*template.Template, error) {
55 if b, err := base.Clone(); err != nil {
56 return nil, err
57 } else {
58 return b.ParseFS(fs, path)
59 }
60 }
61 whoami, err := parse("templates/whoami.html")
giolekva603e73a2021-10-22 14:46:45 +040062 if err != nil {
63 return nil, err
64 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040065 register, err := parse("templates/register.html")
giolekva603e73a2021-10-22 14:46:45 +040066 if err != nil {
67 return nil, err
68 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040069 login, err := parse("templates/login.html")
giolekva603e73a2021-10-22 14:46:45 +040070 if err != nil {
71 return nil, err
72 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040073 consent, err := parse("templates/consent.html")
74 if err != nil {
75 return nil, err
76 }
giodd213152024-09-27 11:26:59 +020077 changePassword, err := parse("templates/change-password.html")
78 if err != nil {
79 return nil, err
80 }
81 changePasswordSuccess, err := parse("templates/change-password-success.html")
82 if err != nil {
83 return nil, err
84 }
85 return &Templates{whoami, register, login, consent, changePassword, changePasswordSuccess}, nil
giolekva603e73a2021-10-22 14:46:45 +040086}
87
88type Server struct {
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040089 r *mux.Router
90 serv *http.Server
91 kratos string
92 hydra *HydraClient
93 tmpls *Templates
94 enableRegistration bool
giodd213152024-09-27 11:26:59 +020095 api *APIServer
96 defaultReturnTo string
giolekva603e73a2021-10-22 14:46:45 +040097}
98
giodd213152024-09-27 11:26:59 +020099func NewServer(
100 port int,
101 kratos string,
102 hydra *HydraClient,
103 tmpls *Templates,
104 enableRegistration bool,
105 api *APIServer,
106 defaultReturnTo string,
107) *Server {
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400108 r := mux.NewRouter()
109 serv := &http.Server{
110 Addr: fmt.Sprintf(":%d", port),
111 Handler: r,
112 }
giodd213152024-09-27 11:26:59 +0200113 return &Server{r, serv, kratos, hydra, tmpls, enableRegistration, api, defaultReturnTo}
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400114}
115
giolekva47031752021-11-12 14:34:33 +0400116func cacheControlWrapper(h http.Handler) http.Handler {
117 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118 // TODO(giolekva): enable caching
119 // w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
120 h.ServeHTTP(w, r)
121 })
122}
123
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400124func (s *Server) Start() error {
giolekva47031752021-11-12 14:34:33 +0400125 var staticFS = http.FS(static)
126 fs := http.FileServer(staticFS)
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400127 s.r.PathPrefix("/static/").Handler(cacheControlWrapper(fs))
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400128 if s.enableRegistration {
129 s.r.Path("/register").Methods(http.MethodGet).HandlerFunc(s.registerInitiate)
130 s.r.Path("/register").Methods(http.MethodPost).HandlerFunc(s.register)
131 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400132 s.r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
133 s.r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
134 s.r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
135 s.r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
136 s.r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
giob7df27f2026-07-28 10:36:17 +0400137 s.r.Path("/settings").Methods("POST").HandlerFunc(s.changePassword)
138 s.r.Path("/settings").Methods("GET").HandlerFunc(s.changePasswordForm)
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400139 s.r.Path("/").HandlerFunc(s.whoami)
140 return s.serv.ListenAndServe()
giolekva603e73a2021-10-22 14:46:45 +0400141}
142
143func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
144 jar, err := cookiejar.New(nil)
145 if err != nil {
146 return "", err
147 }
148 client := &http.Client{
149 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400150 Transport: &http.Transport{
151 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
152 },
giolekva603e73a2021-10-22 14:46:45 +0400153 }
giolekvadd750802021-11-07 13:24:21 +0400154 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400155 if err != nil {
156 return "", err
157 }
158 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400159 resp, err := client.Get(fmt.Sprintf(*kratos+"/self-service/"+flowType+"/flows?id=%s", flow))
giolekva603e73a2021-10-22 14:46:45 +0400160 if err != nil {
161 return "", err
162 }
163 respBody, err := ioutil.ReadAll(resp.Body)
164 if err != nil {
165 return "", err
166 }
167 token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
168 if err != nil {
169 return "", err
170 }
171 return token.String(), nil
172}
173
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400174func (s *Server) registerInitiate(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400175 if err := r.ParseForm(); err != nil {
176 http.Error(w, err.Error(), http.StatusInternalServerError)
177 return
178 }
179 flow, ok := r.Form["flow"]
180 if !ok {
181 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
182 return
183 }
184 csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
185 if err != nil {
186 http.Error(w, err.Error(), http.StatusInternalServerError)
187 return
188 }
giolekva603e73a2021-10-22 14:46:45 +0400189 w.Header().Set("Content-Type", "text/html")
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400190 if err := s.tmpls.Register.Execute(w, csrfToken); err != nil {
giolekva603e73a2021-10-22 14:46:45 +0400191 http.Error(w, err.Error(), http.StatusInternalServerError)
192 return
193 }
194}
195
196type regReq struct {
197 CSRFToken string `json:"csrf_token"`
198 Method string `json:"method"`
199 Password string `json:"password"`
200 Traits regReqTraits `json:"traits"`
201}
202
203type regReqTraits struct {
204 Username string `json:"username"`
205}
206
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400207func (s *Server) register(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400208 if err := r.ParseForm(); err != nil {
209 http.Error(w, err.Error(), http.StatusInternalServerError)
210 return
211 }
212 flow, ok := r.Form["flow"]
213 if !ok {
214 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
215 return
216 }
217 req := regReq{
218 CSRFToken: r.FormValue("csrf_token"),
219 Method: "password",
220 Password: r.FormValue("password"),
221 Traits: regReqTraits{
222 Username: r.FormValue("username"),
223 },
224 }
225 var reqBody bytes.Buffer
226 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
227 http.Error(w, err.Error(), http.StatusInternalServerError)
228 return
229 }
230 if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
231 http.Error(w, err.Error(), http.StatusInternalServerError)
232 return
233 } else {
234 for _, c := range resp.Cookies() {
235 http.SetCookie(w, c)
236 }
237 http.Redirect(w, r, "/", http.StatusSeeOther)
238 }
239}
240
241// Login flow
242
gio038c9e12026-07-28 17:52:55 +0400243func clearLoginChallengeCookie(w http.ResponseWriter) {
244 http.SetCookie(w, &http.Cookie{
245 Name: "login_challenge",
246 Value: "",
247 Path: "/",
248 MaxAge: -1,
249 HttpOnly: true,
250 SameSite: http.SameSiteLaxMode,
251 })
252}
253
giolekva603e73a2021-10-22 14:46:45 +0400254func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
255 if err := r.ParseForm(); err != nil {
256 http.Error(w, err.Error(), http.StatusInternalServerError)
257 return
258 }
gio038c9e12026-07-28 17:52:55 +0400259 challenge, hasChallenge := r.Form["login_challenge"]
260 flow, hasFlow := r.Form["flow"]
261 if !hasChallenge && !hasFlow {
262 clearLoginChallengeCookie(w)
263 }
264 if hasChallenge {
giodd213152024-09-27 11:26:59 +0200265 _, username, err := getWhoAmIFromKratos(r.Cookies())
Giorgi Lekveishvili7016d882024-04-09 09:06:53 +0400266 if err != nil && err != ErrNotLoggedIn {
267 http.Error(w, err.Error(), http.StatusInternalServerError)
268 return
269 }
270 if err == nil {
271 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge[0], username)
272 if err != nil {
273 http.Error(w, err.Error(), http.StatusInternalServerError)
274 return
275 }
gio038c9e12026-07-28 17:52:55 +0400276 clearLoginChallengeCookie(w)
Giorgi Lekveishvili7016d882024-04-09 09:06:53 +0400277 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
278 return
279 }
giolekva788dc6e2021-10-25 20:40:53 +0400280 // TODO(giolekva): encrypt
281 http.SetCookie(w, &http.Cookie{
282 Name: "login_challenge",
283 Value: challenge[0],
gio038c9e12026-07-28 17:52:55 +0400284 Path: "/",
giolekva788dc6e2021-10-25 20:40:53 +0400285 HttpOnly: true,
gio038c9e12026-07-28 17:52:55 +0400286 SameSite: http.SameSiteLaxMode,
giolekva788dc6e2021-10-25 20:40:53 +0400287 })
giolekva788dc6e2021-10-25 20:40:53 +0400288 }
giodd213152024-09-27 11:26:59 +0200289 returnTo := r.FormValue("return_to")
290 if returnTo == "" && s.defaultReturnTo != "" {
291 returnTo = s.defaultReturnTo
292 }
gio038c9e12026-07-28 17:52:55 +0400293 if !hasFlow {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400294 addr := s.kratos + "/self-service/login/browser"
295 if returnTo != "" {
296 addr += fmt.Sprintf("?return_to=%s", returnTo)
297 }
298 http.Redirect(w, r, addr, http.StatusSeeOther)
giolekva603e73a2021-10-22 14:46:45 +0400299 return
300 }
301 csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
302 if err != nil {
303 http.Error(w, err.Error(), http.StatusInternalServerError)
304 return
305 }
giolekva603e73a2021-10-22 14:46:45 +0400306 w.Header().Set("Content-Type", "text/html")
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400307 if err := s.tmpls.Login.Execute(w, map[string]any{
308 "csrfToken": csrfToken,
309 "enableRegistration": s.enableRegistration,
310 }); err != nil {
giolekva603e73a2021-10-22 14:46:45 +0400311 http.Error(w, err.Error(), http.StatusInternalServerError)
312 return
313 }
314}
315
316type loginReq struct {
317 CSRFToken string `json:"csrf_token"`
318 Method string `json:"method"`
319 Password string `json:"password"`
320 Username string `json:"password_identifier"`
321}
322
323func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
324 jar, err := cookiejar.New(nil)
325 if err != nil {
326 return nil, err
327 }
328 client := &http.Client{
329 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400330 Transport: &http.Transport{
331 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
332 },
giolekva603e73a2021-10-22 14:46:45 +0400333 }
giolekvadd750802021-11-07 13:24:21 +0400334 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400335 if err != nil {
336 return nil, err
337 }
338 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400339 resp, err := client.Post(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
giolekva603e73a2021-10-22 14:46:45 +0400340 if err != nil {
341 return nil, err
342 }
343 return resp, nil
344}
345
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400346func postFormToKratos(flowType, flow string, cookies []*http.Cookie, data url.Values) (*http.Response, error) {
347 jar, err := cookiejar.New(nil)
348 if err != nil {
349 return nil, err
350 }
351 client := &http.Client{
352 Jar: jar,
353 Transport: &http.Transport{
354 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
355 },
356 CheckRedirect: func(req *http.Request, via []*http.Request) error {
357 return http.ErrUseLastResponse
358 },
359 }
360 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
361 if err != nil {
362 return nil, err
363 }
364 client.Jar.SetCookies(b, cookies)
365 resp, err := client.PostForm(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), data)
366 if err != nil {
367 return nil, err
368 }
369 return resp, nil
370}
371
giolekva603e73a2021-10-22 14:46:45 +0400372type logoutResp struct {
373 LogoutURL string `json:"logout_url"`
374}
375
376func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
377 jar, err := cookiejar.New(nil)
378 if err != nil {
379 return "", err
380 }
381 client := &http.Client{
382 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400383 Transport: &http.Transport{
384 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
385 },
giolekva603e73a2021-10-22 14:46:45 +0400386 }
giolekvadd750802021-11-07 13:24:21 +0400387 b, err := url.Parse(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400388 if err != nil {
389 return "", err
390 }
391 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400392 resp, err := client.Get(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400393 if err != nil {
394 return "", err
395 }
396 var lr logoutResp
397 if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
398 return "", err
399 }
400 return lr.LogoutURL, nil
401}
402
giodd213152024-09-27 11:26:59 +0200403func getWhoAmIFromKratos(cookies []*http.Cookie) (string, string, error) {
giolekva603e73a2021-10-22 14:46:45 +0400404 jar, err := cookiejar.New(nil)
405 if err != nil {
giodd213152024-09-27 11:26:59 +0200406 return "", "", err
giolekva603e73a2021-10-22 14:46:45 +0400407 }
408 client := &http.Client{
409 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400410 Transport: &http.Transport{
411 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
412 },
giolekva603e73a2021-10-22 14:46:45 +0400413 }
giolekvadd750802021-11-07 13:24:21 +0400414 b, err := url.Parse(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400415 if err != nil {
giodd213152024-09-27 11:26:59 +0200416 return "", "", err
giolekva603e73a2021-10-22 14:46:45 +0400417 }
418 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400419 resp, err := client.Get(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400420 if err != nil {
giodd213152024-09-27 11:26:59 +0200421 return "", "", err
giolekva603e73a2021-10-22 14:46:45 +0400422 }
423 respBody, err := ioutil.ReadAll(resp.Body)
424 if err != nil {
giodd213152024-09-27 11:26:59 +0200425 return "", "", err
giolekva603e73a2021-10-22 14:46:45 +0400426 }
427 username, err := regogo.Get(string(respBody), "input.identity.traits.username")
428 if err != nil {
giodd213152024-09-27 11:26:59 +0200429 return "", "", err
giolekva603e73a2021-10-22 14:46:45 +0400430 }
431 if username.String() == "" {
giodd213152024-09-27 11:26:59 +0200432 return "", "", ErrNotLoggedIn
giolekva603e73a2021-10-22 14:46:45 +0400433 }
giodd213152024-09-27 11:26:59 +0200434 id, err := regogo.Get(string(respBody), "input.identity.id")
435 if err != nil {
436 return "", "", err
437 }
438 if id.String() == "" {
439 return "", "", ErrNotLoggedIn
440 }
441 return id.String(), username.String(), nil
giolekva603e73a2021-10-22 14:46:45 +0400442
443}
444
giolekva788dc6e2021-10-25 20:40:53 +0400445func extractError(r io.Reader) error {
446 respBody, err := ioutil.ReadAll(r)
447 if err != nil {
448 return err
449 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400450 fmt.Printf("++ %s\n", respBody)
giolekva788dc6e2021-10-25 20:40:53 +0400451 t, err := regogo.Get(string(respBody), "input.ui.messages[0].type")
452 if err != nil {
453 return err
454 }
455 if t.String() == "error" {
456 message, err := regogo.Get(string(respBody), "input.ui.messages[0].text")
457 if err != nil {
458 return err
459 }
460 return errors.New(message.String())
461 }
462 return nil
463}
464
giolekva603e73a2021-10-22 14:46:45 +0400465func (s *Server) login(w http.ResponseWriter, r *http.Request) {
466 if err := r.ParseForm(); err != nil {
467 http.Error(w, err.Error(), http.StatusInternalServerError)
468 return
469 }
470 flow, ok := r.Form["flow"]
471 if !ok {
472 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
473 return
474 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400475 req := url.Values{
476 "csrf_token": []string{r.FormValue("csrf_token")},
477 "method": []string{"password"},
478 "password": []string{r.FormValue("password")},
479 "identifier": []string{r.FormValue("username")},
giolekva603e73a2021-10-22 14:46:45 +0400480 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400481 resp, err := postFormToKratos("login", flow[0], r.Cookies(), req)
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400482 var vv bytes.Buffer
483 io.Copy(&vv, resp.Body)
484 fmt.Println(vv.String())
giolekva788dc6e2021-10-25 20:40:53 +0400485 if err != nil {
giolekvaeb590282021-10-22 17:31:40 +0400486 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
giolekva788dc6e2021-10-25 20:40:53 +0400487 redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
giolekvaeb590282021-10-22 17:31:40 +0400488 if err != nil {
489 http.Error(w, err.Error(), http.StatusInternalServerError)
490 return
491 }
gio038c9e12026-07-28 17:52:55 +0400492 clearLoginChallengeCookie(w)
giolekva788dc6e2021-10-25 20:40:53 +0400493 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
494 return
giolekvaeb590282021-10-22 17:31:40 +0400495 }
giolekva788dc6e2021-10-25 20:40:53 +0400496 http.Error(w, err.Error(), http.StatusInternalServerError)
497 return
giolekva603e73a2021-10-22 14:46:45 +0400498 }
giolekva788dc6e2021-10-25 20:40:53 +0400499 for _, c := range resp.Cookies() {
500 http.SetCookie(w, c)
501 }
502 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
giodd213152024-09-27 11:26:59 +0200503 _, username, err := getWhoAmIFromKratos(resp.Cookies())
giolekva788dc6e2021-10-25 20:40:53 +0400504 if err != nil {
505 http.Error(w, err.Error(), http.StatusInternalServerError)
506 return
507 }
508 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
509 if err != nil {
510 http.Error(w, err.Error(), http.StatusInternalServerError)
511 return
512 }
gio038c9e12026-07-28 17:52:55 +0400513 clearLoginChallengeCookie(w)
giolekva788dc6e2021-10-25 20:40:53 +0400514 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
515 return
516 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400517 if resp.StatusCode == http.StatusSeeOther {
518 http.Redirect(w, r, resp.Header.Get("Location"), http.StatusSeeOther)
519 } else {
520 http.Redirect(w, r, "/", http.StatusSeeOther)
521 }
giolekva603e73a2021-10-22 14:46:45 +0400522}
523
524func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
525 if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
526 http.Error(w, err.Error(), http.StatusInternalServerError)
527 return
528 } else {
529 http.Redirect(w, r, logoutURL, http.StatusSeeOther)
530 }
531}
532
533func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
giodd213152024-09-27 11:26:59 +0200534 if _, username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
giolekva603e73a2021-10-22 14:46:45 +0400535 if errors.Is(err, ErrNotLoggedIn) {
536 http.Redirect(w, r, "/login", http.StatusSeeOther)
537 return
538 }
539 http.Error(w, err.Error(), http.StatusInternalServerError)
540 } else {
541 if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
542 http.Error(w, err.Error(), http.StatusInternalServerError)
543 }
544 }
545}
546
giolekva788dc6e2021-10-25 20:40:53 +0400547// TODO(giolekva): verify if logged in
548func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
549 if err := r.ParseForm(); err != nil {
550 http.Error(w, err.Error(), http.StatusBadRequest)
551 return
552 }
553 challenge, ok := r.Form["consent_challenge"]
554 if !ok {
555 http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
556 return
557 }
558 consent, err := s.hydra.GetConsentChallenge(challenge[0])
559 if err != nil {
560 http.Error(w, err.Error(), http.StatusInternalServerError)
561 return
562 }
giodd213152024-09-27 11:26:59 +0200563 _, username, err := getWhoAmIFromKratos(r.Cookies())
Giorgi Lekveishvili1f2c1c52024-04-12 07:17:58 +0400564 if err != nil {
giolekva788dc6e2021-10-25 20:40:53 +0400565 http.Error(w, err.Error(), http.StatusInternalServerError)
566 return
567 }
Giorgi Lekveishvili1f2c1c52024-04-12 07:17:58 +0400568 acceptedScopes := consent.RequestedScopes
569 idToken := map[string]string{
570 "username": username,
571 "email": username + "@" + *emailDomain,
572 }
573 // TODO(gio): is auto consent safe? should such behaviour be configurable?
574 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
575 http.Error(w, err.Error(), http.StatusInternalServerError)
576 } else {
577 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
578 }
579 // w.Header().Set("Content-Type", "text/html")
580 // if err := s.tmpls.Consent.Execute(w, consent.RequestedScopes); err != nil {
581 // http.Error(w, err.Error(), http.StatusInternalServerError)
582 // return
583 // }
giolekva788dc6e2021-10-25 20:40:53 +0400584}
585
586func (s *Server) processConsent(w http.ResponseWriter, r *http.Request) {
587 if err := r.ParseForm(); err != nil {
588 http.Error(w, err.Error(), http.StatusBadRequest)
589 return
590 }
giodd213152024-09-27 11:26:59 +0200591 _, username, err := getWhoAmIFromKratos(r.Cookies())
giolekva788dc6e2021-10-25 20:40:53 +0400592 if err != nil {
593 http.Error(w, err.Error(), http.StatusInternalServerError)
594 return
595 }
596 if _, accepted := r.Form["allow"]; accepted {
597 acceptedScopes, _ := r.Form["scope"]
598 idToken := map[string]string{
599 "username": username,
giolekvadd750802021-11-07 13:24:21 +0400600 "email": username + "@" + *emailDomain,
giolekva788dc6e2021-10-25 20:40:53 +0400601 }
602 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
603 http.Error(w, err.Error(), http.StatusInternalServerError)
604 } else {
605 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
606 }
607 return
608 } else {
609 // TODO(giolekva): implement rejection logic
610 }
611}
612
giodd213152024-09-27 11:26:59 +0200613type changePasswordData struct {
614 Username string
615 Password string
giob7df27f2026-07-28 10:36:17 +0400616 CSRFToken string
617 FormAction string
giodd213152024-09-27 11:26:59 +0200618 PasswordErrors []ValidationError
619}
620
621func (s *Server) changePasswordForm(w http.ResponseWriter, r *http.Request) {
giob7df27f2026-07-28 10:36:17 +0400622 flow := r.FormValue("flow")
623 if flow == "" {
624 http.Redirect(w, r, s.kratos+"/self-service/settings/browser", http.StatusSeeOther)
giodd213152024-09-27 11:26:59 +0200625 return
626 }
giob7df27f2026-07-28 10:36:17 +0400627 _, username, err := getWhoAmIFromKratos(r.Cookies())
628 if err != nil {
629 http.Error(w, err.Error(), http.StatusInternalServerError)
630 return
631 }
632 csrfToken, err := getCSRFToken("settings", flow, r.Cookies())
633 if err != nil {
634 http.Error(w, err.Error(), http.StatusInternalServerError)
635 return
636 }
637 if err := s.tmpls.ChangePassword.Execute(w, changePasswordData{Username: username, CSRFToken: csrfToken, FormAction: r.URL.Path + "?flow=" + url.QueryEscape(flow)}); err != nil {
giodd213152024-09-27 11:26:59 +0200638 http.Error(w, err.Error(), http.StatusInternalServerError)
639 return
640 }
641}
642
643func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
644 if err := r.ParseForm(); err != nil {
645 http.Error(w, err.Error(), http.StatusBadRequest)
646 return
647 }
giob7df27f2026-07-28 10:36:17 +0400648 flow := r.FormValue("flow")
649 if flow == "" {
650 http.Redirect(w, r, s.kratos+"/self-service/settings/browser", http.StatusSeeOther)
651 return
652 }
giodd213152024-09-27 11:26:59 +0200653 password := r.FormValue("password")
giob7df27f2026-07-28 10:36:17 +0400654 _, username, err := getWhoAmIFromKratos(r.Cookies())
giodd213152024-09-27 11:26:59 +0200655 if err != nil {
giob7df27f2026-07-28 10:36:17 +0400656 http.Error(w, err.Error(), http.StatusInternalServerError)
657 return
658 }
659 if verr := validatePassword(password); len(verr) > 0 {
660 if err := s.tmpls.ChangePassword.Execute(w, changePasswordData{Username: username, Password: password, CSRFToken: r.FormValue("csrf_token"), FormAction: r.URL.Path + "?flow=" + url.QueryEscape(flow), PasswordErrors: verr}); err != nil {
giodd213152024-09-27 11:26:59 +0200661 http.Error(w, err.Error(), http.StatusInternalServerError)
662 }
663 return
664 }
giob7df27f2026-07-28 10:36:17 +0400665 resp, err := postFormToKratos("settings", flow, r.Cookies(), url.Values{
666 "csrf_token": {r.FormValue("csrf_token")},
667 "method": {"password"},
668 "password": {password},
669 })
670 if err != nil {
giodd213152024-09-27 11:26:59 +0200671 http.Error(w, err.Error(), http.StatusInternalServerError)
giob7df27f2026-07-28 10:36:17 +0400672 return
673 }
674 defer resp.Body.Close()
675 if resp.StatusCode >= http.StatusBadRequest {
676 if err := extractError(resp.Body); err != nil {
677 if renderErr := s.tmpls.ChangePassword.Execute(w, changePasswordData{Username: username, Password: password, CSRFToken: r.FormValue("csrf_token"), FormAction: r.URL.Path + "?flow=" + url.QueryEscape(flow), PasswordErrors: []ValidationError{{Field: "password", Message: err.Error()}}}); renderErr != nil {
678 http.Error(w, renderErr.Error(), http.StatusInternalServerError)
679 }
giodd213152024-09-27 11:26:59 +0200680 return
681 }
giob7df27f2026-07-28 10:36:17 +0400682 http.Error(w, "password change failed", resp.StatusCode)
683 return
684 }
685 if err := s.tmpls.ChangePasswordSuccess.Execute(w, nil); err != nil {
686 http.Error(w, err.Error(), http.StatusInternalServerError)
687 return
giodd213152024-09-27 11:26:59 +0200688 }
689}
690
giolekva603e73a2021-10-22 14:46:45 +0400691func main() {
692 flag.Parse()
693 t, err := ParseTemplates(tmpls)
694 if err != nil {
695 log.Fatal(err)
696 }
giodd213152024-09-27 11:26:59 +0200697 api := NewAPIServer(*apiPort, *kratosAPI)
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400698 go func() {
giodd213152024-09-27 11:26:59 +0200699 log.Fatal(api.Start())
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400700 }()
701 func() {
702 s := NewServer(
703 *port,
704 *kratos,
705 NewHydraClient(*hydra),
706 t,
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400707 *enableRegistration,
giodd213152024-09-27 11:26:59 +0200708 api,
709 *defaultReturnTo,
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400710 )
711 log.Fatal(s.Start())
712 }()
giolekva603e73a2021-10-22 14:46:45 +0400713}