Compare commits

...

10 commits
0.2.0 ... main

Author SHA1 Message Date
be15b18259 Fix linter warnings 2024-05-12 12:12:27 +02:00
9f53e37693 Remove copyright years from base template 2024-05-12 12:03:19 +02:00
c4724723b6 Switch from logrus to log/slog
Use log/slog from the Go standard library to reduce dependencies.
2024-05-12 12:02:27 +02:00
291c1857c6 Update dependencies 2024-05-12 10:20:19 +02:00
c607cdae94 Update linter, remove copyright years
Git has all history information, copyright years don't provide useful additional information.
2024-05-12 10:20:06 +02:00
ae86e52d40 Release 0.3.0 2023-08-07 15:19:03 +02:00
815c8e792a Use oauth2.AuthCodeURL to simplify Authenticate 2023-08-03 23:54:03 +02:00
7ec9e393e0 Add separate protected resource page
This commit adds a separate protected resource page to demonstrate how
to selectively require logins.

Add code to improve client performance by providing modification timestamps
and Cache-Control headers for embedded static files.
2023-08-03 16:46:28 +02:00
0a4cc75bd3 HTML improvements
- link from logo to start page
- move common page header to templates/base.gohtml
2023-08-03 13:04:10 +02:00
9ad06a2935 Improve token handling
- add identity information to the index page
- let the session expire when the token expires
2023-07-30 16:48:26 +02:00
32 changed files with 912 additions and 385 deletions

1
.gitignore vendored
View file

