diff --git a/.gitignore b/.gitignore index 5673f56..dc675de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /.idea/ -resourceapp.json -authapp.json +/*.toml +/certs/ diff --git a/cmd/runapp/main.go b/cmd/app/main.go similarity index 100% rename from cmd/runapp/main.go rename to cmd/app/main.go diff --git a/cmd/idp/main.go b/cmd/idp/main.go new file mode 100644 index 0000000..16ff6f2 --- /dev/null +++ b/cmd/idp/main.go @@ -0,0 +1,149 @@ +package main + +import ( + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "os" + "os/signal" + "sync/atomic" + "time" + + "github.com/go-openapi/runtime/client" + "github.com/gorilla/csrf" + "github.com/knadh/koanf" + "github.com/knadh/koanf/parsers/json" + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/providers/confmap" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/posflag" + hydra "github.com/ory/hydra-client-go/client" + log "github.com/sirupsen/logrus" + flag "github.com/spf13/pflag" + + commonHandlers "git.cacert.org/oidc_login/common/handlers" + "git.cacert.org/oidc_login/idp/handlers" + "git.cacert.org/oidc_login/idp/services" +) + +func main() { + f := flag.NewFlagSet("config", flag.ContinueOnError) + f.Usage = func() { + fmt.Println(f.FlagUsages()) + os.Exit(0) + } + f.StringSlice("conf", []string{"idp.toml"}, "path to one or more .toml files") + logger := log.New() + var err error + + if err = f.Parse(os.Args[1:]); err != nil { + logger.Fatal(err) + } + + config := koanf.New(".") + + cFiles, _ := f.GetStringSlice("conf") + for _, c := range cFiles { + if err := config.Load(file.Provider(c), toml.Parser()); err != nil { + logger.Fatalf("error loading config file: %s", err) + } + } + _ = config.Load(confmap.Provider(map[string]interface{}{ + "server.port": 3000, + "server.name": "login.cacert.localhost", + "server.key": "certs/idp.cacert.localhost.key", + "server.certificate": "certs/idp.cacert.localhost.crt.pem", + "admin.url": "https://hydra.cacert.localhost:4445/", + }, "."), nil) + _ = config.Load(file.Provider("idp.json"), json.Parser()) + if err := config.Load(posflag.Provider(f, ".", config), nil); err != nil { + logger.Fatalf("error loading configuration: %s", err) + } + + logger.Infoln("Server is starting") + ctx := context.Background() + + ctx = services.InitI18n(ctx, logger) + adminURL, err := url.Parse(config.MustString("admin.url")) + if err != nil { + logger.Fatalf("error parsing admin URL: %v", err) + } + clientTransport := client.New(adminURL.Host, adminURL.Path, []string{adminURL.Scheme}) + adminClient := hydra.New(clientTransport, nil) + + handlerContext := context.WithValue(ctx, handlers.CtxAdminClient, adminClient.Admin) + loginHandler, err := handlers.NewLoginHandler(handlerContext) + if err != nil { + logger.Fatalf("error initializing login handler: %v", err) + } + consentHandler := handlers.NewConsentHandler(handlerContext) + + router := http.NewServeMux() + router.Handle("/login", loginHandler) + router.Handle("/consent", consentHandler) + router.Handle("/health", commonHandlers.NewHealthHandler()) + + if err != nil { + logger.Fatal(err) + } + + csrfKey, err := base64.StdEncoding.DecodeString(config.MustString("security.csrf.key")) + if err != nil { + logger.Fatalf("could not parse CSRF key bytes: %v", err) + } + handler := csrf.Protect(csrfKey, csrf.Secure(true))(router) + + nextRequestId := func() string { + return fmt.Sprintf("%d", time.Now().UnixNano()) + } + + tlsConfig := &tls.Config{ + ServerName: config.String("server.name"), + MinVersion: tls.VersionTLS12, + } + server := &http.Server{ + Addr: fmt.Sprintf("%s:%d", config.String("server.name"), config.Int("server.port")), + Handler: commonHandlers.Tracing(nextRequestId)( + commonHandlers.Logging(logger)( + commonHandlers.EnableHSTS()(handler), + ), + ), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 15 * time.Second, + TLSConfig: tlsConfig, + } + + done := make(chan bool) + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + + go func() { + <-quit + logger.Infoln("Server is shutting down...") + atomic.StoreInt32(&commonHandlers.Healthy, 0) + + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + server.SetKeepAlivesEnabled(false) + if err := server.Shutdown(ctx); err != nil { + logger.Fatalf("Could not gracefully shutdown the server: %v\n", err) + } + close(done) + }() + + logger.Infof("Server is ready to handle requests at https://%s/", server.Addr) + atomic.StoreInt32(&commonHandlers.Healthy, 1) + if err := server.ListenAndServeTLS( + config.String("server.certificate"), config.String("server.key"), + ); err != nil && err != http.ErrServerClosed { + logger.Fatalf("Could not listen on %s: %v\n", server.Addr, err) + } + + <-done + logger.Infoln("Server stopped") +} diff --git a/common/handlers/observability.go b/common/handlers/observability.go new file mode 100644 index 0000000..3c34c42 --- /dev/null +++ b/common/handlers/observability.go @@ -0,0 +1,56 @@ +package handlers + +import ( + "context" + "net/http" + "sync/atomic" + + log "github.com/sirupsen/logrus" +) + +type key int + +const ( + requestIdKey key = iota +) + +func Logging(logger *log.Logger) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + requestId, ok := r.Context().Value(requestIdKey).(string) + if !ok { + requestId = "unknown" + } + logger.Infoln(requestId, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent()) + }() + next.ServeHTTP(w, r) + }) + } +} + +func Tracing(nextRequestId func() string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestId := r.Header.Get("X-Request-Id") + if requestId == "" { + requestId = nextRequestId() + } + ctx := context.WithValue(r.Context(), requestIdKey, requestId) + w.Header().Set("X-Request-Id", requestId) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +var Healthy int32 + +func NewHealthHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if atomic.LoadInt32(&Healthy) == 1 { + w.WriteHeader(http.StatusNoContent) + return + } + w.WriteHeader(http.StatusServiceUnavailable) + }) +} diff --git a/common/handlers/security.go b/common/handlers/security.go new file mode 100644 index 0000000..76050ba --- /dev/null +++ b/common/handlers/security.go @@ -0,0 +1,16 @@ +package handlers + +import ( + "fmt" + "net/http" + "time" +) + +func EnableHSTS() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", int((time.Hour*24*180).Seconds()))) + next.ServeHTTP(w, r) + }) + } +} diff --git a/go.mod b/go.mod index f3ce1e0..261d84d 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/nicksnyder/go-i18n/v2 v2.1.1 github.com/ory/hydra-client-go v1.8.5 github.com/sirupsen/logrus v1.4.2 + github.com/spf13/pflag v1.0.5 github.com/tidwall/pretty v1.0.1 // indirect golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 // indirect golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect diff --git a/idp/handlers/common.go b/idp/handlers/common.go new file mode 100644 index 0000000..dd8839a --- /dev/null +++ b/idp/handlers/common.go @@ -0,0 +1,7 @@ +package handlers + +type handlerContextKey int + +const ( + CtxAdminClient handlerContextKey = iota +) diff --git a/idp/handlers/consent.go b/idp/handlers/consent.go new file mode 100644 index 0000000..874884a --- /dev/null +++ b/idp/handlers/consent.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "context" + "net/http" + "time" + + "github.com/ory/hydra-client-go/client/admin" + "github.com/ory/hydra-client-go/models" + log "github.com/sirupsen/logrus" +) + +type consentHandler struct { + adminClient *admin.Client +} + +func (c *consentHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + consentChallenge := request.URL.Query().Get("consent_challenge") + consentRequest, err := c.adminClient.AcceptConsentRequest( + admin.NewAcceptConsentRequestParams().WithConsentChallenge(consentChallenge).WithBody( + &models.AcceptConsentRequest{ + GrantAccessTokenAudience: nil, + GrantScope: []string{"openid", "offline"}, + HandledAt: models.NullTime(time.Now()), + Remember: true, + RememberFor: 86400, + }).WithTimeout(time.Second * 10)) + if err != nil { + log.Panic(err) + } + writer.Header().Add("Location", *consentRequest.GetPayload().RedirectTo) + writer.WriteHeader(http.StatusFound) +} + +func NewConsentHandler(ctx context.Context) *consentHandler { + return &consentHandler{ + adminClient: ctx.Value(CtxAdminClient).(*admin.Client), + } +} diff --git a/idp/handlers/login.go b/idp/handlers/login.go new file mode 100644 index 0000000..e4f65fa --- /dev/null +++ b/idp/handlers/login.go @@ -0,0 +1,157 @@ +package handlers + +import ( + "context" + "fmt" + "html/template" + "net/http" + "time" + + "github.com/go-playground/form/v4" + "github.com/go-playground/validator/v10" + "github.com/gorilla/csrf" + "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/ory/hydra-client-go/client/admin" + "github.com/ory/hydra-client-go/models" + log "github.com/sirupsen/logrus" + + "git.cacert.org/oidc_login/idp/services" +) + +type loginHandler struct { + loginTemplate *template.Template + bundle *i18n.Bundle + messageCatalog map[string]*i18n.Message + adminClient *admin.Client +} + +type LoginInformation struct { + Email string `form:"email" validate:"required,email"` + Password string `form:"password" validate:"required"` +} + +func (h *loginHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + var err error + challenge := request.URL.Query().Get("login_challenge") + log.Debugf("received challenge %s\n", challenge) + validate := validator.New() + + switch request.Method { + case http.MethodGet: + // GET should render login form + + err = h.loginTemplate.Lookup("base").Execute(writer, map[string]interface{}{ + "Title": "Title", + csrf.TemplateTag: csrf.TemplateField(request), + "LabelEmail": "Email", + "LabelPassword": "Password", + "LabelLogin": "Login", + "errors": map[string]string{}, + }) + if err != nil { + log.Error(err) + http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + break + case http.MethodPost: + // POST should perform the action + var loginInfo LoginInformation + + // validate input + decoder := form.NewDecoder() + err = decoder.Decode(&loginInfo, request.Form) + if err != nil { + log.Error(err) + http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + err := validate.Struct(&loginInfo) + if err != nil { + errors := make(map[string]string) + for _, err := range err.(validator.ValidationErrors) { + accept := request.Header.Get("Accept-Language") + errors[err.Field()] = h.lookupErrorMessage(err.Tag(), err.Field(), err.Value(), i18n.NewLocalizer(h.bundle, accept)) + } + + err = h.loginTemplate.Lookup("base").Execute(writer, map[string]interface{}{ + "Title": "Title", + csrf.TemplateTag: csrf.TemplateField(request), + "LabelEmail": "Email", + "LabelPassword": "Password", + "LabelLogin": "Login", + "Email": loginInfo.Email, + "errors": errors, + }) + if err != nil { + log.Error(err) + http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + return + } + + // GET user data + // finish login and redirect to target + // TODO: get or generate a user id + subject := "a-user-with-an-id" + loginRequest, err := h.adminClient.AcceptLoginRequest( + admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{ + Acr: "no-creds", + Remember: true, + RememberFor: 0, + Subject: &subject, + }).WithTimeout(time.Second * 10)) + if err != nil { + log.Panic(err) + } + writer.Header().Add("Location", *loginRequest.GetPayload().RedirectTo) + writer.WriteHeader(http.StatusFound) + break + default: + http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } +} + +func (h *loginHandler) lookupErrorMessage(tag string, field string, value interface{}, l *i18n.Localizer) string { + var message *i18n.Message + message, ok := h.messageCatalog[fmt.Sprintf("%s-%s", field, tag)] + if !ok { + log.Infof("no specific error message %s-%s", field, tag) + message, ok = h.messageCatalog[tag] + if !ok { + log.Infof("no specific error message %s", tag) + message, ok = h.messageCatalog["unknown"] + if !ok { + log.Error("no default translation found") + return tag + } + } + } + + translation, err := l.Localize(&i18n.LocalizeConfig{ + DefaultMessage: message, + TemplateData: map[string]interface{}{ + "Value": value, + }, + }) + if err != nil { + log.Error(err) + return tag + } + return translation +} + +func NewLoginHandler(ctx context.Context) (*loginHandler, error) { + loginTemplate, err := template.ParseFiles("templates/base.html", "templates/login.html") + if err != nil { + return nil, err + } + return &loginHandler{ + loginTemplate: loginTemplate, + bundle: ctx.Value(services.CtxI18nBundle).(*i18n.Bundle), + messageCatalog: ctx.Value(services.CtxI18nCatalog).(map[string]*i18n.Message), + adminClient: ctx.Value(CtxAdminClient).(*admin.Client), + }, nil +} diff --git a/idp/services/i18n.go b/idp/services/i18n.go new file mode 100644 index 0000000..edb25dd --- /dev/null +++ b/idp/services/i18n.go @@ -0,0 +1,55 @@ +package services + +import ( + "context" + + "github.com/BurntSushi/toml" + "github.com/nicksnyder/go-i18n/v2/i18n" + log "github.com/sirupsen/logrus" + "golang.org/x/text/language" +) + +type contextKey int + +const ( + CtxI18nBundle contextKey = iota + CtxI18nCatalog +) + +func InitI18n(ctx context.Context, logger *log.Logger) context.Context { + bundle := i18n.NewBundle(language.English) + bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + _, err := bundle.LoadMessageFile("de.toml") + if err != nil { + logger.Warnln("message bundle de.toml not found") + } + catalog := initMessageCatalog() + ctx = context.WithValue(ctx, CtxI18nBundle, bundle) + ctx = context.WithValue(ctx, CtxI18nCatalog, catalog) + return ctx +} + +func initMessageCatalog() map[string]*i18n.Message { + messageCatalog := make(map[string]*i18n.Message) + messageCatalog["unknown"] = &i18n.Message{ + ID: "ErrorUnknown", + Other: "Unknown error", + } + messageCatalog["email"] = &i18n.Message{ + ID: "ErrorEmail", + Other: "Please enter a valid email address.", + } + messageCatalog["Email-required"] = &i18n.Message{ + ID: "ErrorEmailRequired", + Other: "Please enter an email address.", + } + messageCatalog["required"] = &i18n.Message{ + ID: "ErrorRequired", + Other: "Please enter a value", + } + messageCatalog["Password-required"] = &i18n.Message{ + ID: "ErrorPasswordRequired", + Other: "Please enter a password.", + } + return messageCatalog +} diff --git a/main.go b/main.go deleted file mode 100644 index 0e0f540..0000000 --- a/main.go +++ /dev/null @@ -1,339 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "html/template" - "net/http" - "net/url" - "os" - "os/signal" - "sync/atomic" - "time" - - "github.com/BurntSushi/toml" - openApiClient "github.com/go-openapi/runtime/client" - "github.com/go-playground/form/v4" - "github.com/go-playground/validator/v10" - "github.com/gorilla/csrf" - "github.com/nicksnyder/go-i18n/v2/i18n" - "github.com/ory/hydra-client-go/client" - "github.com/ory/hydra-client-go/client/admin" - "github.com/ory/hydra-client-go/models" - log "github.com/sirupsen/logrus" - "golang.org/x/text/language" -) - -type key int - -const ( - requestIdKey key = iota -) - -var ( - adminClient *client.OryHydra - listenAddr string - healthy int32 - validate *validator.Validate - bundle *i18n.Bundle - messageCatalog map[string]*i18n.Message -) - -func main() { - flag.StringVar(&listenAddr, "listen-addr", ":3000", "server listen address") - flag.Parse() - - logger := log.New() - logger.Infoln("Server is starting") - - validate = validator.New() - - router := http.NewServeMux() - loginHandler, err := NewLoginHandler() - router.Handle("/login", loginHandler) - router.Handle("/consent", NewConsentHandler()) - router.Handle("/health", health()) - - adminURL, err := url.Parse("https://localhost:4445/") - if err != nil { - log.Panic(err) - } - apiClient, err := openApiClient.TLSClient(openApiClient.TLSClientOptions{InsecureSkipVerify: true}) - if err != nil { - log.Panic(err) - } - clientTransport := openApiClient.NewWithClient(adminURL.Host, adminURL.Path, []string{adminURL.Scheme}, apiClient) - adminClient = client.New(clientTransport, nil) - - if err != nil { - log.Fatal(err) - } - - csrfKey := []byte("abcdefghijklmnopqrstuvwxyz012345") - handler := csrf.Protect(csrfKey)(router) - - nextRequestId := func() string { - return fmt.Sprintf("%d", time.Now().UnixNano()) - } - - bundle = i18n.NewBundle(language.English) - bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) - _, err = bundle.LoadMessageFile("de.toml") - if err != nil { - logger.Warnln("message bundle de.toml not found") - } - - initMessageCatalog() - - server := &http.Server{ - Addr: ":3000", - Handler: tracing(nextRequestId)(logging(logger)(handler)), - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - IdleTimeout: 15 * time.Second, - } - - done := make(chan bool) - quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt) - - go func() { - <-quit - logger.Infoln("Server is shutting down...") - atomic.StoreInt32(&healthy, 0) - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - server.SetKeepAlivesEnabled(false) - if err := server.Shutdown(ctx); err != nil { - logger.Fatalf("Could not gracefully shutdown the server: %v\n", err) - } - close(done) - }() - - logger.Infoln("Server is ready to handle requests at", listenAddr) - atomic.StoreInt32(&healthy, 1) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.Fatalf("Could not listen on %s: %v\n", listenAddr, err) - } - - <-done - logger.Infoln("Server stopped") -} - -func initMessageCatalog() { - messageCatalog = make(map[string]*i18n.Message) - messageCatalog["unknown"] = &i18n.Message{ - ID: "ErrorUnknown", - Other: "Unknown error", - } - messageCatalog["email"] = &i18n.Message{ - ID: "ErrorEmail", - Other: "Please enter a valid email address.", - } - messageCatalog["Email-required"] = &i18n.Message{ - ID: "ErrorEmailRequired", - Other: "Please enter an email address.", - } - messageCatalog["required"] = &i18n.Message{ - ID: "ErrorRequired", - Other: "Please enter a value", - } - messageCatalog["Password-required"] = &i18n.Message{ - ID: "ErrorPasswordRequired", - Other: "Please enter a password.", - } -} - -func health() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if atomic.LoadInt32(&healthy) == 1 { - w.WriteHeader(http.StatusNoContent) - return - } - w.WriteHeader(http.StatusServiceUnavailable) - }) -} - -func logging(logger *log.Logger) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - requestId, ok := r.Context().Value(requestIdKey).(string) - if !ok { - requestId = "unknown" - } - logger.Infoln(requestId, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent()) - }() - next.ServeHTTP(w, r) - }) - } -} - -func tracing(nextRequestId func() string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - requestId := r.Header.Get("X-Request-Id") - if requestId == "" { - requestId = nextRequestId() - } - ctx := context.WithValue(r.Context(), requestIdKey, requestId) - w.Header().Set("X-Request-Id", requestId) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} - -type consentHandler struct { -} - -func (c *consentHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - consentChallenge := request.URL.Query().Get("consent_challenge") - consentRequest, err := adminClient.Admin.AcceptConsentRequest(admin.NewAcceptConsentRequestParams().WithConsentChallenge(consentChallenge).WithBody(&models.AcceptConsentRequest{ - GrantAccessTokenAudience: nil, - GrantScope: []string{"openid", "offline"}, - HandledAt: models.NullTime(time.Now()), - Remember: true, - RememberFor: 86400, - }).WithTimeout(time.Second * 10)) - if err != nil { - log.Panic(err) - } - writer.Header().Add("Location", *consentRequest.GetPayload().RedirectTo) - writer.WriteHeader(http.StatusFound) -} - -func NewConsentHandler() *consentHandler { - return &consentHandler{} -} - -type loginHandler struct { - loginTemplate *template.Template -} - -type LoginInformation struct { - Email string `form:"email" validate:"required,email"` - Password string `form:"password" validate:"required"` -} - -func (l *loginHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - var err error - challenge := request.URL.Query().Get("login_challenge") - log.Debugf("received challenge %s\n", challenge) - - switch request.Method { - case http.MethodGet: - // GET should render login form - - err = l.loginTemplate.Lookup("base").Execute(writer, map[string]interface{}{ - "Title": "Title", - csrf.TemplateTag: csrf.TemplateField(request), - "LabelEmail": "Email", - "LabelPassword": "Password", - "LabelLogin": "Login", - "errors": map[string]string{}, - }) - if err != nil { - log.Error(err) - http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - break - case http.MethodPost: - // POST should perform the action - var loginInfo LoginInformation - - // validate input - decoder := form.NewDecoder() - err = decoder.Decode(&loginInfo, request.Form) - if err != nil { - log.Error(err) - http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - err := validate.Struct(&loginInfo) - if err != nil { - errors := make(map[string]string) - for _, err := range err.(validator.ValidationErrors) { - accept := request.Header.Get("Accept-Language") - errors[err.Field()] = lookupErrorMessage(err.Tag(), err.Field(), err.Value(), i18n.NewLocalizer(bundle, accept)) - } - - err = l.loginTemplate.Lookup("base").Execute(writer, map[string]interface{}{ - "Title": "Title", - csrf.TemplateTag: csrf.TemplateField(request), - "LabelEmail": "Email", - "LabelPassword": "Password", - "LabelLogin": "Login", - "Email": loginInfo.Email, - "errors": errors, - }) - if err != nil { - log.Error(err) - http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - return - } - - // GET user data - // finish login and redirect to target - // TODO: get or generate a user id - subject := "a-user-with-an-id" - loginRequest, err := adminClient.Admin.AcceptLoginRequest( - admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{ - Acr: "no-creds", - Remember: true, - RememberFor: 0, - Subject: &subject, - }).WithTimeout(time.Second * 10)) - if err != nil { - log.Panic(err) - } - writer.Header().Add("Location", *loginRequest.GetPayload().RedirectTo) - writer.WriteHeader(http.StatusFound) - break - default: - http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } -} - -func lookupErrorMessage(tag string, field string, value interface{}, l *i18n.Localizer) string { - var message *i18n.Message - message, ok := messageCatalog[fmt.Sprintf("%s-%s", field, tag)] - if !ok { - log.Infof("no specific error message %s-%s", field, tag) - message, ok = messageCatalog[tag] - if !ok { - log.Infof("no specific error message %s", tag) - message, ok = messageCatalog["unknown"] - if !ok { - log.Error("no default translation found") - return tag - } - } - } - - translation, err := l.Localize(&i18n.LocalizeConfig{ - DefaultMessage: message, - TemplateData: map[string]interface{}{ - "Value": value, - }, - }) - if err != nil { - log.Error(err) - return tag - } - return translation -} - -func NewLoginHandler() (*loginHandler, error) { - loginTemplate, err := template.ParseFiles("templates/base.html", "templates/login.html") - if err != nil { - return nil, err - } - return &loginHandler{loginTemplate: loginTemplate}, nil -}