Implement error pages, improve request logging
This commit is contained in:
parent
0cf51b8ff1
commit
e9c34a2337
19 changed files with 462 additions and 167 deletions
|
@ -22,6 +22,10 @@ other = "Bitte gib ein Passwort ein."
|
||||||
hash = "sha1-31632fcec9d22a8463757f459e51c7c0eccd1f28"
|
hash = "sha1-31632fcec9d22a8463757f459e51c7c0eccd1f28"
|
||||||
other = "Dieses Feld wird benötigt."
|
other = "Dieses Feld wird benötigt."
|
||||||
|
|
||||||
|
[ErrorTitle]
|
||||||
|
hash = "sha1-736aec25a98f5ec5b71400bb0163f891f509b566"
|
||||||
|
other = "Es ist ein Fehler aufgetreten"
|
||||||
|
|
||||||
[ErrorUnknown]
|
[ErrorUnknown]
|
||||||
hash = "sha1-e5fd9aa24c9417e7332e6f25936ae2a6ec8f1524"
|
hash = "sha1-e5fd9aa24c9417e7332e6f25936ae2a6ec8f1524"
|
||||||
other = "Unbekannter Fehler"
|
other = "Unbekannter Fehler"
|
||||||
|
|
|
@ -4,6 +4,7 @@ ErrorEmail = "Please enter a valid email address."
|
||||||
ErrorEmailRequired = "Please enter an email address."
|
ErrorEmailRequired = "Please enter an email address."
|
||||||
ErrorPasswordRequired = "Please enter a password."
|
ErrorPasswordRequired = "Please enter a password."
|
||||||
ErrorRequired = "Please enter a value"
|
ErrorRequired = "Please enter a value"
|
||||||
|
ErrorTitle = "An error has occurred"
|
||||||
ErrorUnknown = "Unknown error"
|
ErrorUnknown = "Unknown error"
|
||||||
IndexGreeting = "Hello {{ .User }}"
|
IndexGreeting = "Hello {{ .User }}"
|
||||||
IndexIntroductionText = "This is an authorization protected resource"
|
IndexIntroductionText = "This is an authorization protected resource"
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"git.cacert.org/oidc_login/app/services"
|
"git.cacert.org/oidc_login/app/services"
|
||||||
|
"git.cacert.org/oidc_login/common/handlers"
|
||||||
commonServices "git.cacert.org/oidc_login/common/services"
|
commonServices "git.cacert.org/oidc_login/common/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ type oidcCallbackHandler struct {
|
||||||
|
|
||||||
func (c *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (c *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.URL.Path != "/callback" {
|
if r.URL.Path != "/callback" {
|
||||||
|
@ -39,7 +40,13 @@ func (c *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||||
errorText := r.URL.Query().Get("error")
|
errorText := r.URL.Query().Get("error")
|
||||||
errorDescription := r.URL.Query().Get("error_description")
|
errorDescription := r.URL.Query().Get("error_description")
|
||||||
if errorText != "" {
|
if errorText != "" {
|
||||||
c.RenderErrorTemplate(w, errorText, errorDescription, http.StatusForbidden)
|
errorDetails := &handlers.ErrorDetails{
|
||||||
|
ErrorMessage: errorText,
|
||||||
|
}
|
||||||
|
if errorDescription != "" {
|
||||||
|
errorDetails.ErrorDetails = []string{errorDescription}
|
||||||
|
}
|
||||||
|
handlers.GetErrorBucket(r).AddError(errorDetails)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,14 +113,6 @@ Not valid after: %s
|
||||||
w.WriteHeader(http.StatusFound)
|
w.WriteHeader(http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *oidcCallbackHandler) RenderErrorTemplate(w http.ResponseWriter, errorText string, errorDescription string, status int) {
|
|
||||||
if errorDescription != "" {
|
|
||||||
http.Error(w, errorDescription, status)
|
|
||||||
} else {
|
|
||||||
http.Error(w, errorText, status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCallbackHandler(ctx context.Context, logger *log.Logger) *oidcCallbackHandler {
|
func NewCallbackHandler(ctx context.Context, logger *log.Logger) *oidcCallbackHandler {
|
||||||
return &oidcCallbackHandler{
|
return &oidcCallbackHandler{
|
||||||
keySet: commonServices.GetJwkSet(ctx),
|
keySet: commonServices.GetJwkSet(ctx),
|
||||||
|
|
104
cmd/app/main.go
104
cmd/app/main.go
|
@ -6,20 +6,11 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/knadh/koanf"
|
|
||||||
"github.com/knadh/koanf/parsers/toml"
|
"github.com/knadh/koanf/parsers/toml"
|
||||||
"github.com/knadh/koanf/providers/confmap"
|
"github.com/knadh/koanf/providers/confmap"
|
||||||
"github.com/knadh/koanf/providers/env"
|
|
||||||
"github.com/knadh/koanf/providers/file"
|
|
||||||
"github.com/knadh/koanf/providers/posflag"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
flag "github.com/spf13/pflag"
|
|
||||||
|
|
||||||
"git.cacert.org/oidc_login/app/handlers"
|
"git.cacert.org/oidc_login/app/handlers"
|
||||||
"git.cacert.org/oidc_login/app/services"
|
"git.cacert.org/oidc_login/app/services"
|
||||||
|
@ -28,48 +19,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
f := flag.NewFlagSet("config", flag.ContinueOnError)
|
|
||||||
f.Usage = func() {
|
|
||||||
fmt.Println(f.FlagUsages())
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
f.StringSlice("conf", []string{"resource_app.toml"}, "path to one or more .toml files")
|
|
||||||
logger := log.New()
|
logger := log.New()
|
||||||
var err error
|
config, err := commonServices.ConfigureApplication(
|
||||||
|
logger,
|
||||||
if err = f.Parse(os.Args[1:]); err != nil {
|
"RESOURCE_APP",
|
||||||
logger.Fatal(err)
|
map[string]interface{}{
|
||||||
}
|
"server.port": 4000,
|
||||||
|
"server.name": "app.cacert.localhost",
|
||||||
config := koanf.New(".")
|
"server.key": "certs/app.cacert.localhost.key",
|
||||||
|
"server.certificate": "certs/app.cacert.localhost.crt.pem",
|
||||||
_ = config.Load(confmap.Provider(map[string]interface{}{
|
"oidc.server": "https://auth.cacert.localhost:4444/",
|
||||||
"server.port": 4000,
|
"session.path": "sessions/app",
|
||||||
"server.name": "app.cacert.localhost",
|
"i18n.languages": []string{"en", "de"},
|
||||||
"server.key": "certs/app.cacert.localhost.key",
|
})
|
||||||
"server.certificate": "certs/app.cacert.localhost.crt.pem",
|
if err != nil {
|
||||||
"oidc.server": "https://auth.cacert.localhost:4444/",
|
log.Fatalf("error loading configuration: %v", err)
|
||||||
"session.path": "sessions/app",
|
|
||||||
"i18n.languages": []string{"en", "de"},
|
|
||||||
}, "."), nil)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := config.Load(posflag.Provider(f, ".", config), nil); err != nil {
|
|
||||||
logger.Fatalf("error loading configuration: %s", err)
|
|
||||||
}
|
|
||||||
if err := config.Load(file.Provider("resource_app.toml"), toml.Parser()); err != nil && !os.IsNotExist(err) {
|
|
||||||
log.Fatalf("error loading config: %v", err)
|
|
||||||
}
|
|
||||||
const prefix = "RESOURCE_APP_"
|
|
||||||
if err := config.Load(env.Provider(prefix, ".", func(s string) string {
|
|
||||||
return strings.Replace(strings.ToLower(
|
|
||||||
strings.TrimPrefix(s, prefix)), "_", ".", -1)
|
|
||||||
}), nil); err != nil {
|
|
||||||
log.Fatalf("error loading config: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcServer := config.MustString("oidc.server")
|
oidcServer := config.MustString("oidc.server")
|
||||||
|
@ -151,6 +115,14 @@ func main() {
|
||||||
tracing := commonHandlers.Tracing(nextRequestId)
|
tracing := commonHandlers.Tracing(nextRequestId)
|
||||||
logging := commonHandlers.Logging(logger)
|
logging := commonHandlers.Logging(logger)
|
||||||
hsts := commonHandlers.EnableHSTS()
|
hsts := commonHandlers.EnableHSTS()
|
||||||
|
errorMiddleware, err := commonHandlers.ErrorHandling(
|
||||||
|
ctx,
|
||||||
|
logger,
|
||||||
|
"templates/app",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("could not initialize request error handling: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
ServerName: config.String("server.name"),
|
ServerName: config.String("server.name"),
|
||||||
|
@ -158,40 +130,12 @@ func main() {
|
||||||
}
|
}
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: serverAddr,
|
Addr: serverAddr,
|
||||||
Handler: tracing(logging(hsts(router))),
|
Handler: tracing(logging(hsts(errorMiddleware(router)))),
|
||||||
ReadTimeout: 5 * time.Second,
|
ReadTimeout: 5 * time.Second,
|
||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 10 * time.Second,
|
||||||
IdleTimeout: 15 * time.Second,
|
IdleTimeout: 15 * time.Second,
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
done := make(chan bool)
|
commonHandlers.StartApplication(logger, ctx, server, config)
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,21 +11,13 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-openapi/runtime/client"
|
"github.com/go-openapi/runtime/client"
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"github.com/knadh/koanf"
|
|
||||||
"github.com/knadh/koanf/parsers/toml"
|
|
||||||
"github.com/knadh/koanf/providers/confmap"
|
|
||||||
"github.com/knadh/koanf/providers/env"
|
|
||||||
"github.com/knadh/koanf/providers/file"
|
|
||||||
"github.com/knadh/koanf/providers/posflag"
|
|
||||||
hydra "github.com/ory/hydra-client-go/client"
|
hydra "github.com/ory/hydra-client-go/client"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
flag "github.com/spf13/pflag"
|
|
||||||
|
|
||||||
commonHandlers "git.cacert.org/oidc_login/common/handlers"
|
commonHandlers "git.cacert.org/oidc_login/common/handlers"
|
||||||
commonServices "git.cacert.org/oidc_login/common/services"
|
commonServices "git.cacert.org/oidc_login/common/services"
|
||||||
|
@ -34,45 +26,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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()
|
logger := log.New()
|
||||||
var err error
|
config, err := commonServices.ConfigureApplication(
|
||||||
|
logger,
|
||||||
if err = f.Parse(os.Args[1:]); err != nil {
|
"IDP",
|
||||||
logger.Fatal(err)
|
map[string]interface{}{
|
||||||
}
|
"server.port": 3000,
|
||||||
|
"server.name": "login.cacert.localhost",
|
||||||
config := koanf.New(".")
|
"server.key": "certs/idp.cacert.localhost.key",
|
||||||
|
"server.certificate": "certs/idp.cacert.localhost.crt.pem",
|
||||||
_ = config.Load(confmap.Provider(map[string]interface{}{
|
"security.client.ca-file": "certs/client_ca.pem",
|
||||||
"server.port": 3000,
|
"admin.url": "https://hydra.cacert.localhost:4445/",
|
||||||
"server.name": "login.cacert.localhost",
|
"i18n.languages": []string{"en", "de"},
|
||||||
"server.key": "certs/idp.cacert.localhost.key",
|
})
|
||||||
"server.certificate": "certs/idp.cacert.localhost.crt.pem",
|
if err != nil {
|
||||||
"security.client.ca-file": "certs/client_ca.pem",
|
log.Fatalf("error loading configuration: %v", err)
|
||||||
"admin.url": "https://hydra.cacert.localhost:4445/",
|
|
||||||
"i18n.languages": []string{"en", "de"},
|
|
||||||
}, "."), nil)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := config.Load(posflag.Provider(f, ".", config), nil); err != nil {
|
|
||||||
logger.Fatalf("error loading configuration: %s", err)
|
|
||||||
}
|
|
||||||
const prefix = "IDP_"
|
|
||||||
if err := config.Load(env.Provider(prefix, ".", func(s string) string {
|
|
||||||
return strings.Replace(strings.ToLower(
|
|
||||||
strings.TrimPrefix(s, prefix)), "_", ".", -1)
|
|
||||||
}), nil); err != nil {
|
|
||||||
log.Fatalf("error loading config: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infoln("Server is starting")
|
logger.Infoln("Server is starting")
|
||||||
|
@ -139,6 +107,14 @@ func main() {
|
||||||
csrf.Secure(true),
|
csrf.Secure(true),
|
||||||
csrf.SameSite(csrf.SameSiteStrictMode),
|
csrf.SameSite(csrf.SameSiteStrictMode),
|
||||||
csrf.MaxAge(600))
|
csrf.MaxAge(600))
|
||||||
|
errorMiddleware, err := commonHandlers.ErrorHandling(
|
||||||
|
ctx,
|
||||||
|
logger,
|
||||||
|
"templates/idp",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("could not initialize request error handling: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
clientCertPool := x509.NewCertPool()
|
clientCertPool := x509.NewCertPool()
|
||||||
pemBytes, err := ioutil.ReadFile(config.MustString("security.client.ca-file"))
|
pemBytes, err := ioutil.ReadFile(config.MustString("security.client.ca-file"))
|
||||||
|
@ -155,7 +131,7 @@ func main() {
|
||||||
}
|
}
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: fmt.Sprintf("%s:%d", config.String("server.name"), config.Int("server.port")),
|
Addr: fmt.Sprintf("%s:%d", config.String("server.name"), config.Int("server.port")),
|
||||||
Handler: tracing(logging(hsts(csrfProtect(router)))),
|
Handler: tracing(logging(hsts(errorMiddleware(csrfProtect(router))))),
|
||||||
ReadTimeout: 20 * time.Second,
|
ReadTimeout: 20 * time.Second,
|
||||||
WriteTimeout: 20 * time.Second,
|
WriteTimeout: 20 * time.Second,
|
||||||
IdleTimeout: 30 * time.Second,
|
IdleTimeout: 30 * time.Second,
|
||||||
|
|
137
common/handlers/errors.go
Normal file
137
common/handlers/errors.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
commonServices "git.cacert.org/oidc_login/common/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type errorKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
errorBucketKey errorKey = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorDetails struct {
|
||||||
|
ErrorMessage string
|
||||||
|
ErrorDetails []string
|
||||||
|
ErrorCode string
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorBucket struct {
|
||||||
|
errorDetails *ErrorDetails
|
||||||
|
templates *template.Template
|
||||||
|
logger *log.Logger
|
||||||
|
bundle *i18n.Bundle
|
||||||
|
messageCatalog *commonServices.MessageCatalog
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ErrorBucket) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if b.errorDetails != nil {
|
||||||
|
accept := r.Header.Get("Accept-Language")
|
||||||
|
localizer := i18n.NewLocalizer(b.bundle, accept)
|
||||||
|
err := b.templates.Lookup("base").Execute(w, map[string]interface{}{
|
||||||
|
"Title": b.messageCatalog.LookupMessage(
|
||||||
|
"ErrorTitle",
|
||||||
|
nil,
|
||||||
|
localizer,
|
||||||
|
),
|
||||||
|
"details": b.errorDetails,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error rendering error template: %v", err)
|
||||||
|
http.Error(
|
||||||
|
w,
|
||||||
|
http.StatusText(http.StatusInternalServerError),
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetErrorBucket(r *http.Request) *ErrorBucket {
|
||||||
|
return r.Context().Value(errorBucketKey).(*ErrorBucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// call this from your application's handler
|
||||||
|
func (b *ErrorBucket) AddError(details *ErrorDetails) {
|
||||||
|
b.errorDetails = details
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
ctx context.Context
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *errorResponseWriter) WriteHeader(code int) {
|
||||||
|
w.statusCode = code
|
||||||
|
if code >= 400 {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
errorBucket := w.ctx.Value(errorBucketKey).(*ErrorBucket)
|
||||||
|
if errorBucket != nil && errorBucket.errorDetails == nil {
|
||||||
|
errorBucket.AddError(&ErrorDetails{
|
||||||
|
ErrorMessage: http.StatusText(code),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *errorResponseWriter) Write(content []byte) (int, error) {
|
||||||
|
if w.statusCode > 400 {
|
||||||
|
errorBucket := w.ctx.Value(errorBucketKey).(*ErrorBucket)
|
||||||
|
if errorBucket != nil {
|
||||||
|
if errorBucket.errorDetails.ErrorDetails == nil {
|
||||||
|
errorBucket.errorDetails.ErrorDetails = make([]string, 0)
|
||||||
|
}
|
||||||
|
errorBucket.errorDetails.ErrorDetails = append(
|
||||||
|
errorBucket.errorDetails.ErrorDetails, string(content),
|
||||||
|
)
|
||||||
|
return len(content), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w.ResponseWriter.Write(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorHandling(
|
||||||
|
handlerContext context.Context,
|
||||||
|
logger *log.Logger,
|
||||||
|
templateBaseDir string,
|
||||||
|
) (func(http.Handler) http.Handler, error) {
|
||||||
|
errorTemplates, err := template.ParseFiles(
|
||||||
|
path.Join(templateBaseDir, "base.gohtml"),
|
||||||
|
path.Join(templateBaseDir, "errors.gohtml"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
errorBucket := &ErrorBucket{
|
||||||
|
templates: errorTemplates,
|
||||||
|
logger: logger,
|
||||||
|
bundle: commonServices.GetI18nBundle(handlerContext),
|
||||||
|
messageCatalog: commonServices.GetMessageCatalog(handlerContext),
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), errorBucketKey, errorBucket)
|
||||||
|
interCeptingResponseWriter := &errorResponseWriter{
|
||||||
|
w,
|
||||||
|
ctx,
|
||||||
|
http.StatusOK,
|
||||||
|
}
|
||||||
|
next.ServeHTTP(
|
||||||
|
interCeptingResponseWriter,
|
||||||
|
r.WithContext(ctx),
|
||||||
|
)
|
||||||
|
errorBucket.serveHTTP(w, r)
|
||||||
|
})
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -14,17 +14,44 @@ const (
|
||||||
requestIdKey key = iota
|
requestIdKey key = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type statusCodeInterceptor struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
code int
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sci *statusCodeInterceptor) WriteHeader(code int) {
|
||||||
|
sci.code = code
|
||||||
|
sci.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sci *statusCodeInterceptor) Write(content []byte) (int, error) {
|
||||||
|
count, err := sci.ResponseWriter.Write(content)
|
||||||
|
sci.count += count
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
func Logging(logger *log.Logger) func(http.Handler) http.Handler {
|
func Logging(logger *log.Logger) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
interceptor := &statusCodeInterceptor{w, http.StatusOK, 0}
|
||||||
defer func() {
|
defer func() {
|
||||||
requestId, ok := r.Context().Value(requestIdKey).(string)
|
requestId, ok := r.Context().Value(requestIdKey).(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
requestId = "unknown"
|
requestId = "unknown"
|
||||||
}
|
}
|
||||||
logger.Infoln(requestId, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent())
|
logger.Infof(
|
||||||
|
"%s %s \"%s %s\" %d %d \"%s\"",
|
||||||
|
requestId,
|
||||||
|
r.RemoteAddr,
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
interceptor.code,
|
||||||
|
interceptor.count,
|
||||||
|
r.UserAgent(),
|
||||||
|
)
|
||||||
}()
|
}()
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(interceptor, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
45
common/handlers/startup.go
Normal file
45
common/handlers/startup.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/knadh/koanf"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartApplication(logger *logrus.Logger, ctx context.Context, server *http.Server, config *koanf.Koanf) {
|
||||||
|
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(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(&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")
|
||||||
|
}
|
65
common/services/configuration.go
Normal file
65
common/services/configuration.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/knadh/koanf"
|
||||||
|
"github.com/knadh/koanf/parsers/toml"
|
||||||
|
"github.com/knadh/koanf/providers/confmap"
|
||||||
|
"github.com/knadh/koanf/providers/env"
|
||||||
|
"github.com/knadh/koanf/providers/file"
|
||||||
|
"github.com/knadh/koanf/providers/posflag"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConfigureApplication(
|
||||||
|
logger *logrus.Logger,
|
||||||
|
appName string,
|
||||||
|
defaultConfig map[string]interface{},
|
||||||
|
) (*koanf.Koanf, error) {
|
||||||
|
f := pflag.NewFlagSet("config", pflag.ContinueOnError)
|
||||||
|
f.Usage = func() {
|
||||||
|
fmt.Println(f.FlagUsages())
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
f.StringSlice(
|
||||||
|
"conf",
|
||||||
|
[]string{fmt.Sprintf("%s.toml", strings.ToLower(appName))},
|
||||||
|
"path to one or more .toml files",
|
||||||
|
)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if err = f.Parse(os.Args[1:]); err != nil {
|
||||||
|
logger.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := koanf.New(".")
|
||||||
|
|
||||||
|
_ = config.Load(confmap.Provider(defaultConfig, "."), nil)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := config.Load(posflag.Provider(f, ".", config), nil); err != nil {
|
||||||
|
logger.Fatalf("error loading configuration: %s", err)
|
||||||
|
}
|
||||||
|
if err := config.Load(
|
||||||
|
file.Provider("resource_app.toml"),
|
||||||
|
toml.Parser(),
|
||||||
|
); err != nil && !os.IsNotExist(err) {
|
||||||
|
logrus.Fatalf("error loading config: %v", err)
|
||||||
|
}
|
||||||
|
prefix := fmt.Sprintf("%s_", strings.ToUpper(appName))
|
||||||
|
if err := config.Load(env.Provider(prefix, ".", func(s string) string {
|
||||||
|
return strings.Replace(strings.ToLower(
|
||||||
|
strings.TrimPrefix(s, prefix)), "_", ".", -1)
|
||||||
|
}), nil); err != nil {
|
||||||
|
logrus.Fatalf("error loading config: %v", err)
|
||||||
|
}
|
||||||
|
return config, err
|
||||||
|
}
|
|
@ -91,6 +91,10 @@ func InitI18n(ctx context.Context, logger *log.Logger, languages []string) conte
|
||||||
|
|
||||||
func initMessageCatalog(logger *log.Logger) *MessageCatalog {
|
func initMessageCatalog(logger *log.Logger) *MessageCatalog {
|
||||||
messages := make(map[string]*i18n.Message)
|
messages := make(map[string]*i18n.Message)
|
||||||
|
messages["ErrorTitle"] = &i18n.Message{
|
||||||
|
ID: "ErrorTitle",
|
||||||
|
Other: "An error has occurred",
|
||||||
|
}
|
||||||
return &MessageCatalog{messages: messages, logger: logger}
|
return &MessageCatalog{messages: messages, logger: logger}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,9 @@ body {
|
||||||
body.idp {
|
body.idp {
|
||||||
display: -ms-flexbox;
|
display: -ms-flexbox;
|
||||||
display: flex;
|
display: flex;
|
||||||
// -ms-flex-align: center;
|
}
|
||||||
// align-items: center;
|
|
||||||
|
.error-message, body.idp {
|
||||||
padding-top: 40px;
|
padding-top: 40px;
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ import (
|
||||||
"github.com/ory/hydra-client-go/models"
|
"github.com/ory/hydra-client-go/models"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.cacert.org/oidc_login/common/handlers"
|
||||||
commonServices "git.cacert.org/oidc_login/common/services"
|
commonServices "git.cacert.org/oidc_login/common/services"
|
||||||
"git.cacert.org/oidc_login/idp/services"
|
"git.cacert.org/oidc_login/idp/services"
|
||||||
)
|
)
|
||||||
|
@ -70,8 +72,25 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
consentData, err := h.adminClient.GetConsentRequest(
|
consentData, err := h.adminClient.GetConsentRequest(
|
||||||
admin.NewGetConsentRequestParams().WithConsentChallenge(challenge))
|
admin.NewGetConsentRequestParams().WithConsentChallenge(challenge))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("error getting consent information: %v", err)
|
h.logger.Errorf("error getting consent information: %v", err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
var errorDetails *handlers.ErrorDetails
|
||||||
|
switch v := err.(type) {
|
||||||
|
case *admin.GetConsentRequestConflict:
|
||||||
|
errorDetails = &handlers.ErrorDetails{
|
||||||
|
ErrorMessage: *v.Payload.Error,
|
||||||
|
ErrorDetails: []string{v.Payload.ErrorDescription},
|
||||||
|
}
|
||||||
|
if v.Payload.StatusCode != 0 {
|
||||||
|
errorDetails.ErrorCode = strconv.Itoa(int(v.Payload.StatusCode))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
errorDetails = &handlers.ErrorDetails{
|
||||||
|
ErrorMessage: "could not get consent details",
|
||||||
|
ErrorDetails: []string{http.StatusText(http.StatusInternalServerError)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handlers.GetErrorBucket(r).AddError(errorDetails)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +105,11 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
decoder := form.NewDecoder()
|
decoder := form.NewDecoder()
|
||||||
if err := decoder.Decode(&consentInfo, r.Form); err != nil {
|
if err := decoder.Decode(&consentInfo, r.Form); err != nil {
|
||||||
h.logger.Error(err)
|
h.logger.Error(err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(
|
||||||
|
w,
|
||||||
|
http.StatusText(http.StatusInternalServerError),
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,8 +122,8 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Context(),
|
r.Context(),
|
||||||
`SELECT email, verified, fname, mname, lname, dob, language, modified
|
`SELECT email, verified, fname, mname, lname, dob, language, modified
|
||||||
FROM users
|
FROM users
|
||||||
WHERE uniqueID = ?
|
WHERE uniqueid = ?
|
||||||
AND LOCKED = 0`,
|
AND locked = 0`,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Errorf("error preparing user information SQL: %v", err)
|
h.logger.Errorf("error preparing user information SQL: %v", err)
|
||||||
|
@ -181,7 +204,13 @@ WHERE uniqueID = ?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *consentHandler) renderConsentForm(w http.ResponseWriter, r *http.Request, consentData *admin.GetConsentRequestOK, err error, localizer *i18n.Localizer) {
|
func (h *consentHandler) renderConsentForm(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
consentData *admin.GetConsentRequestOK,
|
||||||
|
err error,
|
||||||
|
localizer *i18n.Localizer,
|
||||||
|
) {
|
||||||
trans := func(id string, values ...map[string]interface{}) string {
|
trans := func(id string, values ...map[string]interface{}) string {
|
||||||
if len(values) > 0 {
|
if len(values) > 0 {
|
||||||
return h.messageCatalog.LookupMessage(id, values[0], localizer)
|
return h.messageCatalog.LookupMessage(id, values[0], localizer)
|
||||||
|
|
|
@ -7,9 +7,9 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-openapi/runtime"
|
|
||||||
"github.com/go-playground/form/v4"
|
"github.com/go-playground/form/v4"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
|
@ -19,6 +19,7 @@ import (
|
||||||
"github.com/ory/hydra-client-go/models"
|
"github.com/ory/hydra-client-go/models"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.cacert.org/oidc_login/common/handlers"
|
||||||
commonServices "git.cacert.org/oidc_login/common/services"
|
commonServices "git.cacert.org/oidc_login/common/services"
|
||||||
"git.cacert.org/oidc_login/idp/services"
|
"git.cacert.org/oidc_login/idp/services"
|
||||||
)
|
)
|
||||||
|
@ -143,15 +144,33 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// finish login and redirect to target
|
// finish login and redirect to target
|
||||||
loginRequest, err := h.adminClient.AcceptLoginRequest(
|
loginRequest, err := h.adminClient.AcceptLoginRequest(
|
||||||
admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{
|
admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(
|
||||||
Acr: string(authMethod),
|
&models.AcceptLoginRequest{
|
||||||
Remember: true,
|
Acr: string(authMethod),
|
||||||
RememberFor: 0,
|
Remember: true,
|
||||||
Subject: userId,
|
RememberFor: 0,
|
||||||
}).WithTimeout(time.Second * 10))
|
Subject: userId,
|
||||||
|
}).WithTimeout(time.Second * 10))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Errorf("error getting login request: %#v", err)
|
h.logger.Errorf("error getting login request: %#v", err)
|
||||||
http.Error(w, err.Error(), err.(*runtime.APIError).Code)
|
var errorDetails *handlers.ErrorDetails
|
||||||
|
switch v := err.(type) {
|
||||||
|
case *admin.AcceptLoginRequestNotFound:
|
||||||
|
errorDetails = &handlers.ErrorDetails{
|
||||||
|
ErrorMessage: *v.Payload.Error,
|
||||||
|
ErrorDetails: []string{v.Payload.ErrorDescription},
|
||||||
|
}
|
||||||
|
if v.Payload.StatusCode != 0 {
|
||||||
|
errorDetails.ErrorCode = strconv.Itoa(int(v.Payload.StatusCode))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
errorDetails = &handlers.ErrorDetails{
|
||||||
|
ErrorMessage: "could not accept login",
|
||||||
|
ErrorDetails: []string{err.Error()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handlers.GetErrorBucket(r).AddError(errorDetails)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
|
w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
|
||||||
|
@ -255,7 +274,7 @@ func (h *loginHandler) performCertificateLogin(emails []string, r *http.Request)
|
||||||
db := services.GetDb(h.context)
|
db := services.GetDb(h.context)
|
||||||
|
|
||||||
query, args, err := sqlx.In(
|
query, args, err := sqlx.In(
|
||||||
`SELECT DISTINCT u.uniqueID
|
`SELECT DISTINCT u.uniqueid
|
||||||
FROM users u
|
FROM users u
|
||||||
JOIN email e ON e.memid = u.id
|
JOIN email e ON e.memid = u.id
|
||||||
WHERE e.email IN (?)
|
WHERE e.email IN (?)
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<link rel="icon" href="/images/favicon-128.png" sizes="128x128">
|
<link rel="icon" href="/images/favicon-128.png" sizes="128x128">
|
||||||
<link rel="icon" href="/images/favicon-192.png" sizes="192x192">
|
<link rel="icon" href="/images/favicon-192.png" sizes="192x192">
|
||||||
<link rel="icon" href="/images/favicon-228.png" sizes="228x228">
|
<link rel="icon" href="/images/favicon-228.png" sizes="228x228">
|
||||||
|
<link rel="icon" href="/images/favicon.ico">
|
||||||
|
|
||||||
<!-- Android -->
|
<!-- Android -->
|
||||||
<link rel="shortcut icon" sizes="196x196" href="/images/favicon-196.png">
|
<link rel="shortcut icon" sizes="196x196" href="/images/favicon-196.png">
|
||||||
|
@ -22,10 +23,18 @@
|
||||||
<link rel="apple-touch-icon" href="/images/favicon-180.png" sizes="180x180">
|
<link rel="apple-touch-icon" href="/images/favicon-180.png" sizes="180x180">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/cacert.bundle.css">
|
<link rel="stylesheet" href="/css/cacert.bundle.css">
|
||||||
|
<meta name="theme-color" content="#11568c">
|
||||||
<title>{{ .Title }}</title>
|
<title>{{ .Title }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="resource-app d-flex flex-column h-100">
|
||||||
{{ template "content" . }}
|
<main role="main" class="flex-shrink-0">
|
||||||
|
{{ template "content" . }}
|
||||||
|
</main>
|
||||||
|
<footer class="footer mt-auto py-3">
|
||||||
|
<div class="container">
|
||||||
|
<span class="text-muted small">© 2020 <a href="https://www.cacert.org/">CAcert</a></span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
<script type="text/javascript" src="/js/cacert.bundle.js"></script>
|
<script type="text/javascript" src="/js/cacert.bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
13
templates/app/errors.gohtml
Normal file
13
templates/app/errors.gohtml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<div class="container text-center error-message">
|
||||||
|
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
|
||||||
|
<h1>{{ .Title }}</h1>
|
||||||
|
<h2>{{ if .details.ErrorCode }}
|
||||||
|
<strong>{{ .details.ErrorCode }}</strong> {{ end }}{{ .details.ErrorMessage }}</h2>
|
||||||
|
{{ if .details.ErrorDetails }}
|
||||||
|
{{ range .details.ErrorDetails }}
|
||||||
|
<p>{{ . }}</p>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
|
@ -1,5 +1,8 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h1>{{ .Greeting }}</h1>
|
<div class="container">
|
||||||
<p>{{ .IntroductionText }}</p>
|
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
|
||||||
<a href="{{ .LogoutURL }}">{{ .LogoutLabel }}</a>
|
<h1>{{ .Greeting }}</h1>
|
||||||
|
<p>{{ .IntroductionText }}</p>
|
||||||
|
<a class="btn btn-outline-primary" href="{{ .LogoutURL }}">{{ .LogoutLabel }}</a>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
|
@ -26,8 +26,15 @@
|
||||||
<meta name="theme-color" content="#11568c">
|
<meta name="theme-color" content="#11568c">
|
||||||
<title>{{ .Title }}</title>
|
<title>{{ .Title }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="text-center idp">
|
<body class="text-center idp d-flex flex-column h-100">
|
||||||
{{ template "content" . }}
|
<main role="main" class="flex-shrink-0">
|
||||||
|
{{ template "content" . }}
|
||||||
|
</main>
|
||||||
|
<footer class="footer mt-auto py-3">
|
||||||
|
<div class="container">
|
||||||
|
<span class="text-muted small">© 2020 <a href="https://www.cacert.org/">CAcert</a></span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
<script type="text/javascript" src="/js/cacert.bundle.js"></script>
|
<script type="text/javascript" src="/js/cacert.bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
13
templates/idp/errors.gohtml
Normal file
13
templates/idp/errors.gohtml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<div class="container">
|
||||||
|
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
|
||||||
|
<h1>{{ .Title }}</h1>
|
||||||
|
<h2>{{ if .details.ErrorCode }}
|
||||||
|
<strong>{{ .details.ErrorCode }}</strong> {{ end }}{{ .details.ErrorMessage }}</h2>
|
||||||
|
{{ if .details.ErrorDetails }}
|
||||||
|
{{ range .details.ErrorDetails }}
|
||||||
|
<p>{{ . }}</p>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
|
@ -1,4 +1,3 @@
|
||||||
[LabelAcceptCertLogin]
|
[ErrorTitle]
|
||||||
description = "Label for a button to accept certificate login"
|
hash = "sha1-736aec25a98f5ec5b71400bb0163f891f509b566"
|
||||||
hash = "sha1-95cf27f4bdee62b51ee8bc673d25a46bcceed452"
|
other = "Es ist ein Fehler aufgetreten"
|
||||||
other = "Ja, bitte nutze das Zertifikat"
|
|
||||||
|
|
Reference in a new issue