@ -5,6 +5,7 @@
/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 {{ YEAR-RANGE }} {{ ORGANIZATION }} Copyright {{ 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
- gomnd - mnd
- gosec - gosec
- lll - lll
- makezero - makezero

View file

@ -18,13 +18,12 @@ go.sum: go.mod
go mod tidy -v go mod tidy -v
translations: $(TRANSLATIONS) $(GOFILES) translations: $(TRANSLATIONS) $(GOFILES)
cd translations ; \ if [ ! -z "$(wildcard translations/translate.*.toml)" ]; then \
goi18n extract .. ; \
goi18n merge active.*.toml ; \
if translate.*.toml 2>/dev/null; then \
echo "missing translations"; \ echo "missing translations"; \
goi18n merge active.*.toml translate.*.toml; \ goi18n merge -outdir translations translations/active.*.toml translations/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,6 +5,16 @@ 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] ## [0.2.0]
### Changed ### Changed
- re-order configuration precedence - re-order configuration precedence

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020-2023 CAcert Inc. Copyright 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,6 +23,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"time" "time"
@ -30,7 +31,6 @@ 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,59 +53,89 @@ var (
date = "unknown" date = "unknown"
) )
func main() { type StaticFSWrapper struct {
logger := log.New() http.FileSystem
ModTime time.Time
}
config, err := services.ConfigureApplication( func (f *StaticFSWrapper) Open(name string) (http.File, error) {
logger, file, err := f.FileSystem.Open(name)
"RESOURCE_APP",
services.DefaultConfiguration, return &StaticFileWrapper{File: file, fixedModTime: f.ModTime}, err //nolint:wrapcheck
}
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 {
log.Fatalf("error loading configuration: %v", err) logger.Error("error loading configuration", "error", 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")
if level := config.String("log.level"); level != "" { logger.Info(
logLevel, err := log.ParseLevel(level) "Starting CAcert OpenID Connect demo application",
if err != nil { "version", version, "commit", commit, "date", date,
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 demo application")
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 := &tls.Config{ tlsClientConfig, err := getTLSConfig(config)
MinVersion: tls.VersionTLS12, if err != nil {
} logger.Error("error loading tls config", "error", err)
os.Exit(1)
if config.Exists("api-client.rootCAs") {
rootCAFile := config.MustString("api-client.rootCAs")
caCertPool := x509.NewCertPool()
pemBytes, err := os.ReadFile(rootCAFile)
if err != nil {
log.Fatalf("could not read CA certificate file: %v", err)
}
caCertPool.AppendCertsFromPEM(pemBytes)
tlsClientConfig.RootCAs = caCertPool
} }
apiTransport := &http.Transport{TLSClientConfig: tlsClientConfig} apiTransport := &http.Transport{TLSClientConfig: tlsClientConfig}
@ -118,45 +148,77 @@ func main() {
APIClient: apiClient, APIClient: apiClient,
}) })
if err != nil { if err != nil {
log.Fatalf("OpenID Connect discovery failed: %s", err) logger.Error("OpenID Connect discovery failed", "error", err)
os.Exit(1)
} }
sessionPath, sessionAuthKey, sessionEncKey := configureSessionParameters(config) sessionPath, sessionAuthKey, sessionEncKey, err := configureSessionParameters(logger, config)
services.InitSessionStore(logger, sessionPath, sessionAuthKey, sessionEncKey) if err != nil {
logger.Error("error configuring session parameters", "error", err)
os.Exit(1)
}
authMiddleware := handlers.Authenticate(logger, oidcInfo.OAuth2Config, oidcClientID) if err := services.InitSessionStore(sessionPath, sessionAuthKey, sessionEncKey); err != nil {
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"))
indexHandler, err := handlers.NewIndexHandler(bundle, catalog, ui.Templates, oidcInfo, publicURL) tokenInfoService, err := services.InitTokenInfoService(logger, oidcInfo)
if err != nil { if err != nil {
logger.WithError(err).Fatal("could not initialize index handler") logger.Error("could not initialize token info service", "error", err)
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("/", authMiddleware(indexHandler)) router.Handle("/", 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.Handle("/images/", staticFiles) router.HandleFunc("/images/", staticFiles)
router.Handle("/css/", staticFiles) router.HandleFunc("/css/", staticFiles)
router.Handle("/js/", staticFiles) router.HandleFunc("/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(logger) logging := handlers.Logging(logLogger)
hsts := handlers.EnableHSTS() hsts := handlers.EnableHSTS()
errorMiddleware, err := handlers.ErrorHandling(logger, ui.Templates, bundle, catalog) errorMiddleware, err := handlers.ErrorHandling(logger, bundle, catalog)
if err != nil { if err != nil {
logger.WithError(err).Fatal("could not initialize request error handling") logger.Error("could not initialize request error handling", "error", err)
os.Exit(1)
} }
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
@ -173,7 +235,50 @@ func main() {
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
} }
handlers.StartApplication(context.Background(), logger, server, publicURL, config) if err := handlers.StartApplication(context.Background(), logger, server, publicURL, config); err != nil {
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 {
@ -186,28 +291,36 @@ func buildPublicURL(hostname string, port int) string {
return fmt.Sprintf("https://%s", hostname) return fmt.Sprintf("https://%s", hostname)
} }
func configureSessionParameters(config *koanf.Koanf) (string, []byte, []byte) { func configureSessionParameters(logger *slog.Logger, config *koanf.Koanf) (string, []byte, []byte, error) {
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 {
log.WithError(err).Fatal("could not decode session auth key") return "", nil, nil, fmt.Errorf("could not decode session authentication key: %w", err)
} }
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 {
log.WithError(err).Fatal("could not decode session encryption key") return "", nil, nil, fmt.Errorf("could not decode session encryption key: %w", err)
} }
generated := false generated := false
if len(sessionAuthKey) != sessionAuthKeyLength { if len(sessionAuthKey) != sessionAuthKeyLength {
sessionAuthKey = services.GenerateKey(sessionAuthKeyLength) sessionAuthKey, err = 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 = services.GenerateKey(sessionKeyLength) sessionEncKey, err = services.GenerateKey(sessionKeyLength)
if err != nil {
return "", nil, nil, fmt.Errorf("could not generate session encryption key: %w", err)
}
generated = true generated = true
} }
@ -219,11 +332,12 @@ func configureSessionParameters(config *koanf.Koanf) (string, []byte, []byte) {
tomlData, err := config.Marshal(toml.Parser()) tomlData, err := config.Marshal(toml.Parser())
if err != nil { if err != nil {
log.WithError(err).Fatal("could not encode session config") return "", nil, nil, fmt.Errorf("could not encode session configuration: %w", err)
} }
log.Infof("put the following in your resource_app.toml:\n%s", string(tomlData)) logger.Info("put the following in your resource_app.toml")
fmt.Print(string(tomlData)) //nolint:forbidigo
} }
return sessionPath, sessionAuthKey, sessionEncKey return sessionPath, sessionAuthKey, sessionEncKey, nil
} }

29
go.mod
View file

@ -1,27 +1,25 @@
module code.cacert.org/cacert/oidc-demo-app module code.cacert.org/cacert/oidc-demo-app
go 1.19 go 1.22
require ( require (
github.com/BurntSushi/toml v1.3.2 github.com/BurntSushi/toml v1.3.2
github.com/gorilla/sessions v1.2.1 github.com/gorilla/sessions v1.2.2
github.com/knadh/koanf v1.5.0 github.com/knadh/koanf v1.5.0
github.com/lestrrat-go/jwx v1.2.26 github.com/lestrrat-go/jwx v1.2.29
github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/nicksnyder/go-i18n/v2 v2.4.0
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.10.0 golang.org/x/oauth2 v0.20.0
golang.org/x/text v0.11.0 golang.org/x/text v0.15.0
) )
require ( require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/gorilla/securecookie v1.1.2 // 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.1 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // 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
@ -30,9 +28,6 @@ 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.11.0 // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.12.0 // indirect golang.org/x/sys v0.20.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,7 +1,6 @@
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=
@ -39,8 +38,9 @@ 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.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
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,8 +84,6 @@ 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=
@ -98,12 +96,15 @@ 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.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
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=
@ -165,14 +166,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.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/blackmagic v1.0.2/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.26 h1:4iFo8FPRZGDYe1t19mQP0zTRqA7n8HnJ5lkIiDvJcB0= github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ=
github.com/lestrrat-go/jwx v1.2.26/go.mod h1:MaiCdGbn3/cckbOFSCluJlJMmp9dmZm5hDuIkx8ftpQ= github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8=
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=
@ -209,8 +210,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.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA= github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0= github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
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=
@ -251,14 +252,13 @@ 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,8 +267,9 @@ 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=
@ -285,9 +286,10 @@ 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.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
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=
@ -306,7 +308,6 @@ 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=
@ -320,13 +321,12 @@ 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.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
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.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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,17 +370,19 @@ 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.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=
@ -388,11 +390,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.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
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=
@ -413,8 +415,6 @@ 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,8 +440,6 @@ 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 2020-2023 CAcert Inc. Copyright 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,21 +18,18 @@ 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 *logrus.Logger logger *slog.Logger
} }
func (h *AfterLogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *AfterLogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
session, err := services.GetSessionStore().Get(r, sessionName) session, err := GetSession(r)
if err != nil { if err != nil {
h.logger.WithError(err).Error("could not get session") h.logger.Error("could not get session", "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -41,13 +38,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.WithError(err).Error("could not save session") h.logger.Error("could not save session", "error", err)
} }
w.Header().Set("Location", "/") w.Header().Set("Location", "/")
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
} }
func NewAfterLogoutHandler(logger *logrus.Logger) *AfterLogoutHandler { func NewAfterLogoutHandler(logger *slog.Logger) *AfterLogoutHandler {
return &AfterLogoutHandler{logger: logger} return &AfterLogoutHandler{logger: logger}
} }

View file

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

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020-2023 CAcert Inc. Copyright 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,11 +21,12 @@ import (
"context" "context"
"fmt" "fmt"
"html/template" "html/template"
"io/fs" "log/slog"
"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"
) )
@ -46,26 +47,34 @@ type ErrorDetails struct {
type ErrorBucket struct { type ErrorBucket struct {
errorDetails *ErrorDetails errorDetails *ErrorDetails
templates *template.Template templates *template.Template
logger *log.Logger logger *slog.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 {
accept := r.Header.Get("Accept-Language") localizer := GetLocalizer(b, r)
localizer := i18n.NewLocalizer(b.bundle, accept)
err := b.templates.Lookup("base").Execute(w, map[string]interface{}{ data := BaseTemplateData(b, localizer)
"Title": b.messageCatalog.LookupMessage( data["Title"] = b.messageCatalog.LookupMessage(
"ErrorTitle", "ErrorTitle",
nil, nil,
localizer, localizer,
), )
"details": b.errorDetails, data["details"] = b.errorDetails
})
err := b.templates.Lookup("base").Execute(w, data)
if err != nil { if err != nil {
log.WithError(err).Error("error rendering error template") b.logger.Error("error rendering error template", "error", err)
http.Error( http.Error(
w, w,
http.StatusText(http.StatusInternalServerError), http.StatusText(http.StatusInternalServerError),
@ -134,13 +143,12 @@ func (w *errorResponseWriter) Write(content []byte) (int, error) {
} }
func ErrorHandling( func ErrorHandling(
logger *log.Logger, logger *slog.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(
templateFS, ui.Templates,
"templates/base.gohtml", "templates/base.gohtml",
"templates/errors.gohtml", "templates/errors.gohtml",
) )

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020-2023 CAcert Inc. Copyright 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,117 +20,136 @@ package handlers
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"io/fs" "log/slog"
"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
keySet jwk.Set logger *slog.Logger
logoutURL string logoutURL string
messageCatalog *services.MessageCatalog messageCatalog *services.MessageCatalog
publicURL string publicURL string
tokenInfo *services.TokenInfoService
} }
func (h *IndexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { func (h *IndexHandler) GetBundle() *i18n.Bundle {
if request.Method != http.MethodGet { return h.bundle
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 request.URL.Path != "/" { if r.URL.Path != "/" {
http.NotFound(writer, request) http.NotFound(w, r)
return return
} }
accept := request.Header.Get("Accept-Language") localizer := GetLocalizer(h, r)
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(writer, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
var ( session, err := GetSession(r)
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(writer, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
writer.Header().Add("Content-Type", "text/html") 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)
}
w.Header().Add("Content-Type", "text/html")
msgLookup := h.messageCatalog.LookupMessage msgLookup := h.messageCatalog.LookupMessage
err = h.indexTemplate.Lookup("base").Execute(writer, map[string]interface{}{ data := BaseTemplateData(h, localizer)
"Title": msgLookup("IndexTitle", nil, localizer),
"Greeting": msgLookup("IndexGreeting", map[string]interface{}{ data["Title"] = msgLookup("IndexTitle", nil, localizer)
"User": oidcToken.Name(), data["Greeting"] = msgLookup("GreetingAnonymous", nil, localizer)
}, localizer), data["LoginLabel"] = msgLookup("LoginLabel", nil, localizer)
"IntroductionText": msgLookup("IndexIntroductionText", nil, localizer), data["IntroductionText"] = msgLookup("IndexIntroductionText", nil, localizer)
"LogoutLabel": msgLookup("LogoutLabel", nil, localizer), data["IsAuthenticated"] = false
"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(writer, err.Error(), http.StatusInternalServerError) http.Error(w, 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(
templateFS, ui.Templates,
"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

@ -0,0 +1,51 @@
/*
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 2020-2023 CAcert Inc. Copyright 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,10 +20,9 @@ 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
@ -64,7 +63,7 @@ func Logging(logger *log.Logger) func(http.Handler) http.Handler {
requestID = "unknown" requestID = "unknown"
} }
logger.Infof( logger.Printf(
"[%s] %s \"%s %s\" %d %d \"%s\"", "[%s] %s \"%s %s\" %d %d \"%s\"",
requestID, requestID,
r.RemoteAddr, r.RemoteAddr,
@ -97,7 +96,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, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, _ *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 2020-2023 CAcert Inc. Copyright 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,26 +19,20 @@ 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 *log.Logger logger *slog.Logger
oauth2Config *oauth2.Config oauth2Config *oauth2.Config
} }
@ -65,22 +59,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.WithError(err).Error("could not perform token exchange") c.logger.Error("could not perform token exchange", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
session, err := services.GetSessionStore().Get(r, "resource_session") session, err := GetSession(r)
if err != nil { if err != nil {
c.logger.WithError(err).Error("could not get session store") c.logger.Error("could not get session", "error", err)
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.WithError(err).Error("could not store token in session") c.logger.Error("could not store token in session", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -88,14 +82,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.WithError(err).Error("could not save session") c.logger.Error("could not save session", "error", err)
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[sessionRedirectTarget]; ok { if redirectTarget, ok := session.Values[services.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)
@ -130,8 +124,8 @@ func (c *OidcCallbackHandler) storeTokens(
session *sessions.Session, session *sessions.Session,
tok *oauth2.Token, tok *oauth2.Token,
) error { ) error {
session.Values[sessionKeyAccessToken] = tok.AccessToken session.Values[services.SessionAccessToken] = tok.AccessToken
session.Values[sessionKeyRefreshToken] = tok.RefreshToken session.Values[services.SessionRefreshToken] = tok.RefreshToken
idTokenValue := tok.Extra("id_token") idTokenValue := tok.Extra("id_token")
@ -140,26 +134,29 @@ 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[sessionKeyIDToken] = idToken session.Values[services.SessionIDToken] = idToken
oidcToken, err := ParseIDToken(idToken, c.keySet) session.Options.MaxAge = int(time.Until(tok.Expiry).Seconds())
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.WithFields(log.Fields{ c.logger.Debug(
"sub": oidcToken.Subject(), "receive OpenID Connect ID Token",
"aud": oidcToken.Audience(), "sub", oidcToken.Subject(),
"issued_at": oidcToken.IssuedAt(), "aud", oidcToken.Audience(),
"iss": oidcToken.Issuer(), "issued_at", oidcToken.IssuedAt(),
"not_before": oidcToken.NotBefore(), "iss", oidcToken.Issuer(),
"exp": oidcToken.Expiration(), "not_before", oidcToken.NotBefore(),
}).Debug("receive OpenID Connect ID Token") "exp", oidcToken.Expiration(),
)
return nil return nil
} }
func NewCallbackHandler(logger *log.Logger, keySet jwk.Set, oauth2Config *oauth2.Config) *OidcCallbackHandler { func NewCallbackHandler(logger *slog.Logger, keySet jwk.Set, oauth2Config *oauth2.Config) *OidcCallbackHandler {
return &OidcCallbackHandler{ return &OidcCallbackHandler{
keySet: keySet, keySet: keySet,
logger: logger, logger: logger,

View file

@ -0,0 +1,155 @@
/*
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 2020-2023 CAcert Inc. Copyright 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 2020-2023 CAcert Inc. Copyright 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,6 +20,8 @@ package handlers
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -27,23 +29,18 @@ import (
"time" "time"
"github.com/knadh/koanf" "github.com/knadh/koanf"
"github.com/sirupsen/logrus"
) )
func StartApplication( func StartApplication(
ctx context.Context, ctx context.Context, logger *slog.Logger, server *http.Server, publicURL string, config *koanf.Koanf,
logger *logrus.Logger, ) error {
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.Infoln("Server is shutting down...") logger.Info("Server is shutting down...")
atomic.StoreInt32(&Healthy, 0) atomic.StoreInt32(&Healthy, 0)
const shutdownWaitTime = 30 * time.Second const shutdownWaitTime = 30 * time.Second
@ -55,24 +52,25 @@ func StartApplication(
server.SetKeepAlivesEnabled(false) server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil { if err := server.Shutdown(ctx); err != nil {
logger.WithError(err).Fatal("Could not gracefully shutdown the server") logger.Error("Could not gracefully shutdown the server", "error", err)
} }
close(done) close(done)
}() }()
logger.WithField("public_url", publicURL).Info("Server is ready to handle requests") logger.Info("Server is ready to handle requests", "public_url", publicURL)
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.WithError(err).WithField( logger.Error("Could not listen on requested address", "server_address", server.Addr)
"server_address",
server.Addr, return fmt.Errorf("listening failed: %w", err)
).Fatal("Could not listen on requested address")
} }
<-done <-done
logger.Infoln("Server stopped") logger.Info("Server stopped")
return nil
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020-2023 CAcert Inc. Copyright 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 2020-2023 CAcert Inc. Copyright 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,6 +19,7 @@ package services
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"strings" "strings"
@ -28,7 +29,6 @@ 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"
) )
@ -51,13 +51,12 @@ 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() {
logger.Info(f.FlagUsages()) log.Print(f.FlagUsages())
os.Exit(0) os.Exit(0)
} }
@ -70,7 +69,7 @@ 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 {
logger.WithError(err).Fatal("could not parse command line arguments") return nil, fmt.Errorf("could not parse command line arguments: %w", err)
} }
config := koanf.New(".") config := koanf.New(".")
@ -78,18 +77,18 @@ func ConfigureApplication(
_ = 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) { if err = config.Load(file.Provider(defaultFile), toml.Parser()); err != nil && !os.IsNotExist(err) {
logrus.WithError(err).WithField("file", defaultFile).Fatal("error loading configuration from file") 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 {
logger.WithError(err).WithField("file", c).Fatal("error loading configuration from file") return nil, fmt.Errorf("error loading configuration from file %s: %w", c, err)
} }
} }
if err = config.Load(posflag.Provider(f, ".", config), nil); err != nil { if err = config.Load(posflag.Provider(f, ".", config), nil); err != nil {
logger.WithError(err).Fatal("error loading configuration from command line") return nil, fmt.Errorf("error loading configuration from command line: %w", err)
} }
prefix := fmt.Sprintf("%s_", strings.ToUpper(appName)) prefix := fmt.Sprintf("%s_", strings.ToUpper(appName))
@ -97,7 +96,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 {
logrus.WithError(err).Fatal("error loading configuration from environment") return nil, fmt.Errorf("error loading configuration from environment variables: %w", err)
} }
return config, nil return config, nil

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020-2023 CAcert Inc. Copyright 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,6 +20,7 @@ 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"
@ -27,19 +28,32 @@ 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["IndexGreeting"] = &i18n.Message{ messages["AuthenticatedAs"] = &i18n.Message{
ID: "IndexGreeting", ID: "AuthenticatedAs",
Other: "Hello {{ .User }}", Other: "The identity provider authenticated your identity as {{ .Name }}" +
" 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",
@ -47,7 +61,21 @@ func AddMessages(catalog *MessageCatalog) {
} }
messages["IndexIntroductionText"] = &i18n.Message{ messages["IndexIntroductionText"] = &i18n.Message{
ID: "IndexIntroductionText", ID: "IndexIntroductionText",
Other: "This is an authorization protected resource", Other: "This is a public 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)
@ -55,7 +83,7 @@ func AddMessages(catalog *MessageCatalog) {
type MessageCatalog struct { type MessageCatalog struct {
messages map[string]*i18n.Message messages map[string]*i18n.Message
logger *log.Logger logger *slog.Logger
} }
func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) { func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) {
@ -74,11 +102,11 @@ func (m *MessageCatalog) LookupErrorMessage(
message, ok := m.messages[fieldTag] message, ok := m.messages[fieldTag]
if !ok { if !ok {
m.logger.WithField("field_tag", fieldTag).Info("no specific error message for field and tag") m.logger.Info("no specific error message for field and tag", "field_tag", fieldTag)
message, ok = m.messages[tag] message, ok = m.messages[tag]
if !ok { if !ok {
m.logger.WithField("tag", tag).Info("no specific error message for tag") m.logger.Info("no specific error message for tag", "tag", tag)
message, ok = m.messages["unknown"] message, ok = m.messages["unknown"]
if !ok { if !ok {
@ -96,7 +124,7 @@ func (m *MessageCatalog) LookupErrorMessage(
}, },
}) })
if err != nil { if err != nil {
m.logger.WithError(err).Error("localization failed") m.logger.Error("localization failed", "error", err)
return tag return tag
} }
@ -121,7 +149,7 @@ func (m *MessageCatalog) LookupMessage(
return translation return translation
} }
m.logger.WithField("id", id).Warn("no translation found for id") m.logger.Warn("no translation found for id", "id", id)
return id return id
} }
@ -130,19 +158,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.WithError(err).WithField("message", id).Warn("message not found") m.logger.Warn("message not found", "error", err, "message", id)
if translation != "" { if translation != "" {
return translation return translation
} }
} else { } else {
m.logger.WithError(err).WithField("message", id).Error("translation error") m.logger.Error("translation error", "error", err, "message", id)
} }
return id return id
} }
func InitI18n(logger *log.Logger, languages []string) (*i18n.Bundle, *MessageCatalog) { func InitI18n(logger *slog.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)
@ -151,7 +179,7 @@ func InitI18n(logger *log.Logger, languages []string) (*i18n.Bundle, *MessageCat
bundleBytes, err := translations.Bundles.ReadFile(bundleName) bundleBytes, err := translations.Bundles.ReadFile(bundleName)
if err != nil { if err != nil {
logger.WithField("bundle", bundleName).Warn("message bundle not found") logger.Warn("message bundle not found", "bundle", bundleName)
continue continue
} }
@ -164,7 +192,7 @@ func InitI18n(logger *log.Logger, languages []string) (*i18n.Bundle, *MessageCat
return bundle, catalog return bundle, catalog
} }
func initMessageCatalog(logger *log.Logger) *MessageCatalog { func initMessageCatalog(logger *slog.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 2020-2023 CAcert Inc. Copyright 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,17 +57,19 @@ 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 *log.Logger, params *OidcParams) (*OIDCInformation, error) { func DiscoverOIDC(logger *slog.Logger, params *OidcParams) (*OIDCInformation, error) {
discoveryURL, err := url.Parse(params.OidcServer) discoveryURL, err := url.Parse(params.OidcServer)
if err != nil { if err != nil {
logger.WithError(err).WithField( logger.Error(
"oidc.server", "could not parse parameter oidc.server as URL",
params.OidcServer, "oidc.server", params.OidcServer,
).Fatal("could not parse parameter value") )
} else {
discoveryURL.Path = "/.well-known/openid-configuration" return nil, fmt.Errorf("could not parse parameter value: %w", err)
} }
discoveryURL.Path = "/.well-known/openid-configuration"
var ( var (
body []byte body []byte
req *http.Request req *http.Request
@ -104,7 +106,7 @@ func DiscoverOIDC(logger *log.Logger, params *OidcParams) (*OIDCInformation, err
AuthURL: discoveryResponse.AuthorizationEndpoint, AuthURL: discoveryResponse.AuthorizationEndpoint,
TokenURL: discoveryResponse.TokenEndpoint, TokenURL: discoveryResponse.TokenEndpoint,
}, },
Scopes: []string{"openid", "offline"}, Scopes: []string{"openid", "email", "profile"},
} }
const jwkFetchTimeout = 10 * time.Second const jwkFetchTimeout = 10 * time.Second

View file

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

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020-2023 CAcert Inc. Copyright 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,24 +18,33 @@ 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
func InitSessionStore(logger *log.Logger, sessionPath string, keys ...[]byte) { const (
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:gomnd if err = os.MkdirAll(sessionPath, 0700); err != nil { //nolint:mnd
logger.WithError(err).Fatal("could not create session store director") return fmt.Errorf("could not create session store director: %w", err)
} }
} }
} }
return nil
} }
func GetSessionStore() *sessions.FilesystemStore { func GetSessionStore() *sessions.FilesystemStore {

100
internal/services/token.go Normal file
View file

@ -0,0 +1,100 @@
/*
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,20 +1,47 @@
[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"
[IndexGreeting] [GreetingAnonymous]
hash = "sha1-d4a13058e497fa24143ea96d50d82b818455ef61" hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hallo {{ .User }}" other = "Hallo"
[GreetingAuthenticated]
hash = "sha1-22e08bfee49f285ac06df7d582c2a65bab86fa35"
other = "Hallo {{ .Name }}"
[IndexIntroductionText] [IndexIntroductionText]
hash = "sha1-c2c530e263fc9c38482338ed290aafb496794179" hash = "sha1-e190189ce0b76d957315332b6ca336f90a4d3d8c"
other = "Das ist eine zugriffsgeschützte Resource" other = "Das ist eine öffentliche 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,8 +1,23 @@
AuthenticatedAs = "The identity provider authenticated your identity as {{ .Name }} with the email address {{ .Email }}."
ErrorTitle = "An error has occurred" ErrorTitle = "An error has occurred"
IndexGreeting = "Hello {{ .User }}" GreetingAnonymous = "Hello"
IndexIntroductionText = "This is an authorization protected resource" GreetingAuthenticated = "Hello {{ .Name }}"
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 2020-2023 CAcert Inc. Copyright 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,12 +27,21 @@
<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">
<main role="main" class="flex-shrink-0"> <header class="container flex-shrink-0">
{{ template "content" . }} <a href="/"><img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4"></a>
</main> <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" . }}
<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">© 2020-2023 <a href="https://www.cacert.org/">CAcert</a></span> <span class="text-muted small">© <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,6 +1,5 @@
{{ define "content" }} {{ define "content" }}
<div class="container text-center error-message"> <main class="container 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>
@ -9,5 +8,5 @@
<p>{{ . }}</p> <p>{{ . }}</p>
{{ end }} {{ end }}
{{ end }} {{ end }}
</div> </main>
{{ end }} {{ end }}

View file

@ -1,8 +1,12 @@
{{ define "content" }} {{ define "content" }}
<div class="container"> <main 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>
<a class="btn btn-outline-primary" href="{{ .LogoutURL }}">{{ .LogoutLabel }}</a> {{ if .IsAuthenticated }}
</div> <p>{{ .AuthenticatedAs }}</p>
<a class="btn btn-outline-primary" href="{{ .LogoutURL }}">{{ .LogoutLabel }}</a>
{{ else }}
<a class="btn btn-primary" href="/login">{{ .LoginLabel }}</a>
{{ end }}
</main>
{{ end }} {{ end }}

View file

@ -0,0 +1,8 @@
{{ 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 2020-2023 CAcert Inc. Copyright 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");