Compare commits

..

No commits in common. "main" and "0.1.0" have entirely different histories.
main ... 0.1.0

32 changed files with 390 additions and 927 deletions

3
.gitignore vendored
View file

@ -2,10 +2,9 @@
/.idea/ /.idea/
/demo-app /demo-app
/dist /dist
/resource_app*.toml /resource_app.toml
/sessions /sessions
/static /static
/translations/translate.*.toml
/ui/css/ /ui/css/
/ui/images/ /ui/images/
/ui/js/ /ui/js/

View file

@ -8,7 +8,7 @@ linters-settings:
const: const:
ORGANIZATION: CAcert Inc. ORGANIZATION: CAcert Inc.
template: |- template: |-
Copyright {{ ORGANIZATION }} Copyright {{ YEAR-RANGE }} {{ ORGANIZATION }}
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -47,7 +47,7 @@ linters:
- gofmt - gofmt
- goheader - goheader
- goimports - goimports
- mnd - gomnd
- gosec - gosec
- lll - lll
- makezero - makezero

View file

@ -18,12 +18,13 @@ go.sum: go.mod
go mod tidy -v go mod tidy -v
translations: $(TRANSLATIONS) $(GOFILES) translations: $(TRANSLATIONS) $(GOFILES)
if [ ! -z "$(wildcard translations/translate.*.toml)" ]; then \ cd translations ; \
goi18n extract .. ; \
goi18n merge active.*.toml ; \
if translate.*.toml 2>/dev/null; then \
echo "missing translations"; \ echo "missing translations"; \
goi18n merge -outdir translations translations/active.*.toml translations/translate.*.toml; \ goi18n merge active.*.toml translate.*.toml; \
fi ; \ fi
goi18n extract -outdir translations . ; \
goi18n merge -outdir translations translations/active.*.toml
lint: $(GOFILES) lint: $(GOFILES)
golangci-lint run --verbose golangci-lint run --verbose

View file

