blob: 244a9b0cf12ad7aa5ab68a4671e4f6c214774812 [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"
gioe71b12b2026-07-29 10:02:37 +040018 "time"
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")
giolekva788dc6e2021-10-25 20:40:53 +040026var hydra = flag.String("hydra", "hydra.pcloud", "Hydra admin server address")
giolekvadd750802021-11-07 13:24:21 +040027var emailDomain = flag.String("email-domain", "lekva.me", "Email domain")
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040028var apiPort = flag.Int("api-port", 8081, "API Port to listen on")
29var kratosAPI = flag.String("kratos-api", "", "Kratos API address")
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040030var enableRegistration = flag.Bool("enable-registration", false, "If true account registration will be enabled")
giodd213152024-09-27 11:26:59 +020031var defaultReturnTo = flag.String("default-return-to", "", "Default redirect address after login")
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040032
giolekva603e73a2021-10-22 14:46:45 +040033var ErrNotLoggedIn = errors.New("Not logged in")
34
35//go:embed templates/*
36var tmpls embed.FS
37
giolekva47031752021-11-12 14:34:33 +040038//go:embed static
39var static embed.FS
40
giolekva603e73a2021-10-22 14:46:45 +040041type Templates struct {
giodd213152024-09-27 11:26:59 +020042 WhoAmI *template.Template
43 Register *template.Template
44 Login *template.Template
giodd213152024-09-27 11:26:59 +020045 ChangePassword *template.Template
46 ChangePasswordSuccess *template.Template
gioe71b12b2026-07-29 10:02:37 +040047 Error *template.Template
giolekva603e73a2021-10-22 14:46:45 +040048}
49
50func ParseTemplates(fs embed.FS) (*Templates, error) {
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040051 base, err := template.ParseFS(fs, "templates/base.html")
giolekva788dc6e2021-10-25 20:40:53 +040052 if err != nil {
53 return nil, err
54 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040055 parse := func(path string) (*template.Template, error) {
56 if b, err := base.Clone(); err != nil {
57 return nil, err
58 } else {
59 return b.ParseFS(fs, path)
60 }
61 }
62 whoami, err := parse("templates/whoami.html")
giolekva603e73a2021-10-22 14:46:45 +040063 if err != nil {
64 return nil, err
65 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040066 register, err := parse("templates/register.html")
giolekva603e73a2021-10-22 14:46:45 +040067 if err != nil {
68 return nil, err
69 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040070 login, err := parse("templates/login.html")
giolekva603e73a2021-10-22 14:46:45 +040071 if err != nil {
72 return nil, err
73 }
giodd213152024-09-27 11:26:59 +020074 changePassword, err := parse("templates/change-password.html")
75 if err != nil {
76 return nil, err
77 }
78 changePasswordSuccess, err := parse("templates/change-password-success.html")
79 if err != nil {
80 return nil, err
81 }
gioe71b12b2026-07-29 10:02:37 +040082 errorPage, err := parse("templates/error.html")
83 if err != nil {
84 return nil, err
85 }
86 return &Templates{whoami, register, login, changePassword, changePasswordSuccess, errorPage}, nil
87}
88
89const (
90 invalidLoginMessage = "Username or password is incorrect."
91 duplicateRegistrationMessage = "Username is not available."
92 expiredFlowMessage = "This form expired. Please try again."
93 registrationRejectedMessage = "Registration could not be completed. Please review your details and try again."
94 passwordChangeRejectedMessage = "Password could not be changed. Please choose a different password and try again."
95 authenticationUnavailableMessage = "Authentication service is temporarily unavailable. Please try again."
96
97 authNoticeCookieName = "auth_ui_notice"
98 authNoticeLoginInvalid = "login_invalid"
99 authNoticeFlowExpired = "flow_expired"
100)
101
102var errFlowExpired = errors.New("self-service flow expired")
103
104type LoginPageData struct {
105 FormAction string
106 CSRFToken string
107 EnableRegistration bool
108 GeneralNotice string
109}
110
111type RegisterPageData struct {
112 FormAction string
113 CSRFToken string
114 Username string
115 UsernameErrors []ValidationError
116 PasswordErrors []ValidationError
117 GeneralError string
118}
119
120type ChangePasswordPageData struct {
121 Username string
122 CSRFToken string
123 FormAction string
124 PasswordErrors []ValidationError
125 GeneralError string
126}
127
128type AccountPageData struct {
129 Username string
130}
131
132type ErrorPageData struct {
133 Title string
134 Message string
135 Status int
136 RecoveryHref string
137 RecoveryText string
138}
139
140type oryFlowResponse struct {
141 ID string `json:"id"`
142 UI struct {
143 Nodes []struct {
144 Attributes struct {
145 Name string `json:"name"`
146 Value json.RawMessage `json:"value"`
147 } `json:"attributes"`
148 } `json:"nodes"`
149 Messages []struct {
150 ID int64 `json:"id"`
151 } `json:"messages"`
152 } `json:"ui"`
153}
154
155type oryErrorResponse struct {
156 Error struct {
157 ID string `json:"id"`
158 } `json:"error"`
giolekva603e73a2021-10-22 14:46:45 +0400159}
160
161type Server struct {
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400162 r *mux.Router
163 serv *http.Server
164 kratos string
165 hydra *HydraClient
166 tmpls *Templates
167 enableRegistration bool
giodd213152024-09-27 11:26:59 +0200168 api *APIServer
169 defaultReturnTo string
giolekva603e73a2021-10-22 14:46:45 +0400170}
171
giodd213152024-09-27 11:26:59 +0200172func NewServer(
173 port int,
174 kratos string,
175 hydra *HydraClient,
176 tmpls *Templates,
177 enableRegistration bool,
178 api *APIServer,
179 defaultReturnTo string,
180) *Server {
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400181 r := mux.NewRouter()
182 serv := &http.Server{
183 Addr: fmt.Sprintf(":%d", port),
184 Handler: r,
185 }
giodd213152024-09-27 11:26:59 +0200186 return &Server{r, serv, kratos, hydra, tmpls, enableRegistration, api, defaultReturnTo}
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400187}
188
gioe71b12b2026-07-29 10:02:37 +0400189func executeTemplate(tmpl *template.Template, data any) ([]byte, error) {
190 var page bytes.Buffer
191 if err := tmpl.Execute(&page, data); err != nil {
192 return nil, err
193 }
194 return page.Bytes(), nil
195}
196
197func writeTemplate(w http.ResponseWriter, status int, page []byte) {
198 w.Header().Set("Content-Type", "text/html; charset=utf-8")
199 w.WriteHeader(status)
200 _, _ = w.Write(page)
201}
202
203func renderTemplate(w http.ResponseWriter, tmpl *template.Template, status int, data any) {
204 page, err := executeTemplate(tmpl, data)
205 if err != nil {
206 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
207 return
208 }
209 writeTemplate(w, status, page)
210}
211
212func (s *Server) renderDependencyError(w http.ResponseWriter, context string) {
213 data := ErrorPageData{Message: authenticationUnavailableMessage, Status: http.StatusBadGateway}
214 switch context {
215 case "login":
216 data.Title = "Authentication unavailable"
217 data.RecoveryHref = "/login"
218 data.RecoveryText = "Try signing in again"
219 case "registration":
220 data.Title = "Registration unavailable"
221 if s.enableRegistration {
222 data.RecoveryHref = "/register"
223 data.RecoveryText = "Try registration again"
224 } else {
225 data.RecoveryHref = "/login"
226 data.RecoveryText = "Go to sign in"
227 }
228 default:
229 data.Title = "Account unavailable"
230 data.RecoveryHref = "/"
231 data.RecoveryText = "Back to account"
232 }
233 renderTemplate(w, s.tmpls.Error, data.Status, data)
234}
235
236func setAuthNotice(w http.ResponseWriter, code string) {
237 if code != authNoticeLoginInvalid && code != authNoticeFlowExpired {
238 return
239 }
240 http.SetCookie(w, &http.Cookie{
241 Name: authNoticeCookieName,
242 Value: code,
243 Path: "/",
244 MaxAge: 120,
245 Expires: time.Now().Add(2 * time.Minute),
246 HttpOnly: true,
247 SameSite: http.SameSiteLaxMode,
248 })
249}
250
251func clearAuthNotice(w http.ResponseWriter) {
252 http.SetCookie(w, &http.Cookie{
253 Name: authNoticeCookieName,
254 Value: "",
255 Path: "/",
256 MaxAge: -1,
257 HttpOnly: true,
258 SameSite: http.SameSiteLaxMode,
259 })
260}
261
262func pendingAuthNotice(r *http.Request, accepted ...string) (code string, clear bool) {
263 cookie, err := r.Cookie(authNoticeCookieName)
264 if err != nil {
265 return "", false
266 }
267 for _, acceptedCode := range accepted {
268 if cookie.Value == acceptedCode {
269 return cookie.Value, true
270 }
271 }
272 if cookie.Value != authNoticeLoginInvalid && cookie.Value != authNoticeFlowExpired {
273 return "", true
274 }
275 return "", false
276}
277
278func parseOryErrorID(body []byte) string {
279 var response oryErrorResponse
280 if json.Unmarshal(body, &response) != nil {
281 return ""
282 }
283 return response.Error.ID
284}
285
286func parseRetryFlow(body []byte) (flowID, csrfToken string, duplicate bool, err error) {
287 var response oryFlowResponse
288 if json.Unmarshal(body, &response) != nil || response.ID == "" {
289 return "", "", false, errors.New("invalid retry flow")
290 }
291 csrfCount := 0
292 for _, node := range response.UI.Nodes {
293 if node.Attributes.Name != "csrf_token" {
294 continue
295 }
296 csrfCount++
297 if json.Unmarshal(node.Attributes.Value, &csrfToken) != nil || csrfToken == "" {
298 return "", "", false, errors.New("invalid retry csrf token")
299 }
300 }
301 if csrfCount != 1 {
302 return "", "", false, errors.New("invalid retry csrf token count")
303 }
304 for _, message := range response.UI.Messages {
305 if message.ID == 4000007 {
306 duplicate = true
307 }
308 }
309 return response.ID, csrfToken, duplicate, nil
310}
311
312func readResponseBody(resp *http.Response) ([]byte, error) {
313 defer resp.Body.Close()
314 return ioutil.ReadAll(resp.Body)
315}
316
317func localFlowAction(path, flow string) string {
318 return path + "?flow=" + url.QueryEscape(flow)
319}
320
321func (s *Server) restartFlow(w http.ResponseWriter, r *http.Request, flowType string) {
322 setAuthNotice(w, authNoticeFlowExpired)
323 addr := s.kratos + "/self-service/" + flowType + "/browser"
324 if flowType == "login" {
325 returnTo := r.FormValue("return_to")
326 if returnTo == "" && s.defaultReturnTo != "" {
327 returnTo = s.defaultReturnTo
328 }
329 if returnTo != "" {
330 addr += fmt.Sprintf("?return_to=%s", returnTo)
331 }
332 }
333 http.Redirect(w, r, addr, http.StatusSeeOther)
334}
335
giolekva47031752021-11-12 14:34:33 +0400336func cacheControlWrapper(h http.Handler) http.Handler {
337 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
338 // TODO(giolekva): enable caching
339 // w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
340 h.ServeHTTP(w, r)
341 })
342}
343
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400344func (s *Server) Start() error {
giolekva47031752021-11-12 14:34:33 +0400345 var staticFS = http.FS(static)
346 fs := http.FileServer(staticFS)
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400347 s.r.PathPrefix("/static/").Handler(cacheControlWrapper(fs))
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400348 if s.enableRegistration {
349 s.r.Path("/register").Methods(http.MethodGet).HandlerFunc(s.registerInitiate)
350 s.r.Path("/register").Methods(http.MethodPost).HandlerFunc(s.register)
351 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400352 s.r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
353 s.r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
354 s.r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
355 s.r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
356 s.r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
giob7df27f2026-07-28 10:36:17 +0400357 s.r.Path("/settings").Methods("POST").HandlerFunc(s.changePassword)
358 s.r.Path("/settings").Methods("GET").HandlerFunc(s.changePasswordForm)
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400359 s.r.Path("/").HandlerFunc(s.whoami)
360 return s.serv.ListenAndServe()
giolekva603e73a2021-10-22 14:46:45 +0400361}
362
363func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
364 jar, err := cookiejar.New(nil)
365 if err != nil {
366 return "", err
367 }
368 client := &http.Client{
369 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400370 Transport: &http.Transport{
371 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
372 },
giolekva603e73a2021-10-22 14:46:45 +0400373 }
giolekvadd750802021-11-07 13:24:21 +0400374 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400375 if err != nil {
376 return "", err
377 }
378 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400379 resp, err := client.Get(fmt.Sprintf(*kratos+"/self-service/"+flowType+"/flows?id=%s", flow))
giolekva603e73a2021-10-22 14:46:45 +0400380 if err != nil {
381 return "", err
382 }
gioe71b12b2026-07-29 10:02:37 +0400383 respBody, err := readResponseBody(resp)
giolekva603e73a2021-10-22 14:46:45 +0400384 if err != nil {
385 return "", err
386 }
gioe71b12b2026-07-29 10:02:37 +0400387 if resp.StatusCode == http.StatusGone && parseOryErrorID(respBody) == "self_service_flow_expired" {
388 return "", errFlowExpired
389 }
390 if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
391 return "", errors.New("flow fetch failed")
392 }
393 _, token, _, err := parseRetryFlow(respBody)
giolekva603e73a2021-10-22 14:46:45 +0400394 if err != nil {
395 return "", err
396 }
gioe71b12b2026-07-29 10:02:37 +0400397 return token, nil
giolekva603e73a2021-10-22 14:46:45 +0400398}
399
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400400func (s *Server) registerInitiate(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400401 if err := r.ParseForm(); err != nil {
gioe71b12b2026-07-29 10:02:37 +0400402 http.Error(w, "Bad request.", http.StatusBadRequest)
giolekva603e73a2021-10-22 14:46:45 +0400403 return
404 }
gioe71b12b2026-07-29 10:02:37 +0400405 flow := r.FormValue("flow")
406 if flow == "" {
giolekva603e73a2021-10-22 14:46:45 +0400407 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
408 return
409 }
gioe71b12b2026-07-29 10:02:37 +0400410 csrfToken, err := getCSRFToken("registration", flow, r.Cookies())
411 if errors.Is(err, errFlowExpired) {
412 s.restartFlow(w, r, "registration")
413 return
414 }
giolekva603e73a2021-10-22 14:46:45 +0400415 if err != nil {
gioe71b12b2026-07-29 10:02:37 +0400416 s.renderDependencyError(w, "registration")
giolekva603e73a2021-10-22 14:46:45 +0400417 return
418 }
gioe71b12b2026-07-29 10:02:37 +0400419 notice := ""
420 noticeCode, clearNotice := pendingAuthNotice(r, authNoticeFlowExpired)
421 if noticeCode == authNoticeFlowExpired {
422 notice = expiredFlowMessage
423 }
424 page, err := executeTemplate(s.tmpls.Register, RegisterPageData{
425 FormAction: localFlowAction(r.URL.Path, flow),
426 CSRFToken: csrfToken,
427 GeneralError: notice,
428 })
429 if err != nil {
430 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
giolekva603e73a2021-10-22 14:46:45 +0400431 return
432 }
gioe71b12b2026-07-29 10:02:37 +0400433 if clearNotice {
434 clearAuthNotice(w)
435 }
436 writeTemplate(w, http.StatusOK, page)
giolekva603e73a2021-10-22 14:46:45 +0400437}
438
439type regReq struct {
440 CSRFToken string `json:"csrf_token"`
441 Method string `json:"method"`
442 Password string `json:"password"`
443 Traits regReqTraits `json:"traits"`
444}
445
446type regReqTraits struct {
447 Username string `json:"username"`
448}
449
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400450func (s *Server) register(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400451 if err := r.ParseForm(); err != nil {
gioe71b12b2026-07-29 10:02:37 +0400452 http.Error(w, "Bad request.", http.StatusBadRequest)
giolekva603e73a2021-10-22 14:46:45 +0400453 return
454 }
gioe71b12b2026-07-29 10:02:37 +0400455 flow := r.FormValue("flow")
456 if flow == "" {
giolekva603e73a2021-10-22 14:46:45 +0400457 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
458 return
459 }
gioe71b12b2026-07-29 10:02:37 +0400460 username := r.FormValue("username")
461 password := r.FormValue("password")
462 usernameErrors := validateUsername(username)
463 passwordErrors := validatePassword(password)
464 if len(usernameErrors)+len(passwordErrors) > 0 {
465 renderTemplate(w, s.tmpls.Register, http.StatusUnprocessableEntity, RegisterPageData{
466 FormAction: localFlowAction(r.URL.Path, flow),
467 CSRFToken: r.FormValue("csrf_token"),
468 Username: username,
469 UsernameErrors: usernameErrors,
470 PasswordErrors: passwordErrors,
471 })
472 return
473 }
giolekva603e73a2021-10-22 14:46:45 +0400474 req := regReq{
475 CSRFToken: r.FormValue("csrf_token"),
476 Method: "password",
gioe71b12b2026-07-29 10:02:37 +0400477 Password: password,
giolekva603e73a2021-10-22 14:46:45 +0400478 Traits: regReqTraits{
gioe71b12b2026-07-29 10:02:37 +0400479 Username: username,
giolekva603e73a2021-10-22 14:46:45 +0400480 },
481 }
482 var reqBody bytes.Buffer
483 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
gioe71b12b2026-07-29 10:02:37 +0400484 s.renderDependencyError(w, "registration")
giolekva603e73a2021-10-22 14:46:45 +0400485 return
486 }
gioe71b12b2026-07-29 10:02:37 +0400487 resp, err := postToKratos("registration", flow, r.Cookies(), &reqBody)
488 if err != nil {
489 s.renderDependencyError(w, "registration")
giolekva603e73a2021-10-22 14:46:45 +0400490 return
gioe71b12b2026-07-29 10:02:37 +0400491 }
492 for _, cookie := range resp.Cookies() {
493 http.SetCookie(w, cookie)
494 }
495 if resp.StatusCode < http.StatusBadRequest {
496 resp.Body.Close()
giolekva603e73a2021-10-22 14:46:45 +0400497 http.Redirect(w, r, "/", http.StatusSeeOther)
gioe71b12b2026-07-29 10:02:37 +0400498 return
giolekva603e73a2021-10-22 14:46:45 +0400499 }
gioe71b12b2026-07-29 10:02:37 +0400500 body, err := readResponseBody(resp)
501 if err != nil {
502 s.renderDependencyError(w, "registration")
503 return
504 }
505 if (resp.StatusCode == http.StatusGone && parseOryErrorID(body) == "self_service_flow_expired") ||
506 (resp.StatusCode == http.StatusForbidden && parseOryErrorID(body) == "security_csrf_violation") {
507 s.restartFlow(w, r, "registration")
508 return
509 }
510 if resp.StatusCode != http.StatusBadRequest {
511 s.renderDependencyError(w, "registration")
512 return
513 }
514 retryFlow, retryCSRF, duplicate, err := parseRetryFlow(body)
515 if err != nil {
516 s.renderDependencyError(w, "registration")
517 return
518 }
519 status := http.StatusUnprocessableEntity
520 generalError := registrationRejectedMessage
521 if duplicate {
522 status = http.StatusConflict
523 generalError = duplicateRegistrationMessage
524 }
525 renderTemplate(w, s.tmpls.Register, status, RegisterPageData{
526 FormAction: localFlowAction(r.URL.Path, retryFlow),
527 CSRFToken: retryCSRF,
528 Username: username,
529 GeneralError: generalError,
530 })
giolekva603e73a2021-10-22 14:46:45 +0400531}
532
533// Login flow
534
gio038c9e12026-07-28 17:52:55 +0400535func clearLoginChallengeCookie(w http.ResponseWriter) {
536 http.SetCookie(w, &http.Cookie{
537 Name: "login_challenge",
538 Value: "",
539 Path: "/",
540 MaxAge: -1,
541 HttpOnly: true,
542 SameSite: http.SameSiteLaxMode,
543 })
544}
545
giolekva603e73a2021-10-22 14:46:45 +0400546func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
547 if err := r.ParseForm(); err != nil {
gioe71b12b2026-07-29 10:02:37 +0400548 http.Error(w, "Bad request.", http.StatusBadRequest)
giolekva603e73a2021-10-22 14:46:45 +0400549 return
550 }
gio038c9e12026-07-28 17:52:55 +0400551 challenge, hasChallenge := r.Form["login_challenge"]
552 flow, hasFlow := r.Form["flow"]
553 if !hasChallenge && !hasFlow {
554 clearLoginChallengeCookie(w)
555 }
556 if hasChallenge {
giodd213152024-09-27 11:26:59 +0200557 _, username, err := getWhoAmIFromKratos(r.Cookies())
Giorgi Lekveishvili7016d882024-04-09 09:06:53 +0400558 if err != nil && err != ErrNotLoggedIn {
gioe71b12b2026-07-29 10:02:37 +0400559 s.renderDependencyError(w, "login")
Giorgi Lekveishvili7016d882024-04-09 09:06:53 +0400560 return
561 }
562 if err == nil {
563 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge[0], username)
564 if err != nil {
gioe71b12b2026-07-29 10:02:37 +0400565 s.renderDependencyError(w, "login")
Giorgi Lekveishvili7016d882024-04-09 09:06:53 +0400566 return
567 }
gio038c9e12026-07-28 17:52:55 +0400568 clearLoginChallengeCookie(w)
Giorgi Lekveishvili7016d882024-04-09 09:06:53 +0400569 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
570 return
571 }
giolekva788dc6e2021-10-25 20:40:53 +0400572 // TODO(giolekva): encrypt
573 http.SetCookie(w, &http.Cookie{
574 Name: "login_challenge",
575 Value: challenge[0],
gio038c9e12026-07-28 17:52:55 +0400576 Path: "/",
giolekva788dc6e2021-10-25 20:40:53 +0400577 HttpOnly: true,
gio038c9e12026-07-28 17:52:55 +0400578 SameSite: http.SameSiteLaxMode,
giolekva788dc6e2021-10-25 20:40:53 +0400579 })
giolekva788dc6e2021-10-25 20:40:53 +0400580 }
giodd213152024-09-27 11:26:59 +0200581 returnTo := r.FormValue("return_to")
582 if returnTo == "" && s.defaultReturnTo != "" {
583 returnTo = s.defaultReturnTo
584 }
gio038c9e12026-07-28 17:52:55 +0400585 if !hasFlow {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400586 addr := s.kratos + "/self-service/login/browser"
587 if returnTo != "" {
588 addr += fmt.Sprintf("?return_to=%s", returnTo)
589 }
590 http.Redirect(w, r, addr, http.StatusSeeOther)
giolekva603e73a2021-10-22 14:46:45 +0400591 return
592 }
593 csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
gioe71b12b2026-07-29 10:02:37 +0400594 if errors.Is(err, errFlowExpired) {
595 s.restartFlow(w, r, "login")
596 return
597 }
giolekva603e73a2021-10-22 14:46:45 +0400598 if err != nil {
gioe71b12b2026-07-29 10:02:37 +0400599 s.renderDependencyError(w, "login")
giolekva603e73a2021-10-22 14:46:45 +0400600 return
601 }
gioe71b12b2026-07-29 10:02:37 +0400602 notice := ""
603 noticeCode, clearNotice := pendingAuthNotice(r, authNoticeLoginInvalid, authNoticeFlowExpired)
604 switch noticeCode {
605 case authNoticeLoginInvalid:
606 notice = invalidLoginMessage
607 case authNoticeFlowExpired:
608 notice = expiredFlowMessage
609 }
610 page, err := executeTemplate(s.tmpls.Login, LoginPageData{
611 FormAction: localFlowAction(r.URL.Path, flow[0]),
612 CSRFToken: csrfToken,
613 EnableRegistration: s.enableRegistration,
614 GeneralNotice: notice,
615 })
616 if err != nil {
617 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
giolekva603e73a2021-10-22 14:46:45 +0400618 return
619 }
gioe71b12b2026-07-29 10:02:37 +0400620 if clearNotice {
621 clearAuthNotice(w)
622 }
623 writeTemplate(w, http.StatusOK, page)
giolekva603e73a2021-10-22 14:46:45 +0400624}
625
626type loginReq struct {
627 CSRFToken string `json:"csrf_token"`
628 Method string `json:"method"`
629 Password string `json:"password"`
630 Username string `json:"password_identifier"`
631}
632
633func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
634 jar, err := cookiejar.New(nil)
635 if err != nil {
636 return nil, err
637 }
638 client := &http.Client{
639 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400640 Transport: &http.Transport{
641 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
642 },
giolekva603e73a2021-10-22 14:46:45 +0400643 }
giolekvadd750802021-11-07 13:24:21 +0400644 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400645 if err != nil {
646 return nil, err
647 }
648 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400649 resp, err := client.Post(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
giolekva603e73a2021-10-22 14:46:45 +0400650 if err != nil {
651 return nil, err
652 }
653 return resp, nil
654}
655
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400656func postFormToKratos(flowType, flow string, cookies []*http.Cookie, data url.Values) (*http.Response, error) {
657 jar, err := cookiejar.New(nil)
658 if err != nil {
659 return nil, err
660 }
661 client := &http.Client{
662 Jar: jar,
663 Transport: &http.Transport{
664 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
665 },
666 CheckRedirect: func(req *http.Request, via []*http.Request) error {
667 return http.ErrUseLastResponse
668 },
669 }
670 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
671 if err != nil {
672 return nil, err
673 }
674 client.Jar.SetCookies(b, cookies)
675 resp, err := client.PostForm(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), data)
676 if err != nil {
677 return nil, err
678 }
679 return resp, nil
680}
681
giolekva603e73a2021-10-22 14:46:45 +0400682type logoutResp struct {
683 LogoutURL string `json:"logout_url"`
684}
685
686func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
687 jar, err := cookiejar.New(nil)
688 if err != nil {
689 return "", err
690 }
691 client := &http.Client{
692 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400693 Transport: &http.Transport{
694 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
695 },
giolekva603e73a2021-10-22 14:46:45 +0400696 }
giolekvadd750802021-11-07 13:24:21 +0400697 b, err := url.Parse(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400698 if err != nil {
699 return "", err
700 }
701 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400702 resp, err := client.Get(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400703 if err != nil {
704 return "", err
705 }
706 var lr logoutResp
707 if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
708 return "", err
709 }
710 return lr.LogoutURL, nil
711}
712
giodd213152024-09-27 11:26:59 +0200713func getWhoAmIFromKratos(cookies []*http.Cookie) (string, string, error) {
giolekva603e73a2021-10-22 14:46:45 +0400714 jar, err := cookiejar.New(nil)
715 if err != nil {
giodd213152024-09-27 11:26:59 +0200716 return "", "", err
giolekva603e73a2021-10-22 14:46:45 +0400717 }
718 client := &http.Client{
719 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400720 Transport: &http.Transport{
721 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
722 },
giolekva603e73a2021-10-22 14:46:45 +0400723 }
giolekvadd750802021-11-07 13:24:21 +0400724 b, err := url.Parse(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400725 if err != nil {
giodd213152024-09-27 11:26:59 +0200726 return "", "", err
giolekva603e73a2021-10-22 14:46:45 +0400727 }
728 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400729 resp, err := client.Get(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400730 if err != nil {
giodd213152024-09-27 11:26:59 +0200731 return "", "", err
giolekva603e73a2021-10-22 14:46:45 +0400732 }
733 respBody, err := ioutil.ReadAll(resp.Body)
734 if err != nil {
giodd213152024-09-27 11:26:59 +0200735 return "", "", err
giolekva603e73a2021-10-22 14:46:45 +0400736 }
737 username, err := regogo.Get(string(respBody), "input.identity.traits.username")
738 if err != nil {
giodd213152024-09-27 11:26:59 +0200739 return "", "", err
giolekva603e73a2021-10-22 14:46:45 +0400740 }
741 if username.String() == "" {
giodd213152024-09-27 11:26:59 +0200742 return "", "", ErrNotLoggedIn
giolekva603e73a2021-10-22 14:46:45 +0400743 }
giodd213152024-09-27 11:26:59 +0200744 id, err := regogo.Get(string(respBody), "input.identity.id")
745 if err != nil {
746 return "", "", err
747 }
748 if id.String() == "" {
749 return "", "", ErrNotLoggedIn
750 }
751 return id.String(), username.String(), nil
giolekva603e73a2021-10-22 14:46:45 +0400752
753}
754
gioe71b12b2026-07-29 10:02:37 +0400755func isRejectedLoginRedirect(resp *http.Response) bool {
756 if resp.StatusCode != http.StatusSeeOther {
757 return false
giolekva788dc6e2021-10-25 20:40:53 +0400758 }
gioe71b12b2026-07-29 10:02:37 +0400759 location, err := url.Parse(resp.Header.Get("Location"))
760 return err == nil && location.Path == "/login" && location.Query().Get("flow") != ""
giolekva788dc6e2021-10-25 20:40:53 +0400761}
762
giolekva603e73a2021-10-22 14:46:45 +0400763func (s *Server) login(w http.ResponseWriter, r *http.Request) {
764 if err := r.ParseForm(); err != nil {
gioe71b12b2026-07-29 10:02:37 +0400765 http.Error(w, "Bad request.", http.StatusBadRequest)
giolekva603e73a2021-10-22 14:46:45 +0400766 return
767 }
gioe71b12b2026-07-29 10:02:37 +0400768 flow := r.FormValue("flow")
769 if flow == "" {
giolekva603e73a2021-10-22 14:46:45 +0400770 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
771 return
772 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400773 req := url.Values{
gioe71b12b2026-07-29 10:02:37 +0400774 "csrf_token": {r.FormValue("csrf_token")},
775 "method": {"password"},
776 "password": {r.FormValue("password")},
777 "identifier": {r.FormValue("username")},
giolekva603e73a2021-10-22 14:46:45 +0400778 }
gioe71b12b2026-07-29 10:02:37 +0400779 resp, err := postFormToKratos("login", flow, r.Cookies(), req)
giolekva788dc6e2021-10-25 20:40:53 +0400780 if err != nil {
gioe71b12b2026-07-29 10:02:37 +0400781 s.renderDependencyError(w, "login")
giolekva788dc6e2021-10-25 20:40:53 +0400782 return
giolekva603e73a2021-10-22 14:46:45 +0400783 }
gioe71b12b2026-07-29 10:02:37 +0400784 defer resp.Body.Close()
785 var responseBody bytes.Buffer
786 _, _ = io.Copy(&responseBody, resp.Body)
787 fmt.Println(responseBody.String())
788 for _, cookie := range resp.Cookies() {
789 http.SetCookie(w, cookie)
790 }
791 if (resp.StatusCode == http.StatusGone && parseOryErrorID(responseBody.Bytes()) == "self_service_flow_expired") ||
792 (resp.StatusCode == http.StatusForbidden && parseOryErrorID(responseBody.Bytes()) == "security_csrf_violation") {
793 s.restartFlow(w, r, "login")
794 return
795 }
796 if isRejectedLoginRedirect(resp) {
797 setAuthNotice(w, authNoticeLoginInvalid)
798 http.Redirect(w, r, resp.Header.Get("Location"), http.StatusSeeOther)
799 return
800 }
801 if resp.StatusCode != http.StatusSeeOther {
802 s.renderDependencyError(w, "login")
803 return
giolekva788dc6e2021-10-25 20:40:53 +0400804 }
805 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
giodd213152024-09-27 11:26:59 +0200806 _, username, err := getWhoAmIFromKratos(resp.Cookies())
giolekva788dc6e2021-10-25 20:40:53 +0400807 if err != nil {
gioe71b12b2026-07-29 10:02:37 +0400808 s.renderDependencyError(w, "login")
giolekva788dc6e2021-10-25 20:40:53 +0400809 return
810 }
811 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
812 if err != nil {
gioe71b12b2026-07-29 10:02:37 +0400813 s.renderDependencyError(w, "login")
giolekva788dc6e2021-10-25 20:40:53 +0400814 return
815 }
gio038c9e12026-07-28 17:52:55 +0400816 clearLoginChallengeCookie(w)
giolekva788dc6e2021-10-25 20:40:53 +0400817 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
818 return
819 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400820 if resp.StatusCode == http.StatusSeeOther {
821 http.Redirect(w, r, resp.Header.Get("Location"), http.StatusSeeOther)
822 } else {
823 http.Redirect(w, r, "/", http.StatusSeeOther)
824 }
giolekva603e73a2021-10-22 14:46:45 +0400825}
826
827func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
828 if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
gioe71b12b2026-07-29 10:02:37 +0400829 s.renderDependencyError(w, "account")
giolekva603e73a2021-10-22 14:46:45 +0400830 return
831 } else {
832 http.Redirect(w, r, logoutURL, http.StatusSeeOther)
833 }
834}
835
836func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
giodd213152024-09-27 11:26:59 +0200837 if _, username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
giolekva603e73a2021-10-22 14:46:45 +0400838 if errors.Is(err, ErrNotLoggedIn) {
839 http.Redirect(w, r, "/login", http.StatusSeeOther)
840 return
841 }
gioe71b12b2026-07-29 10:02:37 +0400842 s.renderDependencyError(w, "account")
giolekva603e73a2021-10-22 14:46:45 +0400843 } else {
gioe71b12b2026-07-29 10:02:37 +0400844 renderTemplate(w, s.tmpls.WhoAmI, http.StatusOK, AccountPageData{Username: username})
giolekva603e73a2021-10-22 14:46:45 +0400845 }
846}
847
giolekva788dc6e2021-10-25 20:40:53 +0400848// TODO(giolekva): verify if logged in
849func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
850 if err := r.ParseForm(); err != nil {
851 http.Error(w, err.Error(), http.StatusBadRequest)
852 return
853 }
854 challenge, ok := r.Form["consent_challenge"]
855 if !ok {
856 http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
857 return
858 }
859 consent, err := s.hydra.GetConsentChallenge(challenge[0])
860 if err != nil {
861 http.Error(w, err.Error(), http.StatusInternalServerError)
862 return
863 }
giodd213152024-09-27 11:26:59 +0200864 _, username, err := getWhoAmIFromKratos(r.Cookies())
Giorgi Lekveishvili1f2c1c52024-04-12 07:17:58 +0400865 if err != nil {
giolekva788dc6e2021-10-25 20:40:53 +0400866 http.Error(w, err.Error(), http.StatusInternalServerError)
867 return
868 }
Giorgi Lekveishvili1f2c1c52024-04-12 07:17:58 +0400869 acceptedScopes := consent.RequestedScopes
870 idToken := map[string]string{
871 "username": username,
872 "email": username + "@" + *emailDomain,
873 }
874 // TODO(gio): is auto consent safe? should such behaviour be configurable?
875 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
876 http.Error(w, err.Error(), http.StatusInternalServerError)
877 } else {
878 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
879 }
giolekva788dc6e2021-10-25 20:40:53 +0400880}
881
882func (s *Server) processConsent(w http.ResponseWriter, r *http.Request) {
883 if err := r.ParseForm(); err != nil {
884 http.Error(w, err.Error(), http.StatusBadRequest)
885 return
886 }
giodd213152024-09-27 11:26:59 +0200887 _, username, err := getWhoAmIFromKratos(r.Cookies())
giolekva788dc6e2021-10-25 20:40:53 +0400888 if err != nil {
889 http.Error(w, err.Error(), http.StatusInternalServerError)
890 return
891 }
892 if _, accepted := r.Form["allow"]; accepted {
893 acceptedScopes, _ := r.Form["scope"]
894 idToken := map[string]string{
895 "username": username,
giolekvadd750802021-11-07 13:24:21 +0400896 "email": username + "@" + *emailDomain,
giolekva788dc6e2021-10-25 20:40:53 +0400897 }
898 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
899 http.Error(w, err.Error(), http.StatusInternalServerError)
900 } else {
901 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
902 }
903 return
904 } else {
905 // TODO(giolekva): implement rejection logic
906 }
907}
908
giodd213152024-09-27 11:26:59 +0200909func (s *Server) changePasswordForm(w http.ResponseWriter, r *http.Request) {
giob7df27f2026-07-28 10:36:17 +0400910 flow := r.FormValue("flow")
911 if flow == "" {
912 http.Redirect(w, r, s.kratos+"/self-service/settings/browser", http.StatusSeeOther)
giodd213152024-09-27 11:26:59 +0200913 return
914 }
giob7df27f2026-07-28 10:36:17 +0400915 _, username, err := getWhoAmIFromKratos(r.Cookies())
916 if err != nil {
gioe71b12b2026-07-29 10:02:37 +0400917 s.renderDependencyError(w, "account")
giob7df27f2026-07-28 10:36:17 +0400918 return
919 }
920 csrfToken, err := getCSRFToken("settings", flow, r.Cookies())
gioe71b12b2026-07-29 10:02:37 +0400921 if errors.Is(err, errFlowExpired) {
922 s.restartFlow(w, r, "settings")
923 return
924 }
giob7df27f2026-07-28 10:36:17 +0400925 if err != nil {
gioe71b12b2026-07-29 10:02:37 +0400926 s.renderDependencyError(w, "account")
giob7df27f2026-07-28 10:36:17 +0400927 return
928 }
gioe71b12b2026-07-29 10:02:37 +0400929 notice := ""
930 noticeCode, clearNotice := pendingAuthNotice(r, authNoticeFlowExpired)
931 if noticeCode == authNoticeFlowExpired {
932 notice = expiredFlowMessage
933 }
934 page, err := executeTemplate(s.tmpls.ChangePassword, ChangePasswordPageData{
935 Username: username,
936 CSRFToken: csrfToken,
937 FormAction: localFlowAction(r.URL.Path, flow),
938 GeneralError: notice,
939 })
940 if err != nil {
941 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
giodd213152024-09-27 11:26:59 +0200942 return
943 }
gioe71b12b2026-07-29 10:02:37 +0400944 if clearNotice {
945 clearAuthNotice(w)
946 }
947 writeTemplate(w, http.StatusOK, page)
giodd213152024-09-27 11:26:59 +0200948}
949
950func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
951 if err := r.ParseForm(); err != nil {
gioe71b12b2026-07-29 10:02:37 +0400952 http.Error(w, "Bad request.", http.StatusBadRequest)
giodd213152024-09-27 11:26:59 +0200953 return
954 }
giob7df27f2026-07-28 10:36:17 +0400955 flow := r.FormValue("flow")
956 if flow == "" {
957 http.Redirect(w, r, s.kratos+"/self-service/settings/browser", http.StatusSeeOther)
958 return
959 }
giodd213152024-09-27 11:26:59 +0200960 password := r.FormValue("password")
giob7df27f2026-07-28 10:36:17 +0400961 _, username, err := getWhoAmIFromKratos(r.Cookies())
giodd213152024-09-27 11:26:59 +0200962 if err != nil {
gioe71b12b2026-07-29 10:02:37 +0400963 s.renderDependencyError(w, "account")
giob7df27f2026-07-28 10:36:17 +0400964 return
965 }
gioe71b12b2026-07-29 10:02:37 +0400966 if passwordErrors := validatePassword(password); len(passwordErrors) > 0 {
967 renderTemplate(w, s.tmpls.ChangePassword, http.StatusUnprocessableEntity, ChangePasswordPageData{
968 Username: username,
969 CSRFToken: r.FormValue("csrf_token"),
970 FormAction: localFlowAction(r.URL.Path, flow),
971 PasswordErrors: passwordErrors,
972 })
giodd213152024-09-27 11:26:59 +0200973 return
974 }
giob7df27f2026-07-28 10:36:17 +0400975 resp, err := postFormToKratos("settings", flow, r.Cookies(), url.Values{
976 "csrf_token": {r.FormValue("csrf_token")},
977 "method": {"password"},
978 "password": {password},
979 })
980 if err != nil {
gioe71b12b2026-07-29 10:02:37 +0400981 s.renderDependencyError(w, "account")
giob7df27f2026-07-28 10:36:17 +0400982 return
983 }
gioe71b12b2026-07-29 10:02:37 +0400984 if resp.StatusCode < http.StatusBadRequest {
985 resp.Body.Close()
986 renderTemplate(w, s.tmpls.ChangePasswordSuccess, http.StatusOK, nil)
giob7df27f2026-07-28 10:36:17 +0400987 return
988 }
gioe71b12b2026-07-29 10:02:37 +0400989 body, err := readResponseBody(resp)
990 if err != nil {
991 s.renderDependencyError(w, "account")
giob7df27f2026-07-28 10:36:17 +0400992 return
giodd213152024-09-27 11:26:59 +0200993 }
gioe71b12b2026-07-29 10:02:37 +0400994 if (resp.StatusCode == http.StatusGone && parseOryErrorID(body) == "self_service_flow_expired") ||
995 (resp.StatusCode == http.StatusForbidden && parseOryErrorID(body) == "security_csrf_violation") {
996 s.restartFlow(w, r, "settings")
997 return
998 }
999 if resp.StatusCode != http.StatusBadRequest {
1000 s.renderDependencyError(w, "account")
1001 return
1002 }
1003 retryFlow, retryCSRF, _, err := parseRetryFlow(body)
1004 if err != nil {
1005 s.renderDependencyError(w, "account")
1006 return
1007 }
1008 renderTemplate(w, s.tmpls.ChangePassword, http.StatusUnprocessableEntity, ChangePasswordPageData{
1009 Username: username,
1010 CSRFToken: retryCSRF,
1011 FormAction: localFlowAction(r.URL.Path, retryFlow),
1012 GeneralError: passwordChangeRejectedMessage,
1013 })
giodd213152024-09-27 11:26:59 +02001014}
1015
giolekva603e73a2021-10-22 14:46:45 +04001016func main() {
1017 flag.Parse()
1018 t, err := ParseTemplates(tmpls)
1019 if err != nil {
1020 log.Fatal(err)
1021 }
giodd213152024-09-27 11:26:59 +02001022 api := NewAPIServer(*apiPort, *kratosAPI)
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +04001023 go func() {
giodd213152024-09-27 11:26:59 +02001024 log.Fatal(api.Start())
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +04001025 }()
1026 func() {
1027 s := NewServer(
1028 *port,
1029 *kratos,
1030 NewHydraClient(*hydra),
1031 t,
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +04001032 *enableRegistration,
giodd213152024-09-27 11:26:59 +02001033 api,
1034 *defaultReturnTo,
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +04001035 )
1036 log.Fatal(s.Start())
1037 }()
giolekva603e73a2021-10-22 14:46:45 +04001038}