@ -5,25 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.0] - 2023-08-07
### Changed
- let the session expire when the token expires
- link from logo to start page
- move common page header to templates/base.gohtml
- implement caching for static resources
### Added
- add identity output to index page
- add a separate protected resource page
## [0.2.0]
### Changed
- re-order configuration precedence
1. default config file idp.toml
2. config files given via the `--conf` command line argument
3. environment variables with the `RESOURCE_APP_` prefix
### Fixed
- fix logged application name
## [0.1.0] - 2023-07-29 ## [0.1.0] - 2023-07-29
### Changed ### Changed
- initial release - initial release

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -23,7 +23,6 @@ import (
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"time" "time"
@ -31,6 +30,7 @@ import (
"github.com/knadh/koanf" "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"
log "github.com/sirupsen/logrus"
"code.cacert.org/cacert/oidc-demo-app/ui" "code.cacert.org/cacert/oidc-demo-app/ui"
@ -53,89 +53,59 @@ var (
date = "unknown" date = "unknown"
) )
type StaticFSWrapper struct { func main() {
http.FileSystem logger := log.New()
ModTime time.Time
}
func (f *StaticFSWrapper) Open(name string) (http.File, error) { config, err := services.ConfigureApplication(
file, err := f.FileSystem.Open(name) logger,
"RESOURCE_APP",
return &StaticFileWrapper{File: file, fixedModTime: f.ModTime}, err //nolint:wrapcheck services.DefaultConfiguration,
}
type StaticFileWrapper struct {
http.File
fixedModTime time.Time
}
func (f *StaticFileWrapper) Stat() (os.FileInfo, error) {
fileInfo, err := f.File.Stat()
return &StaticFileInfoWrapper{FileInfo: fileInfo, fixedModTime: f.fixedModTime}, err //nolint:wrapcheck
}
type StaticFileInfoWrapper struct {
os.FileInfo
fixedModTime time.Time
}
func (f *StaticFileInfoWrapper) ModTime() time.Time {
return f.fixedModTime
}
func main() { //nolint:cyclop
var (
logLevel = new(slog.LevelVar)
logHandler slog.Handler
logger *slog.Logger
) )
logHandler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})
logger = slog.New(logHandler)
slog.SetDefault(logger)
config, err := services.ConfigureApplication("RESOURCE_APP", services.DefaultConfiguration)
if err != nil { if err != nil {
logger.Error("error loading configuration", "error", err) log.Fatalf("error loading configuration: %v", err)
os.Exit(1)
} }
if level := config.Bytes("log.level"); level != nil {
if err := logLevel.UnmarshalText(level); err != nil {
logger.Error("could not parse log level", "error", err)
os.Exit(1)
}
slog.SetLogLoggerLevel(logLevel.Level())
}
if config.Bool("log.json") {
logHandler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})
logger = slog.New(logHandler)
slog.SetDefault(logger)
}
logLogger := slog.NewLogLogger(logger.Handler(), logLevel.Level())
oidcServer := config.MustString("oidc.server") oidcServer := config.MustString("oidc.server")
oidcClientID := config.MustString("oidc.client-id") oidcClientID := config.MustString("oidc.client-id")
oidcClientSecret := config.MustString("oidc.client-secret") oidcClientSecret := config.MustString("oidc.client-secret")
logger.Info( if level := config.String("log.level"); level != "" {
"Starting CAcert OpenID Connect demo application", logLevel, err := log.ParseLevel(level)
"version", version, "commit", commit, "date", date, if err != nil {
) logger.WithError(err).Fatal("could not parse log level")
logger.Info("Server is starting") }
logger.SetLevel(logLevel)
}
if config.Bool("log.json") {
logger.SetFormatter(&log.JSONFormatter{})
}
logger.WithFields(log.Fields{
"version": version, "commit": commit, "date": date,
}).Info("Starting CAcert OpenID Connect Identity Provider")
logger.Infoln("Server is starting")
bundle, catalog := services.InitI18n(logger, config.Strings("i18n.languages")) bundle, catalog := services.InitI18n(logger, config.Strings("i18n.languages"))
services.AddMessages(catalog) services.AddMessages(catalog)
tlsClientConfig, err := getTLSConfig(config) tlsClientConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
if config.Exists("api-client.rootCAs") {
rootCAFile := config.MustString("api-client.rootCAs")
caCertPool := x509.NewCertPool()
pemBytes, err := os.ReadFile(rootCAFile)
if err != nil { if err != nil {
logger.Error("error loading tls config", "error", err) log.Fatalf("could not read CA certificate file: %v", err)
os.Exit(1) }
caCertPool.AppendCertsFromPEM(pemBytes)
tlsClientConfig.RootCAs = caCertPool
} }
apiTransport := &http.Transport{TLSClientConfig: tlsClientConfig} apiTransport := &http.Transport{TLSClientConfig: tlsClientConfig}
@ -148,77 +118,45 @@ func main() { //nolint:cyclop
APIClient: apiClient, APIClient: apiClient,
}) })
if err != nil { if err != nil {
logger.Error("OpenID Connect discovery failed", "error", err) log.Fatalf("OpenID Connect discovery failed: %s", err)
os.Exit(1)
} }
sessionPath, sessionAuthKey, sessionEncKey, err := configureSessionParameters(logger, config) sessionPath, sessionAuthKey, sessionEncKey := configureSessionParameters(config)
if err != nil { services.InitSessionStore(logger, sessionPath, sessionAuthKey, sessionEncKey)
logger.Error("error configuring session parameters", "error", err)
os.Exit(1)
}
if err := services.InitSessionStore(sessionPath, sessionAuthKey, sessionEncKey); err != nil { authMiddleware := handlers.Authenticate(logger, oidcInfo.OAuth2Config, oidcClientID)
logger.Error("could not initialize session store", "error", err)
os.Exit(1)
}
authMiddleware := handlers.Authenticate(logger, oidcInfo.OAuth2Config)
publicURL := buildPublicURL(config.MustString("server.name"), config.MustInt("server.port")) publicURL := buildPublicURL(config.MustString("server.name"), config.MustInt("server.port"))
tokenInfoService, err := services.InitTokenInfoService(logger, oidcInfo) indexHandler, err := handlers.NewIndexHandler(bundle, catalog, ui.Templates, oidcInfo, publicURL)
if err != nil { if err != nil {
logger.Error("could not initialize token info service", "error", err) logger.WithError(err).Fatal("could not initialize index handler")
os.Exit(1)
}
indexHandler, err := handlers.NewIndexHandler(logger, bundle, catalog, oidcInfo, publicURL, tokenInfoService)
if err != nil {
logger.Error("could not initialize index handler", "error", err)
os.Exit(1)
}
protectedResource, err := handlers.NewProtectedResourceHandler(
logger, bundle, catalog, oidcInfo, publicURL, tokenInfoService,
)
if err != nil {
logger.Error("could not initialize protected resource handler", "error", err)
} }
callbackHandler := handlers.NewCallbackHandler(logger, oidcInfo.KeySet, oidcInfo.OAuth2Config) callbackHandler := handlers.NewCallbackHandler(logger, oidcInfo.KeySet, oidcInfo.OAuth2Config)
afterLogoutHandler := handlers.NewAfterLogoutHandler(logger) afterLogoutHandler := handlers.NewAfterLogoutHandler(logger)
staticFiles := http.FileServer(http.FS(ui.Static))
staticFiles, err := staticFileHandler()
if err != nil {
logger.Error("could not initialize static file handler", "error", err)
os.Exit(1)
}
router := http.NewServeMux() router := http.NewServeMux()
router.Handle("/", indexHandler) router.Handle("/", authMiddleware(indexHandler))
router.Handle("/login", authMiddleware(handlers.NewLoginHandler()))
router.Handle("/protected", authMiddleware(protectedResource))
router.Handle("/callback", callbackHandler) router.Handle("/callback", callbackHandler)
router.Handle("/after-logout", afterLogoutHandler) router.Handle("/after-logout", afterLogoutHandler)
router.Handle("/health", handlers.NewHealthHandler()) router.Handle("/health", handlers.NewHealthHandler())
router.HandleFunc("/images/", staticFiles) router.Handle("/images/", staticFiles)
router.HandleFunc("/css/", staticFiles) router.Handle("/css/", staticFiles)
router.HandleFunc("/js/", staticFiles) router.Handle("/js/", staticFiles)
nextRequestID := func() string { nextRequestID := func() string {
return fmt.Sprintf("%d", time.Now().UnixNano()) return fmt.Sprintf("%d", time.Now().UnixNano())
} }
tracing := handlers.Tracing(nextRequestID) tracing := handlers.Tracing(nextRequestID)
logging := handlers.Logging(logLogger) logging := handlers.Logging(logger)
hsts := handlers.EnableHSTS() hsts := handlers.EnableHSTS()
errorMiddleware, err := handlers.ErrorHandling(logger, bundle, catalog) errorMiddleware, err := handlers.ErrorHandling(logger, ui.Templates, bundle, catalog)
if err != nil { if err != nil {
logger.Error("could not initialize request error handling", "error", err) logger.WithError(err).Fatal("could not initialize request error handling")
os.Exit(1)
} }
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
@ -235,50 +173,7 @@ func main() { //nolint:cyclop
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
} }
if err := handlers.StartApplication(context.Background(), logger, server, publicURL, config); err != nil { handlers.StartApplication(context.Background(), logger, server, publicURL, config)
logger.Error("could not start application", "error", err)
os.Exit(1)
}
}
func staticFileHandler() (func(w http.ResponseWriter, r *http.Request), error) {
stat, err := os.Stat(os.Args[0])
if err != nil {
return nil, fmt.Errorf("could not use stat on binary: %w", err)
}
fileServer := http.FileServer(&StaticFSWrapper{FileSystem: http.FS(ui.Static), ModTime: stat.ModTime()})
staticFiles := func(w http.ResponseWriter, r *http.Request) {
w.Header().Del("Expires")
w.Header().Del("Pragma")
w.Header().Set("Cache-Control", "max-age=3600")
fileServer.ServeHTTP(w, r)
}
return staticFiles, nil
}
func getTLSConfig(config *koanf.Koanf) (*tls.Config, error) {
tlsClientConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
if config.Exists("api-client.rootCAs") {
rootCAFile := config.MustString("api-client.rootCAs")
caCertPool := x509.NewCertPool()
pemBytes, err := os.ReadFile(rootCAFile)
if err != nil {
return nil, fmt.Errorf("could not read CA certificate file: %w", err)
}
caCertPool.AppendCertsFromPEM(pemBytes)
tlsClientConfig.RootCAs = caCertPool
}
return tlsClientConfig, nil
} }
func buildPublicURL(hostname string, port int) string { func buildPublicURL(hostname string, port int) string {
@ -291,36 +186,28 @@ func buildPublicURL(hostname string, port int) string {
return fmt.Sprintf("https://%s", hostname) return fmt.Sprintf("https://%s", hostname)
} }
func configureSessionParameters(logger *slog.Logger, config *koanf.Koanf) (string, []byte, []byte, error) { func configureSessionParameters(config *koanf.Koanf) (string, []byte, []byte) {
sessionPath := config.MustString("session.path") sessionPath := config.MustString("session.path")
sessionAuthKey, err := base64.StdEncoding.DecodeString(config.String("session.auth-key")) sessionAuthKey, err := base64.StdEncoding.DecodeString(config.String("session.auth-key"))
if err != nil { if err != nil {
return "", nil, nil, fmt.Errorf("could not decode session authentication key: %w", err) log.WithError(err).Fatal("could not decode session auth key")
} }
sessionEncKey, err := base64.StdEncoding.DecodeString(config.String("session.enc-key")) sessionEncKey, err := base64.StdEncoding.DecodeString(config.String("session.enc-key"))
if err != nil { if err != nil {
return "", nil, nil, fmt.Errorf("could not decode session encryption key: %w", err) log.WithError(err).Fatal("could not decode session encryption key")
} }
generated := false generated := false
if len(sessionAuthKey) != sessionAuthKeyLength { if len(sessionAuthKey) != sessionAuthKeyLength {
sessionAuthKey, err = services.GenerateKey(sessionAuthKeyLength) sessionAuthKey = services.GenerateKey(sessionAuthKeyLength)
if err != nil {
return "", nil, nil, fmt.Errorf("could not generate session authentication key: %w", err)
}
generated = true generated = true
} }
if len(sessionEncKey) != sessionKeyLength { if len(sessionEncKey) != sessionKeyLength {
sessionEncKey, err = services.GenerateKey(sessionKeyLength) sessionEncKey = services.GenerateKey(sessionKeyLength)
if err != nil {
return "", nil, nil, fmt.Errorf("could not generate session encryption key: %w", err)
}
generated = true generated = true
} }
@ -332,12 +219,11 @@ func configureSessionParameters(logger *slog.Logger, config *koanf.Koanf) (strin
tomlData, err := config.Marshal(toml.Parser()) tomlData, err := config.Marshal(toml.Parser())
if err != nil { if err != nil {
return "", nil, nil, fmt.Errorf("could not encode session configuration: %w", err) log.WithError(err).Fatal("could not encode session config")
} }
logger.Info("put the following in your resource_app.toml") log.Infof("put the following in your resource_app.toml:\n%s", string(tomlData))
fmt.Print(string(tomlData)) //nolint:forbidigo
} }
return sessionPath, sessionAuthKey, sessionEncKey, nil return sessionPath, sessionAuthKey, sessionEncKey
} }

29
go.mod
View file

@ -1,25 +1,27 @@
module code.cacert.org/cacert/oidc-demo-app module code.cacert.org/cacert/oidc-demo-app
go 1.22 go 1.19
require ( require (
github.com/BurntSushi/toml v1.3.2 github.com/BurntSushi/toml v1.3.2
github.com/gorilla/sessions v1.2.2 github.com/gorilla/sessions v1.2.1
github.com/knadh/koanf v1.5.0 github.com/knadh/koanf v1.5.0
github.com/lestrrat-go/jwx v1.2.29 github.com/lestrrat-go/jwx v1.2.26
github.com/nicksnyder/go-i18n/v2 v2.4.0 github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
golang.org/x/oauth2 v0.20.0 golang.org/x/oauth2 v0.10.0
golang.org/x/text v0.15.0 golang.org/x/text v0.11.0
) )
require ( require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect github.com/lestrrat-go/option v1.0.1 // indirect
@ -28,6 +30,9 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.11.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
) )

74
go.sum
View file

@ -1,6 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -38,9 +39,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -51,8 +51,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -84,6 +84,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -96,15 +98,12 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
@ -166,14 +165,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= github.com/lestrrat-go/jwx v1.2.26 h1:4iFo8FPRZGDYe1t19mQP0zTRqA7n8HnJ5lkIiDvJcB0=
github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= github.com/lestrrat-go/jwx v1.2.26/go.mod h1:MaiCdGbn3/cckbOFSCluJlJMmp9dmZm5hDuIkx8ftpQ=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
@ -210,8 +209,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@ -252,13 +251,14 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -267,9 +267,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
@ -286,10 +285,9 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -308,6 +306,7 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -321,12 +320,13 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -370,19 +370,17 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -390,11 +388,11 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -415,6 +413,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
@ -440,6 +440,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -18,18 +18,21 @@ limitations under the License.
package handlers package handlers
import ( import (
"log/slog"
"net/http" "net/http"
"github.com/sirupsen/logrus"
"code.cacert.org/cacert/oidc-demo-app/internal/services"
) )
type AfterLogoutHandler struct { type AfterLogoutHandler struct {
logger *slog.Logger logger *logrus.Logger
} }
func (h *AfterLogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *AfterLogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(r) session, err := services.GetSessionStore().Get(r, sessionName)
if err != nil { if err != nil {
h.logger.Error("could not get session", "error", err) h.logger.WithError(err).Error("could not get session")
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -38,13 +41,13 @@ func (h *AfterLogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
session.Options.MaxAge = -1 session.Options.MaxAge = -1
if err = session.Save(r, w); err != nil { if err = session.Save(r, w); err != nil {
h.logger.Error("could not save session", "error", err) h.logger.WithError(err).Error("could not save session")
} }
w.Header().Set("Location", "/") w.Header().Set("Location", "/")
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
} }
func NewAfterLogoutHandler(logger *slog.Logger) *AfterLogoutHandler { func NewAfterLogoutHandler(logger *logrus.Logger) *AfterLogoutHandler {
return &AfterLogoutHandler{logger: logger} return &AfterLogoutHandler{logger: logger}
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -18,94 +18,107 @@ limitations under the License.
package handlers package handlers
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"net/url"
"github.com/gorilla/sessions" "github.com/lestrrat-go/jwx/jwk"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/jwt/openid"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"code.cacert.org/cacert/oidc-demo-app/internal/models"
"code.cacert.org/cacert/oidc-demo-app/internal/services" "code.cacert.org/cacert/oidc-demo-app/internal/services"
) )
const ( const (
sessionName = "resource_session"
oauth2RedirectStateLength = 8 oauth2RedirectStateLength = 8
sessionName = "resource_app"
) )
func Authenticate(logger *slog.Logger, oauth2Config *oauth2.Config) func(http.Handler) http.Handler { func Authenticate(logger *log.Logger, oauth2Config *oauth2.Config, clientID string) 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) {
session, err := GetSession(r) session, err := services.GetSessionStore().Get(r, sessionName)
if err != nil { if err != nil {
logger.ErrorContext(r.Context(), "failed to get session", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
if _, ok := session.Values[services.SessionIDToken]; ok { if _, ok := session.Values[sessionKeyIDToken]; ok {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
session.Values[services.SessionRedirectTarget] = r.URL.String() session.Values[sessionRedirectTarget] = r.URL.String()
if err = session.Save(r, w); err != nil { if err = session.Save(r, w); err != nil {
logger.ErrorContext(r.Context(), "failed to save session", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
state, err := services.GenerateKey(oauth2RedirectStateLength) var authURL *url.URL
if err != nil {
logger.ErrorContext( if authURL, err = url.Parse(oauth2Config.Endpoint.AuthURL); err != nil {
r.Context(), "failed to generate state for starting OIDC flow", "error", err, http.Error(w, err.Error(), http.StatusInternalServerError)
)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return
} }
authURL := oauth2Config.AuthCodeURL(base64.URLEncoding.EncodeToString(state)) queryValues := authURL.Query()
queryValues.Set("client_id", clientID)
queryValues.Set("response_type", "code")
queryValues.Set("scope", "openid offline_access profile email")
queryValues.Set("state", base64.URLEncoding.EncodeToString(services.GenerateKey(oauth2RedirectStateLength)))
queryValues.Set("claims", getRequestedClaims(logger))
authURL.RawQuery = queryValues.Encode()
w.Header().Set("Location", authURL) w.Header().Set("Location", authURL.String())
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
}) })
} }
} }
func GetSession(r *http.Request) (*sessions.Session, error) { func getRequestedClaims(logger *log.Logger) string {
session, err := services.GetSessionStore().Get(r, sessionName) claims := make(models.OIDCClaimsRequest)
claims["userinfo"] = make(models.ClaimElement)
essentialItem := make(models.IndividualClaimRequest)
essentialItem["essential"] = true
claims["userinfo"]["https://auth.cacert.org/groups"] = &essentialItem
if err != nil { target := make([]byte, 0)
return nil, fmt.Errorf("could not get session") buf := bytes.NewBuffer(target)
enc := json.NewEncoder(buf)
if err := enc.Encode(claims); err != nil {
logger.WithError(err).Warn("could not encode claims request parameter")
} }
return session, nil return buf.String()
} }
type I18NHandler interface { func ParseIDToken(token string, keySet jwk.Set) (openid.Token, error) {
GetBundle() *i18n.Bundle var (
GetCatalog() *services.MessageCatalog parsedIDToken jwt.Token
} err error
)
func GetLocalizer(h I18NHandler, r *http.Request) *i18n.Localizer { if parsedIDToken, err = jwt.ParseString(token, jwt.WithKeySet(keySet), jwt.WithToken(openid.New())); err != nil {
accept := r.Header.Get("Accept-Language") return nil, fmt.Errorf("could not parse ID token: %w", err)
localizer := i18n.NewLocalizer(h.GetBundle(), accept)
return localizer
}
func BaseTemplateData(h I18NHandler, localizer *i18n.Localizer) map[string]interface{} {
msg := h.GetCatalog().LookupMessage
data := map[string]interface{}{
"WelcomeNavLabel": msg("IndexNavLabel", nil, localizer),
"ProtectedNavLabel": msg("ProtectedNavLabel", nil, localizer),
} }
return data if v, ok := parsedIDToken.(openid.Token); ok {
return v, nil
}
return nil, errors.New("ID token is no OpenID Connect Identity Token")
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -21,12 +21,11 @@ import (
"context" "context"
"fmt" "fmt"
"html/template" "html/template"
"log/slog" "io/fs"
"net/http" "net/http"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
log "github.com/sirupsen/logrus"
"code.cacert.org/cacert/oidc-demo-app/ui"
"code.cacert.org/cacert/oidc-demo-app/internal/services" "code.cacert.org/cacert/oidc-demo-app/internal/services"
) )
@ -47,34 +46,26 @@ type ErrorDetails struct {
type ErrorBucket struct { type ErrorBucket struct {
errorDetails *ErrorDetails errorDetails *ErrorDetails
templates *template.Template templates *template.Template
logger *slog.Logger logger *log.Logger
bundle *i18n.Bundle bundle *i18n.Bundle
messageCatalog *services.MessageCatalog messageCatalog *services.MessageCatalog
} }
func (b *ErrorBucket) GetBundle() *i18n.Bundle {
return b.bundle
}
func (b *ErrorBucket) GetCatalog() *services.MessageCatalog {
return b.messageCatalog
}
func (b *ErrorBucket) serveHTTP(w http.ResponseWriter, r *http.Request) { func (b *ErrorBucket) serveHTTP(w http.ResponseWriter, r *http.Request) {
if b.errorDetails != nil { if b.errorDetails != nil {
localizer := GetLocalizer(b, r) accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(b.bundle, accept)
data := BaseTemplateData(b, localizer) err := b.templates.Lookup("base").Execute(w, map[string]interface{}{
data["Title"] = b.messageCatalog.LookupMessage( "Title": b.messageCatalog.LookupMessage(
"ErrorTitle", "ErrorTitle",
nil, nil,
localizer, localizer,
) ),
data["details"] = b.errorDetails "details": b.errorDetails,
})
err := b.templates.Lookup("base").Execute(w, data)
if err != nil { if err != nil {
b.logger.Error("error rendering error template", "error", err) log.WithError(err).Error("error rendering error template")
http.Error( http.Error(
w, w,
http.StatusText(http.StatusInternalServerError), http.StatusText(http.StatusInternalServerError),
@ -143,12 +134,13 @@ func (w *errorResponseWriter) Write(content []byte) (int, error) {
} }
func ErrorHandling( func ErrorHandling(
logger *slog.Logger, logger *log.Logger,
templateFS fs.FS,
bundle *i18n.Bundle, bundle *i18n.Bundle,
messageCatalog *services.MessageCatalog, messageCatalog *services.MessageCatalog,
) (func(http.Handler) http.Handler, error) { ) (func(http.Handler) http.Handler, error) {
errorTemplates, err := template.ParseFS( errorTemplates, err := template.ParseFS(
ui.Templates, templateFS,
"templates/base.gohtml", "templates/base.gohtml",
"templates/errors.gohtml", "templates/errors.gohtml",
) )

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -20,136 +20,117 @@ package handlers
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"log/slog" "io/fs"
"net/http" "net/http"
"net/url" "net/url"
"github.com/lestrrat-go/jwx/jwk"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"code.cacert.org/cacert/oidc-demo-app/ui"
"code.cacert.org/cacert/oidc-demo-app/internal/services" "code.cacert.org/cacert/oidc-demo-app/internal/services"
) )
type IndexHandler struct { type IndexHandler struct {
bundle *i18n.Bundle bundle *i18n.Bundle
indexTemplate *template.Template indexTemplate *template.Template
logger *slog.Logger keySet jwk.Set
logoutURL string logoutURL string
messageCatalog *services.MessageCatalog messageCatalog *services.MessageCatalog
publicURL string publicURL string
tokenInfo *services.TokenInfoService
} }
func (h *IndexHandler) GetBundle() *i18n.Bundle { func (h *IndexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
return h.bundle if request.Method != http.MethodGet {
} http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
func (h *IndexHandler) GetCatalog() *services.MessageCatalog {
return h.messageCatalog
}
func (h *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return return
} }
if r.URL.Path != "/" { if request.URL.Path != "/" {
http.NotFound(w, r) http.NotFound(writer, request)
return return
} }
localizer := GetLocalizer(h, r) accept := request.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(h.bundle, accept)
writer.WriteHeader(http.StatusOK)
session, err := services.GetSessionStore().Get(request, sessionName)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
logoutURL, err := url.Parse(h.logoutURL) logoutURL, err := url.Parse(h.logoutURL)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(writer, err.Error(), http.StatusInternalServerError)
return return
} }
session, err := GetSession(r) var (
idToken string
ok bool
)
if idToken, ok = session.Values[sessionKeyIDToken].(string); ok {
logoutURL.RawQuery = url.Values{
"id_token_hint": []string{idToken},
"post_logout_redirect_uri": []string{fmt.Sprintf("%s/after-logout", h.publicURL)},
}.Encode()
} else {
return
}
oidcToken, err := ParseIDToken(idToken, h.keySet)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(writer, err.Error(), http.StatusInternalServerError)
return return
} }
tokenInfo, err := h.tokenInfo.GetTokenInfo(session) writer.Header().Add("Content-Type", "text/html")
if err != nil {
h.logger.Error("failed to get token info for request", "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !tokenInfo.Expires.IsZero() {
h.logger.Info("id token expires at", "expires", tokenInfo.Expires)
}
w.Header().Add("Content-Type", "text/html")
msgLookup := h.messageCatalog.LookupMessage msgLookup := h.messageCatalog.LookupMessage
data := BaseTemplateData(h, localizer) err = h.indexTemplate.Lookup("base").Execute(writer, map[string]interface{}{
"Title": msgLookup("IndexTitle", nil, localizer),
data["Title"] = msgLookup("IndexTitle", nil, localizer) "Greeting": msgLookup("IndexGreeting", map[string]interface{}{
data["Greeting"] = msgLookup("GreetingAnonymous", nil, localizer) "User": oidcToken.Name(),
data["LoginLabel"] = msgLookup("LoginLabel", nil, localizer) }, localizer),
data["IntroductionText"] = msgLookup("IndexIntroductionText", nil, localizer) "IntroductionText": msgLookup("IndexIntroductionText", nil, localizer),
data["IsAuthenticated"] = false "LogoutLabel": msgLookup("LogoutLabel", nil, localizer),
"LogoutURL": logoutURL.String(),
if tokenInfo.IDToken != "" { })
logoutURL.RawQuery = url.Values{
"id_token_hint": []string{tokenInfo.IDToken},
"post_logout_redirect_uri": []string{fmt.Sprintf("%s/after-logout", h.publicURL)},
}.Encode()
data["Greeting"] = msgLookup(
"GreetingAuthenticated", map[string]interface{}{"Name": tokenInfo.Name}, localizer,
)
data["LogoutLabel"] = msgLookup("LogoutLabel", nil, localizer)
data["LogoutURL"] = logoutURL.String()
data["IsAuthenticated"] = true
data["AuthenticatedAs"] = msgLookup("AuthenticatedAs", map[string]interface{}{
"Name": tokenInfo.Name,
"Email": tokenInfo.Email,
}, localizer)
}
err = h.indexTemplate.Lookup("base").Execute(w, data)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(writer, err.Error(), http.StatusInternalServerError)
return return
} }
} }
func NewIndexHandler( func NewIndexHandler(
logger *slog.Logger,
bundle *i18n.Bundle, bundle *i18n.Bundle,
catalog *services.MessageCatalog, catalog *services.MessageCatalog,
templateFS fs.FS,
oidcInfo *services.OIDCInformation, oidcInfo *services.OIDCInformation,
publicURL string, publicURL string,
tokenInfoService *services.TokenInfoService,
) (*IndexHandler, error) { ) (*IndexHandler, error) {
indexTemplate, err := template.ParseFS( indexTemplate, err := template.ParseFS(
ui.Templates, templateFS,
"templates/base.gohtml", "templates/index.gohtml") "templates/base.gohtml", "templates/index.gohtml")
if err != nil { if err != nil {
return nil, fmt.Errorf("could not parse templates: %w", err) return nil, fmt.Errorf("could not parse templates: %w", err)
} }
return &IndexHandler{ return &IndexHandler{
logger: logger,
bundle: bundle, bundle: bundle,
indexTemplate: indexTemplate, indexTemplate: indexTemplate,
keySet: oidcInfo.KeySet,
logoutURL: oidcInfo.OIDCConfiguration.EndSessionEndpoint, logoutURL: oidcInfo.OIDCConfiguration.EndSessionEndpoint,
tokenInfo: tokenInfoService,
messageCatalog: catalog, messageCatalog: catalog,
publicURL: publicURL, publicURL: publicURL,
}, nil }, nil

View file

@ -1,51 +0,0 @@
/*
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package handlers
import (
"net/http"
)
type LoginHandler struct{}
func (l *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
if r.URL.Path != "/login" {
http.NotFound(w, r)
return
}
next := r.URL.Query().Get("next")
if next != "" {
http.Redirect(w, r, next, http.StatusFound)
return
}
http.Redirect(w, r, "/", http.StatusFound)
}
func NewLoginHandler() *LoginHandler {
return &LoginHandler{}
}

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -20,9 +20,10 @@ package handlers
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"net/http" "net/http"
"sync/atomic" "sync/atomic"
log "github.com/sirupsen/logrus"
) )
type key int type key int
@ -63,7 +64,7 @@ func Logging(logger *log.Logger) func(http.Handler) http.Handler {
requestID = "unknown" requestID = "unknown"
} }
logger.Printf( logger.Infof(
"[%s] %s \"%s %s\" %d %d \"%s\"", "[%s] %s \"%s %s\" %d %d \"%s\"",
requestID, requestID,
r.RemoteAddr, r.RemoteAddr,
@ -96,7 +97,7 @@ func Tracing(nextRequestID func() string) func(http.Handler) http.Handler {
var Healthy int32 var Healthy int32
func NewHealthHandler() http.Handler { func NewHealthHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&Healthy) == 1 { if atomic.LoadInt32(&Healthy) == 1 {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -19,20 +19,26 @@ package handlers
import ( import (
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"time"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/lestrrat-go/jwx/jwk" "github.com/lestrrat-go/jwx/jwk"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"code.cacert.org/cacert/oidc-demo-app/internal/services" "code.cacert.org/cacert/oidc-demo-app/internal/services"
) )
const (
sessionKeyAccessToken = iota
sessionKeyRefreshToken
sessionKeyIDToken
sessionRedirectTarget
)
type OidcCallbackHandler struct { type OidcCallbackHandler struct {
keySet jwk.Set keySet jwk.Set
logger *slog.Logger logger *log.Logger
oauth2Config *oauth2.Config oauth2Config *oauth2.Config
} }
@ -59,22 +65,22 @@ func (c *OidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
tok, err := c.oauth2Config.Exchange(r.Context(), code) tok, err := c.oauth2Config.Exchange(r.Context(), code)
if err != nil { if err != nil {
c.logger.Error("could not perform token exchange", "error", err) c.logger.WithError(err).Error("could not perform token exchange")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
session, err := GetSession(r) session, err := services.GetSessionStore().Get(r, "resource_session")
if err != nil { if err != nil {
c.logger.Error("could not get session", "error", err) c.logger.WithError(err).Error("could not get session store")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
if err = c.storeTokens(session, tok); err != nil { if err = c.storeTokens(session, tok); err != nil {
c.logger.Error("could not store token in session", "error", err) c.logger.WithError(err).Error("could not store token in session")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -82,14 +88,14 @@ func (c *OidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
} }
if err = session.Save(r, w); err != nil { if err = session.Save(r, w); err != nil {
c.logger.Error("could not save session", "error", err) c.logger.WithError(err).Error("could not save session")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
if redirectTarget, ok := session.Values[services.SessionRedirectTarget]; ok { if redirectTarget, ok := session.Values[sessionRedirectTarget]; ok {
if v, ok := redirectTarget.(string); ok { if v, ok := redirectTarget.(string); ok {
w.Header().Set("Location", v) w.Header().Set("Location", v)
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
@ -124,8 +130,8 @@ func (c *OidcCallbackHandler) storeTokens(
session *sessions.Session, session *sessions.Session,
tok *oauth2.Token, tok *oauth2.Token,
) error { ) error {
session.Values[services.SessionAccessToken] = tok.AccessToken session.Values[sessionKeyAccessToken] = tok.AccessToken
session.Values[services.SessionRefreshToken] = tok.RefreshToken session.Values[sessionKeyRefreshToken] = tok.RefreshToken
idTokenValue := tok.Extra("id_token") idTokenValue := tok.Extra("id_token")
@ -134,29 +140,26 @@ func (c *OidcCallbackHandler) storeTokens(
return fmt.Errorf("ID token value %v is not a string", idTokenValue) return fmt.Errorf("ID token value %v is not a string", idTokenValue)
} }
session.Values[services.SessionIDToken] = idToken session.Values[sessionKeyIDToken] = idToken
session.Options.MaxAge = int(time.Until(tok.Expiry).Seconds()) oidcToken, err := ParseIDToken(idToken, c.keySet)
oidcToken, err := services.ParseIDToken(idToken, c.keySet)
if err != nil { if err != nil {
return fmt.Errorf("could not parse ID token: %w", err) return fmt.Errorf("could not parse ID token: %w", err)
} }
c.logger.Debug( c.logger.WithFields(log.Fields{
"receive OpenID Connect ID Token", "sub": oidcToken.Subject(),
"sub", oidcToken.Subject(), "aud": oidcToken.Audience(),
"aud", oidcToken.Audience(), "issued_at": oidcToken.IssuedAt(),
"issued_at", oidcToken.IssuedAt(), "iss": oidcToken.Issuer(),
"iss", oidcToken.Issuer(), "not_before": oidcToken.NotBefore(),
"not_before", oidcToken.NotBefore(), "exp": oidcToken.Expiration(),
"exp", oidcToken.Expiration(), }).Debug("receive OpenID Connect ID Token")
)
return nil return nil
} }
func NewCallbackHandler(logger *slog.Logger, keySet jwk.Set, oauth2Config *oauth2.Config) *OidcCallbackHandler { func NewCallbackHandler(logger *log.Logger, keySet jwk.Set, oauth2Config *oauth2.Config) *OidcCallbackHandler {
return &OidcCallbackHandler{ return &OidcCallbackHandler{
keySet: keySet, keySet: keySet,
logger: logger, logger: logger,

View file

@ -1,155 +0,0 @@
/*
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package handlers
import (
"fmt"
"html/template"
"log/slog"
"net/http"
"net/url"
"github.com/nicksnyder/go-i18n/v2/i18n"
"code.cacert.org/cacert/oidc-demo-app/internal/services"
"code.cacert.org/cacert/oidc-demo-app/ui"
)
type ProtectedResource struct {
bundle *i18n.Bundle
logger *slog.Logger
protectedTemplate *template.Template
logoutURL string
tokenInfo *services.TokenInfoService
messageCatalog *services.MessageCatalog
publicURL string
}
func (h *ProtectedResource) GetBundle() *i18n.Bundle {
return h.bundle
}
func (h *ProtectedResource) GetCatalog() *services.MessageCatalog {
return h.messageCatalog
}
func (h *ProtectedResource) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
if r.URL.Path != "/protected" {
http.NotFound(w, r)
return
}
localizer := GetLocalizer(h, r)
logoutURL, err := url.Parse(h.logoutURL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session, err := GetSession(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tokenInfo, err := h.tokenInfo.GetTokenInfo(session)
if err != nil {
h.logger.Error("failed to get token info for request", "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !tokenInfo.Expires.IsZero() {
h.logger.Info("id token expires at", "expires", tokenInfo.Expires)
}
if tokenInfo.IDToken == "" {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
w.Header().Add("Content-Type", "text/html")
msgLookup := h.messageCatalog.LookupMessage
logoutURL.RawQuery = url.Values{
"id_token_hint": []string{tokenInfo.IDToken},
"post_logout_redirect_uri": []string{fmt.Sprintf("%s/after-logout", h.publicURL)},
}.Encode()
data := BaseTemplateData(h, localizer)
data["Title"] = msgLookup("IndexTitle", nil, localizer)
data["Greeting"] = msgLookup(
"GreetingAuthenticated", map[string]interface{}{"Name": tokenInfo.Name}, localizer,
)
data["LoginLabel"] = msgLookup("LoginLabel", nil, localizer)
data["IntroductionText"] = msgLookup("ProtectedIntroductionText", nil, localizer)
data["IsAuthenticated"] = true
data["LogoutLabel"] = msgLookup("LogoutLabel", nil, localizer)
data["LogoutURL"] = logoutURL.String()
data["AuthenticatedAs"] = msgLookup("AuthenticatedAs", map[string]interface{}{
"Name": tokenInfo.Name,
"Email": tokenInfo.Email,
}, localizer)
err = h.protectedTemplate.Lookup("base").Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func NewProtectedResourceHandler(
logger *slog.Logger,
bundle *i18n.Bundle,
catalog *services.MessageCatalog,
oidcInfo *services.OIDCInformation,
publicURL string, tokenInfoService *services.TokenInfoService,
) (*ProtectedResource, error) {
protectedTemplate, err := template.ParseFS(
ui.Templates,
"templates/base.gohtml", "templates/protected.gohtml")
if err != nil {
return nil, fmt.Errorf("could not parse templates: %w", err)
}
return &ProtectedResource{
logger: logger,
bundle: bundle,
protectedTemplate: protectedTemplate,
logoutURL: oidcInfo.OIDCConfiguration.EndSessionEndpoint,
tokenInfo: tokenInfoService,
messageCatalog: catalog,
publicURL: publicURL,
}, nil
}

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -20,8 +20,6 @@ package handlers
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -29,18 +27,23 @@ import (
"time" "time"
"github.com/knadh/koanf" "github.com/knadh/koanf"
"github.com/sirupsen/logrus"
) )
func StartApplication( func StartApplication(
ctx context.Context, logger *slog.Logger, server *http.Server, publicURL string, config *koanf.Koanf, ctx context.Context,
) error { logger *logrus.Logger,
server *http.Server,
publicURL string,
config *koanf.Koanf,
) {
done := make(chan bool) done := make(chan bool)
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt) signal.Notify(quit, os.Interrupt)
go func() { go func() {
<-quit <-quit
logger.Info("Server is shutting down...") logger.Infoln("Server is shutting down...")
atomic.StoreInt32(&Healthy, 0) atomic.StoreInt32(&Healthy, 0)
const shutdownWaitTime = 30 * time.Second const shutdownWaitTime = 30 * time.Second
@ -52,25 +55,24 @@ func StartApplication(
server.SetKeepAlivesEnabled(false) server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil { if err := server.Shutdown(ctx); err != nil {
logger.Error("Could not gracefully shutdown the server", "error", err) logger.WithError(err).Fatal("Could not gracefully shutdown the server")
} }
close(done) close(done)
}() }()
logger.Info("Server is ready to handle requests", "public_url", publicURL) logger.WithField("public_url", publicURL).Info("Server is ready to handle requests")
atomic.StoreInt32(&Healthy, 1) atomic.StoreInt32(&Healthy, 1)
if err := server.ListenAndServeTLS( if err := server.ListenAndServeTLS(
config.String("server.certificate"), config.String("server.key"), config.String("server.certificate"), config.String("server.key"),
); err != nil && !errors.Is(err, http.ErrServerClosed) { ); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("Could not listen on requested address", "server_address", server.Addr) logger.WithError(err).WithField(
"server_address",
return fmt.Errorf("listening failed: %w", err) server.Addr,
).Fatal("Could not listen on requested address")
} }
<-done <-done
logger.Info("Server stopped") logger.Infoln("Server stopped")
return nil
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -19,7 +19,6 @@ package services
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"strings" "strings"
@ -29,13 +28,11 @@ import (
"github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag" "github.com/knadh/koanf/providers/posflag"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
const ( const defaultServerPort = 4000
defaultServerPort = 4000
defaultFile = "resource_app.toml"
)
var DefaultConfiguration = map[string]interface{}{ var DefaultConfiguration = map[string]interface{}{
"server.bind_address": "", "server.bind_address": "",
@ -51,12 +48,13 @@ var DefaultConfiguration = map[string]interface{}{
} }
func ConfigureApplication( func ConfigureApplication(
logger *logrus.Logger,
appName string, appName string,
defaultConfig map[string]interface{}, defaultConfig map[string]interface{},
) (*koanf.Koanf, error) { ) (*koanf.Koanf, error) {
f := pflag.NewFlagSet("config", pflag.ContinueOnError) f := pflag.NewFlagSet("config", pflag.ContinueOnError)
f.Usage = func() { f.Usage = func() {
log.Print(f.FlagUsages()) logger.Info(f.FlagUsages())
os.Exit(0) os.Exit(0)
} }
@ -69,26 +67,29 @@ func ConfigureApplication(
var err error var err error
if err = f.Parse(os.Args[1:]); err != nil { if err = f.Parse(os.Args[1:]); err != nil {
return nil, fmt.Errorf("could not parse command line arguments: %w", err) logger.WithError(err).Fatal("could not parse command line arguments")
} }
config := koanf.New(".") config := koanf.New(".")
_ = config.Load(confmap.Provider(defaultConfig, "."), nil) _ = config.Load(confmap.Provider(defaultConfig, "."), nil)
if err = config.Load(file.Provider(defaultFile), toml.Parser()); err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("could not load configuration from file %s: %w", defaultFile, err)
}
cFiles, _ := f.GetStringSlice("conf") cFiles, _ := f.GetStringSlice("conf")
for _, c := range cFiles { for _, c := range cFiles {
if err = config.Load(file.Provider(c), toml.Parser()); err != nil { if err = config.Load(file.Provider(c), toml.Parser()); err != nil {
return nil, fmt.Errorf("error loading configuration from file %s: %w", c, err) logger.WithError(err).WithField("file", c).Fatal("error loading configuration from file")
} }
} }
if err = config.Load(posflag.Provider(f, ".", config), nil); err != nil { if err = config.Load(posflag.Provider(f, ".", config), nil); err != nil {
return nil, fmt.Errorf("error loading configuration from command line: %w", err) logger.WithError(err).Fatal("error loading configuration from command line")
}
if err = config.Load(
file.Provider("resource_app.toml"),
toml.Parser(),
); err != nil && !os.IsNotExist(err) {
logrus.WithError(err).Fatal("error loading configuration from resource_app.toml")
} }
prefix := fmt.Sprintf("%s_", strings.ToUpper(appName)) prefix := fmt.Sprintf("%s_", strings.ToUpper(appName))
@ -96,7 +97,7 @@ func ConfigureApplication(
if err = config.Load(env.Provider(prefix, ".", func(s string) string { if err = config.Load(env.Provider(prefix, ".", func(s string) string {
return strings.ReplaceAll(strings.ToLower(strings.TrimPrefix(s, prefix)), "_", ".") return strings.ReplaceAll(strings.ToLower(strings.TrimPrefix(s, prefix)), "_", ".")
}), nil); err != nil { }), nil); err != nil {
return nil, fmt.Errorf("error loading configuration from environment variables: %w", err) logrus.WithError(err).Fatal("error loading configuration from environment")
} }
return config, nil return config, nil

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -20,7 +20,6 @@ package services
import ( import (
"errors" "errors"
"fmt" "fmt"
"log/slog"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language" "golang.org/x/text/language"
@ -28,32 +27,19 @@ import (
"code.cacert.org/cacert/oidc-demo-app/translations" "code.cacert.org/cacert/oidc-demo-app/translations"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
log "github.com/sirupsen/logrus"
) )
func AddMessages(catalog *MessageCatalog) { func AddMessages(catalog *MessageCatalog) {
messages := make(map[string]*i18n.Message) messages := make(map[string]*i18n.Message)
messages["AuthenticatedAs"] = &i18n.Message{ messages["IndexGreeting"] = &i18n.Message{
ID: "AuthenticatedAs", ID: "IndexGreeting",
Other: "The identity provider authenticated your identity as {{ .Name }}" + Other: "Hello {{ .User }}",
" with the email address {{ .Email }}.",
}
messages["GreetingAnonymous"] = &i18n.Message{
ID: "GreetingAnonymous",
Other: "Hello",
}
messages["GreetingAuthenticated"] = &i18n.Message{
ID: "GreetingAuthenticated",
Other: "Hello {{ .Name }}",
} }
messages["IndexTitle"] = &i18n.Message{ messages["IndexTitle"] = &i18n.Message{
ID: "IndexTitle", ID: "IndexTitle",
Other: "Welcome to the Demo application", Other: "Welcome to the Demo application",
} }
messages["LoginLabel"] = &i18n.Message{
ID: "LoginLabel",
Description: "A label on a login button or link",
Other: "Login",
}
messages["LogoutLabel"] = &i18n.Message{ messages["LogoutLabel"] = &i18n.Message{
ID: "LogoutLabel", ID: "LogoutLabel",
Description: "A label on a logout button or link", Description: "A label on a logout button or link",
@ -61,21 +47,7 @@ func AddMessages(catalog *MessageCatalog) {
} }
messages["IndexIntroductionText"] = &i18n.Message{ messages["IndexIntroductionText"] = &i18n.Message{
ID: "IndexIntroductionText", ID: "IndexIntroductionText",
Other: "This is a public resource.", Other: "This is an authorization protected resource",
}
messages["IndexNavLabel"] = &i18n.Message{
ID: "IndexNavLabel",
Description: "Label for the index page in the top navigation",
Other: "Welcome",
}
messages["ProtectedIntroductionText"] = &i18n.Message{
ID: "ProtectedIntroductionText",
Other: "This is an authorization protected resource.",
}
messages["ProtectedNavLabel"] = &i18n.Message{
ID: "ProtectedNavLabel",
Description: "Label for the protected resource page in the top navigation",
Other: "Protected area",
} }
catalog.AddMessages(messages) catalog.AddMessages(messages)
@ -83,7 +55,7 @@ func AddMessages(catalog *MessageCatalog) {
type MessageCatalog struct { type MessageCatalog struct {
messages map[string]*i18n.Message messages map[string]*i18n.Message
logger *slog.Logger logger *log.Logger
} }
func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) { func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) {
@ -99,14 +71,13 @@ func (m *MessageCatalog) LookupErrorMessage(
localizer *i18n.Localizer, localizer *i18n.Localizer,
) string { ) string {
fieldTag := fmt.Sprintf("%s-%s", field, tag) fieldTag := fmt.Sprintf("%s-%s", field, tag)
message, ok := m.messages[fieldTag] message, ok := m.messages[fieldTag]
if !ok { if !ok {
m.logger.Info("no specific error message for field and tag", "field_tag", fieldTag) m.logger.WithField("field_tag", fieldTag).Info("no specific error message for field and tag")
message, ok = m.messages[tag] message, ok = m.messages[tag]
if !ok { if !ok {
m.logger.Info("no specific error message for tag", "tag", tag) m.logger.WithField("tag", tag).Info("no specific error message for tag")
message, ok = m.messages["unknown"] message, ok = m.messages["unknown"]
if !ok { if !ok {
@ -124,7 +95,7 @@ func (m *MessageCatalog) LookupErrorMessage(
}, },
}) })
if err != nil { if err != nil {
m.logger.Error("localization failed", "error", err) m.logger.WithError(err).Error("localization failed")
return tag return tag
} }
@ -149,7 +120,7 @@ func (m *MessageCatalog) LookupMessage(
return translation return translation
} }
m.logger.Warn("no translation found for id", "id", id) m.logger.WithField("id", id).Warn("no translation found for id")
return id return id
} }
@ -158,19 +129,19 @@ func (m *MessageCatalog) handleLocalizeError(id string, translation string, err
var messageNotFound *i18n.MessageNotFoundErr var messageNotFound *i18n.MessageNotFoundErr
if errors.As(err, &messageNotFound) { if errors.As(err, &messageNotFound) {
m.logger.Warn("message not found", "error", err, "message", id) m.logger.WithError(err).WithField("message", id).Warn("message not found")
if translation != "" { if translation != "" {
return translation return translation
} }
} else { } else {
m.logger.Error("translation error", "error", err, "message", id) m.logger.WithError(err).WithField("message", id).Error("translation error")
} }
return id return id
} }
func InitI18n(logger *slog.Logger, languages []string) (*i18n.Bundle, *MessageCatalog) { func InitI18n(logger *log.Logger, languages []string) (*i18n.Bundle, *MessageCatalog) {
bundle := i18n.NewBundle(language.English) bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
@ -179,7 +150,7 @@ func InitI18n(logger *slog.Logger, languages []string) (*i18n.Bundle, *MessageCa
bundleBytes, err := translations.Bundles.ReadFile(bundleName) bundleBytes, err := translations.Bundles.ReadFile(bundleName)
if err != nil { if err != nil {
logger.Warn("message bundle not found", "bundle", bundleName) logger.WithField("bundle", bundleName).Warn("message bundle not found")
continue continue
} }
@ -192,7 +163,7 @@ func InitI18n(logger *slog.Logger, languages []string) (*i18n.Bundle, *MessageCa
return bundle, catalog return bundle, catalog
} }
func initMessageCatalog(logger *slog.Logger) *MessageCatalog { func initMessageCatalog(logger *log.Logger) *MessageCatalog {
messages := make(map[string]*i18n.Message) messages := make(map[string]*i18n.Message)
messages["ErrorTitle"] = &i18n.Message{ messages["ErrorTitle"] = &i18n.Message{
ID: "ErrorTitle", ID: "ErrorTitle",

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -22,12 +22,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
"github.com/lestrrat-go/jwx/jwk" "github.com/lestrrat-go/jwx/jwk"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"code.cacert.org/cacert/oidc-demo-app/internal/models" "code.cacert.org/cacert/oidc-demo-app/internal/models"
@ -57,18 +57,16 @@ type OIDCInformation struct {
// retrieved by GetOAuth2Config. // retrieved by GetOAuth2Config.
// //
// The JSON Web Key Set can be retrieved by GetJwkSet. // The JSON Web Key Set can be retrieved by GetJwkSet.
func DiscoverOIDC(logger *slog.Logger, params *OidcParams) (*OIDCInformation, error) { func DiscoverOIDC(logger *log.Logger, params *OidcParams) (*OIDCInformation, error) {
discoveryURL, err := url.Parse(params.OidcServer) discoveryURL, err := url.Parse(params.OidcServer)
if err != nil { if err != nil {
logger.Error( logger.WithError(err).WithField(
"could not parse parameter oidc.server as URL", "oidc.server",
"oidc.server", params.OidcServer, params.OidcServer,
) ).Fatal("could not parse parameter value")
} else {
return nil, fmt.Errorf("could not parse parameter value: %w", err)
}
discoveryURL.Path = "/.well-known/openid-configuration" discoveryURL.Path = "/.well-known/openid-configuration"
}
var ( var (
body []byte body []byte
@ -106,7 +104,7 @@ func DiscoverOIDC(logger *slog.Logger, params *OidcParams) (*OIDCInformation, er
AuthURL: discoveryResponse.AuthorizationEndpoint, AuthURL: discoveryResponse.AuthorizationEndpoint,
TokenURL: discoveryResponse.TokenEndpoint, TokenURL: discoveryResponse.TokenEndpoint,
}, },
Scopes: []string{"openid", "email", "profile"}, Scopes: []string{"openid", "offline"},
} }
const jwkFetchTimeout = 10 * time.Second const jwkFetchTimeout = 10 * time.Second

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -19,20 +19,21 @@ package services
import ( import (
"crypto/rand" "crypto/rand"
"fmt"
log "github.com/sirupsen/logrus"
) )
func GenerateKey(length int) ([]byte, error) { func GenerateKey(length int) []byte {
key := make([]byte, length) key := make([]byte, length)
read, err := rand.Read(key) read, err := rand.Read(key)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not generate key: %w", err) log.WithError(err).Fatal("could not generate key")
} }
if read != length { if read != length {
return nil, fmt.Errorf("read unexpected number of bytes, read %d, expected %d", read, length) log.WithFields(log.Fields{"read": read, "expected": length}).Fatal("read unexpected number of bytes")
} }
return key, nil return key
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -18,33 +18,24 @@ limitations under the License.
package services package services
import ( import (
"fmt"
"os" "os"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
log "github.com/sirupsen/logrus"
) )
var store *sessions.FilesystemStore var store *sessions.FilesystemStore
const ( func InitSessionStore(logger *log.Logger, sessionPath string, keys ...[]byte) {
SessionAccessToken = iota
SessionRefreshToken
SessionIDToken
SessionRedirectTarget
)
func InitSessionStore(sessionPath string, keys ...[]byte) error {
store = sessions.NewFilesystemStore(sessionPath, keys...) store = sessions.NewFilesystemStore(sessionPath, keys...)
if _, err := os.Stat(sessionPath); err != nil { if _, err := os.Stat(sessionPath); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
if err = os.MkdirAll(sessionPath, 0700); err != nil { //nolint:mnd if err = os.MkdirAll(sessionPath, 0700); err != nil { //nolint:gomnd
return fmt.Errorf("could not create session store director: %w", err) logger.WithError(err).Fatal("could not create session store director")
} }
} }
} }
return nil
} }
func GetSessionStore() *sessions.FilesystemStore { func GetSessionStore() *sessions.FilesystemStore {

View file

@ -1,100 +0,0 @@
/*
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package services
import (
"errors"
"fmt"
"log/slog"
"time"
"github.com/gorilla/sessions"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/jwt/openid"
)
type OIDCTokenInfo struct {
AccessToken string
IDToken string
RefreshToken string
Expires time.Time
Name string
Email string
}
type TokenInfoService struct {
logger *slog.Logger
keySet jwk.Set
}
func (s *TokenInfoService) GetTokenInfo(session *sessions.Session) (*OIDCTokenInfo, error) {
tokenInfo := &OIDCTokenInfo{}
var ok bool
if tokenInfo.AccessToken, ok = session.Values[SessionAccessToken].(string); ok {
s.logger.Debug("found access token in session", "access_token", tokenInfo.AccessToken)
}
if tokenInfo.RefreshToken, ok = session.Values[SessionRefreshToken].(string); ok {
s.logger.Debug("found refresh token in session", "refresh_token", tokenInfo.RefreshToken)
}
if tokenInfo.IDToken, ok = session.Values[SessionIDToken].(string); ok {
s.logger.Debug("found ID token in session", "id_token", tokenInfo.IDToken)
}
if tokenInfo.IDToken == "" {
return tokenInfo, nil
}
oidcToken, err := ParseIDToken(tokenInfo.IDToken, s.keySet)
if err != nil {
return nil, fmt.Errorf("could not parse ID token: %w", err)
}
tokenInfo.Expires = oidcToken.Expiration()
tokenInfo.Name = oidcToken.Name()
tokenInfo.Email = oidcToken.Email()
return tokenInfo, nil
}
func InitTokenInfoService(logger *slog.Logger, oidcInfo *OIDCInformation) (*TokenInfoService, error) {
return &TokenInfoService{logger: logger, keySet: oidcInfo.KeySet}, nil
}
func ParseIDToken(token string, keySet jwk.Set) (openid.Token, error) {
var (
parsedIDToken jwt.Token
err error
)
if parsedIDToken, err = jwt.ParseString(token, jwt.WithKeySet(keySet), jwt.WithToken(openid.New())); err != nil {
return nil, fmt.Errorf("could not parse ID token: %w", err)
}
if v, ok := parsedIDToken.(openid.Token); ok {
return v, nil
}
return nil, errors.New("ID token is no OpenID Connect Identity Token")
}

View file

@ -1,47 +1,20 @@
[AuthenticatedAs]
hash = "sha1-58e33592c806dab9cddd3693c0cfee64a07a0a9b"
other = "Der Identity-Provider hat dich als {{ .Name }} mit der E-Mail-Adresse {{ .Email }} identifiziert."
[ErrorTitle] [ErrorTitle]
hash = "sha1-736aec25a98f5ec5b71400bb0163f891f509b566" hash = "sha1-736aec25a98f5ec5b71400bb0163f891f509b566"
other = "Es ist ein Fehler aufgetreten" other = "Es ist ein Fehler aufgetreten"
[GreetingAnonymous] [IndexGreeting]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0" hash = "sha1-d4a13058e497fa24143ea96d50d82b818455ef61"
other = "Hallo" other = "Hallo {{ .User }}"
[GreetingAuthenticated]
hash = "sha1-22e08bfee49f285ac06df7d582c2a65bab86fa35"
other = "Hallo {{ .Name }}"
[IndexIntroductionText] [IndexIntroductionText]
hash = "sha1-e190189ce0b76d957315332b6ca336f90a4d3d8c" hash = "sha1-c2c530e263fc9c38482338ed290aafb496794179"
other = "Das ist eine öffentliche Resource." other = "Das ist eine zugriffsgeschützte Resource"
[IndexNavLabel]
description = "Label for the index page in the top navigation"
hash = "sha1-f99709e3c9205f21ca31811feec86519b6c1b452"
other = "Willkommen"
[IndexTitle] [IndexTitle]
hash = "sha1-eccb2b889c068d3f25496c1dad3fb0f88d021bd9" hash = "sha1-eccb2b889c068d3f25496c1dad3fb0f88d021bd9"
other = "Willkommen in der Demo-Anwendung" other = "Willkommen in der Demo-Anwendung"
[LoginLabel]
description = "A label on a login button or link"
hash = "sha1-2049c9ee0610e70f7316f98415755b58067e68ee"
other = "Anmelden"
[LogoutLabel] [LogoutLabel]
description = "A label on a logout button or link" description = "A label on a logout button or link"
hash = "sha1-8acfdeb9a8286f00c8e5dd48471cfdc994807579" hash = "sha1-8acfdeb9a8286f00c8e5dd48471cfdc994807579"
other = "Ausloggen" other = "Ausloggen"
[ProtectedIntroductionText]
hash = "sha1-87cd7874f28dfcebcb5460d30b6c2c78dff8f6a4"
other = "Das ist eine zugriffsgeschützte Resource."
[ProtectedNavLabel]
description = "Label for the protected resource page in the top navigation"
hash = "sha1-185c4cf5675f44de72ff76f16e6cab2a82afa752"
other = "Geschützter Bereich"

View file

@ -1,23 +1,8 @@
AuthenticatedAs = "The identity provider authenticated your identity as {{ .Name }} with the email address {{ .Email }}."
ErrorTitle = "An error has occurred" ErrorTitle = "An error has occurred"
GreetingAnonymous = "Hello" IndexGreeting = "Hello {{ .User }}"
GreetingAuthenticated = "Hello {{ .Name }}" IndexIntroductionText = "This is an authorization protected resource"
IndexIntroductionText = "This is a public resource."
IndexTitle = "Welcome to the Demo application" IndexTitle = "Welcome to the Demo application"
ProtectedIntroductionText = "This is an authorization protected resource."
[IndexNavLabel]
description = "Label for the index page in the top navigation"
other = "Welcome"
[LoginLabel]
description = "A label on a login button or link"
other = "Login"
[LogoutLabel] [LogoutLabel]
description = "A label on a logout button or link" description = "A label on a logout button or link"
other = "Logout" other = "Logout"
[ProtectedNavLabel]
description = "Label for the protected resource page in the top navigation"
other = "Protected area"

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");

View file

@ -27,21 +27,12 @@
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
</head> </head>
<body class="resource-app d-flex flex-column h-100"> <body class="resource-app d-flex flex-column h-100">
<header class="container flex-shrink-0"> <main role="main" class="flex-shrink-0">
<a href="/"><img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4"></a>
<ul class="nav">
<li class="nav-item">
<a class="nav-link" href="/">{{ .WelcomeNavLabel }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/protected">{{ .ProtectedNavLabel }}</a>
</li>
</ul>
</header>
{{ template "content" . }} {{ template "content" . }}
</main>
<footer class="footer mt-auto py-3"> <footer class="footer mt-auto py-3">
<div class="container"> <div class="container">
<span class="text-muted small">© <a href="https://www.cacert.org/">CAcert</a></span> <span class="text-muted small">© 2020-2023 <a href="https://www.cacert.org/">CAcert</a></span>
</div> </div>
</footer> </footer>
<script type="text/javascript" src="/js/cacert.bundle.js"></script> <script type="text/javascript" src="/js/cacert.bundle.js"></script>

View file

@ -1,5 +1,6 @@
{{ define "content" }} {{ define "content" }}
<main class="container error-message"> <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> <h1>{{ .Title }}</h1>
<h2>{{ if .details.ErrorCode }} <h2>{{ if .details.ErrorCode }}
<strong>{{ .details.ErrorCode }}</strong> {{ end }}{{ .details.ErrorMessage }}</h2> <strong>{{ .details.ErrorCode }}</strong> {{ end }}{{ .details.ErrorMessage }}</h2>
@ -8,5 +9,5 @@
<p>{{ . }}</p> <p>{{ . }}</p>
{{ end }} {{ end }}
{{ end }} {{ end }}
</main> </div>
{{ end }} {{ end }}

View file

@ -1,12 +1,8 @@
{{ define "content" }} {{ define "content" }}
<main class="container"> <div class="container">
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
<h1>{{ .Greeting }}</h1> <h1>{{ .Greeting }}</h1>
<p>{{ .IntroductionText }}</p> <p>{{ .IntroductionText }}</p>
{{ if .IsAuthenticated }}
<p>{{ .AuthenticatedAs }}</p>
<a class="btn btn-outline-primary" href="{{ .LogoutURL }}">{{ .LogoutLabel }}</a> <a class="btn btn-outline-primary" href="{{ .LogoutURL }}">{{ .LogoutLabel }}</a>
{{ else }} </div>
<a class="btn btn-primary" href="/login">{{ .LoginLabel }}</a>
{{ end }}
</main>
{{ end }} {{ end }}

View file

@ -1,8 +0,0 @@
{{ define "content" }}
<main class="container">
<h1>{{ .Greeting }}</h1>
<p>{{ .IntroductionText }}</p>
<p>{{ .AuthenticatedAs }}</p>
<a class="btn btn-outline-primary" href="{{ .LogoutURL }}">{{ .LogoutLabel }}</a>
</main>
{{ end }}

View file

@ -1,5 +1,5 @@
/* /*
Copyright CAcert Inc. Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");