Compare commits

..

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

39 changed files with 1136 additions and 2404 deletions

5
.gitignore vendored
View file

@ -1,11 +1,10 @@
*.pem
/.idea/
.idea/
/cacert-idp
/certs/
/dist/
/idp.toml
/static
/translations/translate.*.toml
/ui/css/
/ui/images/
/ui/js/
certs/

View file

@ -8,7 +8,7 @@ linters-settings:
const:
ORGANIZATION: CAcert Inc.
template: |-
Copyright {{ ORGANIZATION }}
Copyright {{ YEAR-RANGE }} {{ ORGANIZATION }}
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -23,7 +23,7 @@ linters-settings:
See the License for the specific language governing permissions and
limitations under the License.
goimports:
local-prefixes: code.cacert.org/cacert/oidc-idp
local-prefixes: code.cacert.org/cacert/oidc_idp
misspell:
locale: "US"
ignore-words:
@ -47,7 +47,7 @@ linters:
- gofmt
- goheader
- goimports
- mnd
- gomnd
- gosec
- lll
- makezero

View file

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

View file

@ -8,30 +8,45 @@ UI components that are required by Hydra to allow login and consent.
The code in this repository is licensed under the terms of the Apache License
Version 2.0.
Copyright © 2020-2023 Jan Dittberner
Copyright © 2020-2022 Jan Dittberner
## Setup
### Certificates
You need a set of certificate and private key and a PEM file with CA
certificates used to verify client certificates to run `cacert-idp`.
You need a set of certificates for the IDP. You can use the Test CA created by
the ``setup_test_ca.sh`` script from the [CAcert developer
setup](https://git.dittberner.info/jan/cacert-devsetup) repository like this:
An easy way to generate server certificate and key for local testing is
[`mkcert`](https://github.com/FiloSottile/mkcert/releases).
1. create signing requests
1. Run `mkcert` to generate `idp.cacert.localhost+1.pem` and
`idp.cacert.localhost+1-key.pem`:
```shell
mkcert -cert-file idp.cacert.localhost login.cacert.localhost
```
mkdir certs
cd certs
openssl req -new -newkey rsa:3072 -nodes \
-keyout idp.cacert.localhost.key \
-out idp.cacert.localhost.csr.pem \
-subj /CN=idp.cacert.localhost \
-addext subjectAltName=DNS:idp.cacert.localhost,DNS:login.cacert.localhost
cp *.csr.pem $PATH_TO_DEVSETUP_TESTCA/
```
2. Copy CA certificate for client certificates
2. Use the CA to sign the certificates
```shell
(curl -s http://www.cacert.org/certs/CAcert_Class3Root_x14E228.crt ; \
curl -s http://www.cacert.org/certs/root_X0F.crt ) > client_ca.pem
```
pushd $PATH_TO_DEVSETUP_TESTCA/
openssl ca -config ca.cnf -name class3_ca -extensions server_ext \
-in idp.cacert.localhost.csr.pem \
-out idp.cacert.localhost.crt.pem -days 365
popd
cp $PATH_TO_DEVSETUP_TESTCA/idp.cacert.localhost.crt.pem .
```
3. Copy CA certificate for client certificates
```
openssl x509 -in $PATH_TO_DEVSETUP_TESTCA/class3/ca.crt.pem \
-out client_ca.pem
```
### Configure IDP
@ -66,26 +81,26 @@ internationalization (i18n) support.
The translation workflow needs the `go18n` binary which can be installed via
```shell
```
go install github.com/nicksnyder/go-i18n/v2/goi18n
```
To extract new messages from the code run
```shell
goi18n extract -outdir translations .
```
goi18n extract .
```
Then use
```shell
goi18n merge -outdir translations translations/active.*.toml
```
goi18n merge active.*.toml
```
to create TOML files for translation as `translate.<locale>.toml`. After
translating the messages run
```shell
```
goi18n merge active.*.toml translate.*.toml
```

View file

@ -5,63 +5,13 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### [0.4.0] - 2024-05-20
- HTML for client logo image is only rendered if a client application has a logo URL
- link to client information is only rendered if a client application has a client URL
- use Consent and Deny buttons instead of a checkbox when asking for consent
- update dependencies
- switch from logrus to slog
### Fixed
- fix subject handling for login requests
## [0.3.0] - 2023-08-07
### Changed
- use a session to transport data from the login to the consent screens
- implement skip of consent screen for existing consent
- adapt to Hydra 2.x
- introduce a central template cache
- move common page header to templates/base.gohtml
### Added
- add management of consent to allow users to check and revoke consent
## [0.2.1] - 2023-08-03
### Changed
- improve formatting and german translation of login page
- enforce selection of an email address for multi-address client certificates
## [0.2.0] - 2023-07-29
### Added
- implement logout-successful handler
- implement rendering of authorization server errors
### Changed
- recommend `mkcert` to generate certificates for local development
- change default configuration to use files recommended in README.md
- separate server name and bind address to allow binding to a local address
but publish a different public name
- move default configuration to internal/services/configuration.go
- use structured logging
- support JSON logging and make it the default
- support log level configuration
- update dependencies
- re-order configuration precedence
1. default config file idp.toml
2. config files given via the `--conf` command line argument
3. environment variables with the `IDP_` prefix
- rename module to match Git repository URL
### Fixed
- run deb-systemd-helper in Debian package `postinst` script
## [0.1.3] - 2023-07-24
### Fixed
- fix conf parameter name
## [0.1.2] - 2023-07-24
### Fixed
- fix path to cacert-idp binary
## [0.1.1] - 2023-07-24
### Fixed
- fix ExecStart entry in systemd service unit
## [0.1.0] - 2023-07-24

View file

@ -1,5 +1,5 @@
/*
Copyright CAcert Inc.
Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -22,9 +22,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"os"
@ -32,31 +30,23 @@ import (
"sync/atomic"
"time"
"github.com/go-openapi/runtime/client"
"github.com/gorilla/csrf"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/toml"
"github.com/knadh/koanf/providers/confmap"
hydra "github.com/ory/hydra-client-go/v2"
hydra "github.com/ory/hydra-client-go/client"
log "github.com/sirupsen/logrus"
"code.cacert.org/cacert/oidc-idp/internal/handlers"
"code.cacert.org/cacert/oidc-idp/internal/services"
"code.cacert.org/cacert/oidc-idp/ui"
"code.cacert.org/cacert/oidc_idp/ui"
"code.cacert.org/cacert/oidc_idp/internal/handlers"
"code.cacert.org/cacert/oidc_idp/internal/services"
)
const (
IdleTimeout = 30 * time.Second
ShutdownTimeout = 30 * time.Second
ReadTimeOut = 20 * time.Second
WriteTimeOut = 20 * time.Second
TimeoutThirty = 30 * time.Second
TimeoutTwenty = 20 * time.Second
DefaultCSRFMaxAge = 600
httpsDefaultPort = 443
sessionKeyLength = 32
sessionAuthKeyLength = 64
minCSRFKeyLength = 32
DefaultServerPort = 3000
)
var (
@ -66,83 +56,73 @@ var (
)
func main() {
var (
logLevel = new(slog.LevelVar)
logHandler slog.Handler
logger *slog.Logger
)
logger := log.New()
logHandler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})
logger = slog.New(logHandler)
slog.SetDefault(logger)
config, err := services.ConfigureApplication(logger, "IDP", services.DefaultConfig)
config, err := services.ConfigureApplication(
logger,
"IDP",
map[string]interface{}{
"server.port": DefaultServerPort,
"server.name": "login.cacert.localhost",
"server.key": "certs/idp.cacert.localhost.key",
"server.certificate": "certs/idp.cacert.localhost.crt.pem",
"security.client.ca-file": "certs/client_ca.pem",
"admin.url": "https://hydra.cacert.localhost:4445/",
"i18n.languages": []string{"en", "de"},
})
if err != nil {
logger.Error("error loading configuration", "err", err)
os.Exit(1)
log.Fatalf("error loading configuration: %v", err)
}
if level := config.Bytes("log.level"); level != nil {
err := logLevel.UnmarshalText(level)
logger.WithFields(log.Fields{
"version": version, "commit": commit, "date": date,
}).Info("Starting CAcert OpenID Connect Identity Provider")
logger.Infoln("Server is starting")
bundle, catalog := services.InitI18n(logger, config.Strings("i18n.languages"))
if err = services.AddMessages(catalog); err != nil {
logger.Fatalf("could not add messages for i18n: %v", err)
}
adminURL, err := url.Parse(config.MustString("admin.url"))
if err != nil {
logger.Fatalf("error parsing admin URL: %v", err)
}
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 {
logger.Error("could not parse log level", "error", err)
os.Exit(1)
log.Fatalf("could not read CA certificate file: %v", err)
}
slog.SetLogLoggerLevel(logLevel.Level())
caCertPool.AppendCertsFromPEM(pemBytes)
tlsClientConfig.RootCAs = caCertPool
}
if config.Bool("log.json") {
logHandler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})
logger = slog.New(logHandler)
slog.SetDefault(logger)
}
logger.Info("Starting CAcert OpenID Connect Identity Provider",
"version", version, "commit", commit, "date", date,
tlsClientTransport := &http.Transport{TLSClientConfig: tlsClientConfig}
httpClient := &http.Client{Transport: tlsClientTransport}
clientTransport := client.NewWithClient(
adminURL.Host,
adminURL.Path,
[]string{adminURL.Scheme},
httpClient,
)
logger.Info("Server is starting")
i18nService := services.InitI18n(logger, config.Strings("i18n.languages"))
adminClient := hydra.New(clientTransport, nil)
if err = i18nService.AddMessages(); err != nil {
logger.Error("could not add messages for i18n", "error", err)
os.Exit(1)
}
loginHandler := handlers.NewLoginHandler(logger, bundle, catalog, adminClient.Admin)
consentHandler := handlers.NewConsentHandler(logger, bundle, catalog, adminClient.Admin)
logoutHandler := handlers.NewLogoutHandler(logger, adminClient.Admin)
sessionAuthKey, sessionEncKey, csrfKey, err := configureSessionParameters(config)
if err != nil {
logger.Error("could not configure session parameters", "error", err)
os.Exit(1)
}
services.InitSessionStore(sessionAuthKey, sessionEncKey)
clientTransport, err := configureAdminClient(config)
if err != nil {
logger.Error("could not configure Hydra admin client", "error", err)
os.Exit(1)
}
tc := handlers.PopulateTemplateCache()
needsAuth := handlers.NewAuthMiddleware(logger, tc, i18nService).NeedsAuth
indexHandler := handlers.NewIndex(logger, tc, i18nService)
manageConsentHandler := handlers.NewManageConsent(logger, tc, i18nService, clientTransport.OAuth2API)
revokeConsentHandler := handlers.NewRevokeConsent(logger, tc, i18nService, clientTransport.OAuth2API)
loginHandler := handlers.NewLoginHandler(logger, tc, i18nService, clientTransport.OAuth2API)
consentHandler := handlers.NewConsentHandler(logger, tc, i18nService, clientTransport.OAuth2API)
logoutHandler := handlers.NewLogout(logger, clientTransport.OAuth2API)
logoutSuccessHandler := handlers.NewLogoutSuccess(logger, tc, i18nService)
errorHandler := handlers.NewErrorHandler(logger, i18nService)
logoutSuccessHandler := handlers.NewLogoutSuccessHandler()
errorHandler := handlers.NewErrorHandler()
staticFiles := http.FileServer(http.FS(ui.Static))
router := http.NewServeMux()
router.HandleFunc("/", needsAuth(indexHandler))
router.Handle("/manage-consent", needsAuth(manageConsentHandler))
router.Handle("/revoke-consent/", needsAuth(revokeConsentHandler))
router.Handle("/login", loginHandler)
router.Handle("/consent", consentHandler)
router.Handle("/logout", logoutHandler)
@ -153,6 +133,15 @@ func main() {
router.Handle("/css/", staticFiles)
router.Handle("/js/", staticFiles)
if err != nil {
logger.Fatal(err)
}
csrfKey, err := base64.StdEncoding.DecodeString(config.MustString("security.csrf.key"))
if err != nil {
logger.Fatalf("could not parse CSRF key bytes: %v", err)
}
nextRequestID := func() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
@ -166,61 +155,23 @@ func main() {
csrf.SameSite(csrf.SameSiteStrictMode),
csrf.MaxAge(DefaultCSRFMaxAge))
errorMiddleware, err := handlers.ErrorHandling(logger, tc, i18nService)
errorMiddleware, err := handlers.ErrorHandling(
logger,
ui.Templates,
bundle,
catalog,
)
if err != nil {
logger.Error("could not initialize request error handling", "error", err)
os.Exit(1)
logger.Fatalf("could not initialize request error handling: %v", err)
}
handlerChain := tracing(logging(hsts(errorMiddleware(csrfProtect(router)))))
err = startServer(logger, config, handlerChain)
if err != nil {
logger.Error("server start failed", "error", err)
os.Exit(1)
}
startServer(context.Background(), handlerChain, logger, config)
}
func configureAdminClient(config *koanf.Koanf) (*hydra.APIClient, error) {
adminURL, err := url.Parse(config.MustString("admin.url"))
if err != nil {
return nil, fmt.Errorf("error parsing admin URL: %w", err)
}
configuration := hydra.NewConfiguration()
configuration.Servers = []hydra.ServerConfiguration{
{
URL: adminURL.String(),
},
}
tlsClientConfig := &tls.Config{MinVersion: tls.VersionTLS12, ServerName: adminURL.Hostname()}
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
}
tlsClientTransport := &http.Transport{TLSClientConfig: tlsClientConfig}
c := hydra.NewAPIClient(configuration)
configuration.HTTPClient.Transport = tlsClientTransport
return c, nil
}
func startServer(logger *slog.Logger, config *koanf.Koanf, handlerChain http.Handler) error {
func startServer(ctx context.Context, handlerChain http.Handler, logger *log.Logger, config *koanf.Koanf) {
clientCertificateCAFile := config.MustString("security.client.ca-file")
serverBindAddress := config.String("server.bind_address")
serverName := config.String("server.name")
serverPort := config.Int("server.port")
@ -228,7 +179,7 @@ func startServer(logger *slog.Logger, config *koanf.Koanf, handlerChain http.Han
pemBytes, err := os.ReadFile(clientCertificateCAFile)
if err != nil {
return fmt.Errorf("could not load client CA certificates: %w", err)
logger.Fatalf("could not load client CA certificates: %v", err)
}
clientCertPool.AppendCertsFromPEM(pemBytes)
@ -240,11 +191,11 @@ func startServer(logger *slog.Logger, config *koanf.Koanf, handlerChain http.Han
ClientCAs: clientCertPool,
}
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", serverBindAddress, serverPort),
Addr: fmt.Sprintf("%s:%d", serverName, serverPort),
Handler: handlerChain,
ReadTimeout: ReadTimeOut,
WriteTimeout: WriteTimeOut,
IdleTimeout: IdleTimeout,
ReadTimeout: TimeoutTwenty,
WriteTimeout: TimeoutTwenty,
IdleTimeout: TimeoutThirty,
TLSConfig: tlsConfig,
}
@ -254,109 +205,30 @@ func startServer(logger *slog.Logger, config *koanf.Koanf, handlerChain http.Han
go func() {
<-quit
logger.Info("Server is shutting down...")
logger.Infoln("Server is shutting down...")
atomic.StoreInt32(&handlers.Healthy, 0)
ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout)
ctx, cancel := context.WithTimeout(ctx, TimeoutThirty)
defer cancel()
server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil {
logger.Error("could not shutdown server gracefully", "error", err)
logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
}
close(done)
}()
logger.Info(
"Server is ready to handle requests",
"address", server.Addr, "url", publicAddress(serverName, serverPort),
)
logger.Infof("Server is ready to handle requests at https://%s/", server.Addr)
atomic.StoreInt32(&handlers.Healthy, 1)
if err := server.ListenAndServeTLS(
config.String("server.certificate"), config.String("server.key"),
); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("could not listen on configured server address", "server_addr", server.Addr)
return fmt.Errorf("could not create listener: %w", err)
); err != nil && err != http.ErrServerClosed {
logger.Fatalf("Could not listen on %s: %v\n", server.Addr, err)
}
<-done
logger.Info("Server stopped")
return nil
}
func publicAddress(serverName string, serverPort int) string {
if serverPort != httpsDefaultPort {
return fmt.Sprintf("https://%s:%d/", serverName, serverPort)
}
return fmt.Sprintf("https://%s/", serverName)
}
func configureSessionParameters(config *koanf.Koanf) ([]byte, []byte, []byte, error) { //nolint:cyclop
sessionAuthKey, err := base64.StdEncoding.DecodeString(config.String("session.auth-key"))
if err != nil {
return nil, nil, nil, fmt.Errorf("could not decode session auth key: %w", err)
}
sessionEncKey, err := base64.StdEncoding.DecodeString(config.String("session.enc-key"))
if err != nil {
return nil, nil, nil, fmt.Errorf("could not decode session encryption key: %w", err)
}
csrfKey, err := base64.StdEncoding.DecodeString(config.String("security.csrf.key"))
if err != nil {
return nil, nil, nil, fmt.Errorf("could not decode CSRF key bytes: %w", err)
}
generated := false
if len(sessionAuthKey) != sessionAuthKeyLength {
sessionAuthKey, err = services.GenerateKey(sessionAuthKeyLength)
if err != nil {
return nil, nil, nil, fmt.Errorf("could not generate session authentication key: %w", err)
}
generated = true
}
if len(sessionEncKey) != sessionKeyLength {
sessionEncKey, err = services.GenerateKey(sessionKeyLength)
if err != nil {
return nil, nil, nil, fmt.Errorf("could not generate session encryption key: %w", err)
}
generated = true
}
if len(csrfKey) < minCSRFKeyLength {
csrfKey, err = services.GenerateKey(minCSRFKeyLength)
if err != nil {
return nil, nil, nil, fmt.Errorf("could not generate CSRF key: %w", err)
}
generated = true
}
if generated {
_ = config.Load(confmap.Provider(map[string]interface{}{
"session.auth-key": base64.StdEncoding.EncodeToString(sessionAuthKey),
"session.enc-key": base64.StdEncoding.EncodeToString(sessionEncKey),
"security.csrf.key": base64.StdEncoding.EncodeToString(csrfKey),
}, "."), nil)
tomlData, err := config.Marshal(toml.Parser())
if err != nil {
return nil, nil, nil, fmt.Errorf("could not encode session config: %w", err)
}
slog.Info("generated configuration values, put the following into your idp.toml")
fmt.Printf("------\n%s------\n", string(tomlData)) //nolint:forbidigo
}
return sessionAuthKey, sessionEncKey, csrfKey, nil
logger.Infoln("Server stopped")
}

22
debian/postinst vendored
View file

@ -44,25 +44,3 @@ case "$1" in
fi
;;
esac
UNIT="cacert-oidc-idp.service"
case "$1" in
'configure' | 'abort-upgrade' | 'abort-deconfigure' | 'abort-remove')
# systemctl daemon-reload
# systemctl --global enable $UNIT
# This will only remove masks created by d-s-h on package removal.
deb-systemd-helper unmask $UNIT >/dev/null || true
# was-enabled defaults to true, so new installations run enable.
if deb-systemd-helper --quiet was-enabled $UNIT ; then
# Enables the unit on first installation, creates new
# symlinks on upgrades if the unit file has changed.
deb-systemd-helper enable $UNIT >/dev/null || true
else
# Update the statefile to add new symlinks (if any), which need to be
# cleaned up on purge. Also remove old symlinks.
deb-systemd-helper update-state $UNIT >/dev/null || true
fi
;;
esac

View file

@ -5,10 +5,8 @@ csrf.key = "32-byte-long-random-base64-encoded-key"
client.ca-file = "client.cas.pem"
[server]
# server name
name = "idp.cacert.org"
# server IP address
bind_address = "127.0.0.1"
name = "127.0.0.1"
# server port
port = 3443
# server TLS X.509 certificate in PEM format

60
go.mod
View file

@ -1,38 +1,60 @@
module code.cacert.org/cacert/oidc-idp
module code.cacert.org/cacert/oidc_idp
go 1.22
go 1.19
require (
github.com/BurntSushi/toml v1.3.2
github.com/dustin/go-humanize v1.0.1
github.com/go-playground/form/v4 v4.2.1
github.com/gorilla/csrf v1.7.2
github.com/gorilla/sessions v1.2.2
github.com/BurntSushi/toml v1.2.1
github.com/go-openapi/runtime v0.26.0
github.com/go-playground/form/v4 v4.2.0
github.com/gorilla/csrf v1.7.1
github.com/knadh/koanf v1.5.0
github.com/lestrrat-go/jwx v1.2.29
github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/ory/hydra-client-go/v2 v2.2.0
github.com/lestrrat-go/jwx v1.2.25
github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/ory/hydra-client-go v1.10.6
github.com/sirupsen/logrus v1.9.0
github.com/spf13/pflag v1.0.5
github.com/yuin/goldmark v1.7.1
golang.org/x/text v0.15.0
github.com/yuin/goldmark v1.5.4
golang.org/x/text v0.9.0
)
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.21.4 // indirect
github.com/go-openapi/errors v0.20.3 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/loads v0.21.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/strfmt v0.21.7 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/validate v0.22.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sys v0.20.0 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
go.mongodb.org/mongo-driver v1.11.6 // indirect
go.opentelemetry.io/otel v1.15.1 // indirect
go.opentelemetry.io/otel/trace v1.15.1 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/sys v0.8.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

250
go.sum
View file

@ -1,8 +1,11 @@
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=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@ -13,6 +16,9 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
@ -34,16 +40,15 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
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.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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
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.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -53,8 +58,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/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.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
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.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -63,12 +68,79 @@ github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY=
github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc=
github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo=
github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc=
github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g=
github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro=
github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw=
github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc=
github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg=
github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k=
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k=
github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU=
github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw=
github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U=
github.com/go-playground/form/v4 v4.2.0 h1:N1wh+Goz61e6w66vo8vJkQt+uwZSoLz50kZPJWR8eic=
github.com/go-playground/form/v4 v4.2.0/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -97,22 +169,20 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/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.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.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.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
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/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
@ -151,40 +221,61 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
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/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ=
github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8=
github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA=
github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
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/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@ -207,6 +298,8 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
@ -216,20 +309,27 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
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/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
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/ory/hydra-client-go/v2 v2.2.0 h1:g8hw0YQD5Us1aAgZj7OyBmBGSDwlnY9/2Pb/pQQq8YE=
github.com/ory/hydra-client-go/v2 v2.2.0/go.mod h1:h0DSI2kQA3S2fN7HyD8DNWcvbgDmYRSxfhwu/mSBhH8=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/ory/hydra-client-go v1.10.6 h1:w+uPgePbmztyLzwxWxOF89E/AG6wZuWTteHILn57BoQ=
github.com/ory/hydra-client-go v1.10.6/go.mod h1:HK2SkwXHKFC2TxHd+Ll9Xq2kJIYTQf2JTkqiC+sKcuA=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -255,20 +355,31 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
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.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
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/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.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.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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -277,31 +388,50 @@ 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.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.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/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
go.mongodb.org/mongo-driver v1.11.6 h1:XM7G6PjiGAO5betLF13BIa5TlLUUE3uJ/2Ox3Lz1K+o=
go.mongodb.org/mongo-driver v1.11.6/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY=
go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8=
go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc=
go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY=
go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY=
go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
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/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -312,7 +442,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -330,26 +459,23 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
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-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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -359,7 +485,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -378,23 +506,19 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/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-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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.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-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.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.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=
@ -402,17 +526,19 @@ 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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.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/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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@ -420,7 +546,6 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -455,7 +580,12 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -467,6 +597,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,187 +0,0 @@
/*
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package handlers
import (
"bytes"
"context"
"fmt"
"html/template"
"log/slog"
"net/http"
"github.com/dustin/go-humanize"
"github.com/gorilla/sessions"
"github.com/nicksnyder/go-i18n/v2/i18n"
"code.cacert.org/cacert/oidc-idp/ui"
"code.cacert.org/cacert/oidc-idp/internal/services"
)
const sessionName = "idp_session"
func GetSession(r *http.Request) (*sessions.Session, error) {
session, err := services.GetSessionStore().Get(r, sessionName)
if err != nil {
return nil, fmt.Errorf("could not get session")
}
return session, nil
}
type AuthMiddleware struct {
logger *slog.Logger
trans *services.I18NService
templates TemplateCache
}
func getLocalizer(trans *services.I18NService, r *http.Request) *i18n.Localizer {
accept := r.Header.Get("Accept-Language")
return trans.Localizer(accept)
}
type authContextKey int
const ctxKeyAddresses authContextKey = iota
func (a *AuthMiddleware) NeedsAuth(handler http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_, addresses := getDataFromClientCert(a.logger, r)
if len(addresses) < 1 {
renderNoEmailsInClientCertificate(a.logger, a.templates, a.trans, w, getLocalizer(a.trans, r))
return
}
handler.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKeyAddresses, addresses)))
}
}
func GetAuthenticatedAddresses(r *http.Request) []string {
if addresses, ok := r.Context().Value(ctxKeyAddresses).([]string); ok {
return addresses
}
return nil
}
func NewAuthMiddleware(logger *slog.Logger, tc TemplateCache, trans *services.I18NService) *AuthMiddleware {
return &AuthMiddleware{logger: logger, trans: trans, templates: tc}
}
type templateName string
const (
CertificateLogin templateName = "cert"
ConfirmRevoke templateName = "confirm_revoke"
Consent templateName = "consent"
Error templateName = "error"
Index templateName = "index"
LogoutSuccessful templateName = "logout_successful"
ManageConsent templateName = "manage_consent"
NoChallengeInRequest templateName = "no_challenge"
NoEmailsInClientCertificate templateName = "no_emails"
)
type TemplateCache map[templateName]*template.Template
func (c TemplateCache) render(
logger *slog.Logger, w http.ResponseWriter, name templateName, params map[string]interface{},
) {
rendered := bytes.NewBuffer(make([]byte, 0))
err := c[name].Lookup("base").Execute(rendered, params)
if err != nil {
logger.Error("template rendering failed", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
_, _ = w.Write(rendered.Bytes())
}
func PopulateTemplateCache() TemplateCache {
funcMap := map[string]any{"humantime": humanize.Time}
cache := TemplateCache{
CertificateLogin: template.Must(template.New("").Funcs(funcMap).ParseFS(
ui.Templates,
"templates/base.gohtml", "templates/client_certificate.gohtml",
)),
ConfirmRevoke: template.Must(template.New("").Funcs(funcMap).ParseFS(
ui.Templates,
"templates/base.gohtml", "templates/confirm_revoke.gohtml",
)),
NoEmailsInClientCertificate: template.Must(template.New("").Funcs(funcMap).ParseFS(
ui.Templates,
"templates/base.gohtml", "templates/no_email_in_client_certificate.gohtml",
)),
NoChallengeInRequest: template.Must(template.New("").Funcs(funcMap).ParseFS(
ui.Templates,
"templates/base.gohtml", "templates/no_challenge_in_request.gohtml",
)),
Consent: template.Must(template.New("").Funcs(funcMap).ParseFS(
ui.Templates,
"templates/base.gohtml", "templates/consent.gohtml",
)),
LogoutSuccessful: template.Must(template.New("").Funcs(funcMap).ParseFS(
ui.Templates,
"templates/base.gohtml", "templates/logout_successful.gohtml",
)),
Index: template.Must(template.New("").Funcs(funcMap).ParseFS(
ui.Templates,
"templates/base.gohtml", "templates/index.gohtml",
)),
ManageConsent: template.Must(template.New("").Funcs(funcMap).ParseFS(
ui.Templates,
"templates/base.gohtml", "templates/manage_consent.gohtml",
)),
Error: template.Must(template.ParseFS(
ui.Templates,
"templates/base.gohtml",
"templates/errors.gohtml",
)),
}
return cache
}
func renderNoEmailsInClientCertificate(
logger *slog.Logger,
templates TemplateCache,
trans *services.I18NService,
w http.ResponseWriter,
localizer *i18n.Localizer,
) {
msg := trans.LookupMessage
err := templates[NoEmailsInClientCertificate].Lookup("base").Execute(w, map[string]interface{}{
"Title": msg("NoEmailsInClientCertificateTitle", nil, localizer),
"Explanation": msg("NoEmailsInClientCertificateExplanation", nil, localizer),
})
if err != nil {
logger.Error("template rendering failed", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright CAcert Inc.
Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -22,7 +22,6 @@ import (
"errors"
"fmt"
"html/template"
"log/slog"
"net/http"
"net/url"
"strings"
@ -30,26 +29,30 @@ import (
"github.com/go-playground/form/v4"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
"github.com/lestrrat-go/jwx/jwt/openid"
"github.com/nicksnyder/go-i18n/v2/i18n"
client "github.com/ory/hydra-client-go/v2"
"github.com/ory/hydra-client-go/client/admin"
"github.com/ory/hydra-client-go/models"
log "github.com/sirupsen/logrus"
"code.cacert.org/cacert/oidc-idp/internal/models"
"code.cacert.org/cacert/oidc-idp/internal/services"
commonModels "code.cacert.org/cacert/oidc_idp/internal/models"
"code.cacert.org/cacert/oidc_idp/ui"
"code.cacert.org/cacert/oidc_idp/internal/services"
)
type ConsentHandler struct {
logger *slog.Logger
trans *services.I18NService
adminClient client.OAuth2API
templates TemplateCache
adminClient admin.ClientService
bundle *i18n.Bundle
consentTemplate *template.Template
logger *log.Logger
messageCatalog *services.MessageCatalog
}
type ConsentInformation struct {
GrantedScopes []string `form:"scope"`
SelectedClaims []string `form:"claims"`
ConsentAction string `form:"consent"`
ConsentChecked bool `form:"consent"`
}
type UserInfo struct {
@ -68,6 +71,8 @@ const (
ScopeEmail = "email"
)
const OneDayInSeconds = 86400
func init() {
supportedScopes = make(map[string]*i18n.Message)
supportedScopes[ScopeOpenID] = &i18n.Message{
@ -108,9 +113,10 @@ func (i *UserInfo) GetFullName() string {
func (h *ConsentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
challenge := r.URL.Query().Get("consent_challenge")
h.logger.Debug("received consent challenge", "consent_challenge", challenge)
h.logger.Debugf("received consent challenge %s", challenge)
localizer := getLocalizer(h.trans, r)
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(h.bundle, accept)
// retrieve consent information
consentData, requestedClaims, err := h.getRequestedConsentInformation(challenge, r)
@ -119,195 +125,82 @@ func (h *ConsentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
session, err := GetSession(r)
if err != nil {
h.logger.Error("could get session for request", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
switch r.Method {
case http.MethodGet:
h.handleGet(w, r, consentData, requestedClaims, session, challenge, localizer)
if err := h.renderConsentForm(w, r, consentData, requestedClaims, localizer); err != nil {
h.logger.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
case http.MethodPost:
h.handlePost(w, r, consentData, requestedClaims, session, challenge)
}
}
func (h *ConsentHandler) handleGet(
w http.ResponseWriter,
r *http.Request,
consentData *client.OAuth2ConsentRequest,
requestedClaims *models.OIDCClaimsRequest,
session *sessions.Session,
challenge string,
localizer *i18n.Localizer,
) {
// Hydra has a previous session for this user and client
if consentData.GetSkip() {
consentRequest, err := h.handleExistingConsent(consentData, requestedClaims, session)
if err != nil {
h.logger.Error("could not handle existing consent", "error", err)
var consentInfo ConsentInformation
// validate input
decoder := form.NewDecoder()
if err := decoder.Decode(&consentInfo, r.Form); err != nil {
h.logger.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = h.acceptConsent(w, r, challenge, consentRequest)
if err != nil {
h.logger.Error("could not accept consent", "error", err)
if consentInfo.ConsentChecked {
sessionData, err := h.getSessionData(r, consentInfo, requestedClaims, consentData.Payload)
if err != nil {
h.logger.Errorf("could not get session data: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
consentRequest, err := h.adminClient.AcceptConsentRequest(
admin.NewAcceptConsentRequestParams().WithConsentChallenge(challenge).WithBody(
&models.AcceptConsentRequest{
GrantAccessTokenAudience: nil,
GrantScope: consentInfo.GrantedScopes,
HandledAt: models.NullTime(time.Now()),
Remember: true,
RememberFor: OneDayInSeconds,
Session: sessionData,
}).WithTimeout(TimeoutTen))
if err != nil {
h.logger.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound)
return
}
return
}
h.renderConsentForm(w, r, consentData, requestedClaims, localizer)
}
func (h *ConsentHandler) handlePost(
w http.ResponseWriter,
r *http.Request,
consentData *client.OAuth2ConsentRequest,
requestedClaims *models.OIDCClaimsRequest,
session *sessions.Session,
challenge string,
) {
var consentInfo ConsentInformation
// validate input
decoder := form.NewDecoder()
if err := decoder.Decode(&consentInfo, r.Form); err != nil {
h.logger.Error("could not decode consent form", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if consentInfo.ConsentAction == "consent" {
consentRequest, err := h.rememberNewConsent(consentData, consentInfo, requestedClaims, session)
consentRequest, err := h.adminClient.RejectConsentRequest(
admin.NewRejectConsentRequestParams().WithConsentChallenge(challenge).WithBody(
&models.RejectRequest{}))
if err != nil {
h.logger.Error("could not accept consent", "error", err)
h.logger.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = h.acceptConsent(w, r, challenge, consentRequest)
if err != nil {
h.logger.Error("could not accept consent", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound)
}
consentRequest, response, err := h.adminClient.RejectOAuth2ConsentRequest(
r.Context(),
).ConsentChallenge(challenge).Execute()
if err != nil {
h.logger.Error("reject consent request failed", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer func() { _ = response.Body.Close() }()
h.logger.Debug(
"received response for RejectOAuth2ConsentRequest",
"response", response.Status, "reject_consent_request", consentRequest,
)
w.Header().Add("Location", consentRequest.GetRedirectTo())
w.WriteHeader(http.StatusFound)
}
func (h *ConsentHandler) rememberNewConsent(
consentData *client.OAuth2ConsentRequest,
consentInfo ConsentInformation,
requestedClaims *models.OIDCClaimsRequest,
session *sessions.Session,
) (*client.AcceptOAuth2ConsentRequest, error) {
sessionData, err := h.getSessionData(
consentData.GetRequestedScope(),
consentInfo.GrantedScopes,
consentInfo.SelectedClaims,
requestedClaims,
session,
)
if err != nil {
return nil, fmt.Errorf("could not get session data: %w", err)
}
request := client.NewAcceptOAuth2ConsentRequestWithDefaults()
request.SetRemember(true)
request.SetHandledAt(time.Now())
request.SetGrantAccessTokenAudience(consentData.RequestedAccessTokenAudience)
request.SetGrantScope(consentInfo.GrantedScopes)
request.SetRememberFor(0)
request.SetSession(*sessionData)
return request, nil
}
func (h *ConsentHandler) acceptConsent(
w http.ResponseWriter, r *http.Request, challenge string, request *client.AcceptOAuth2ConsentRequest,
) error {
oAuth2RedirectTo, response, err := h.adminClient.AcceptOAuth2ConsentRequest(
r.Context(),
).ConsentChallenge(challenge).AcceptOAuth2ConsentRequest(*request).Execute()
if err != nil {
return fmt.Errorf("accept consent request failed: %w", err)
}
defer func() { _ = response.Body.Close() }()
h.logger.Debug(
"received response for AcceptOAuth2ConsentRequest",
"response", response.Status, "redirect_to", oAuth2RedirectTo,
)
w.Header().Add("Location", oAuth2RedirectTo.GetRedirectTo())
w.WriteHeader(http.StatusFound)
return nil
}
func (h *ConsentHandler) handleExistingConsent(
data *client.OAuth2ConsentRequest,
claims *models.OIDCClaimsRequest,
session *sessions.Session,
) (*client.AcceptOAuth2ConsentRequest, error) {
sessionData, err := h.getSessionData(data.GetRequestedScope(),
data.GetRequestedScope(), []string{}, claims, session)
if err != nil {
return nil, fmt.Errorf("could not get session data: %w", err)
}
request := client.NewAcceptOAuth2ConsentRequestWithDefaults()
request.SetGrantScope(data.RequestedScope)
request.SetHandledAt(time.Now())
request.SetGrantAccessTokenAudience(data.RequestedAccessTokenAudience)
request.SetSession(*sessionData)
return request, nil
}
func (h *ConsentHandler) getRequestedConsentInformation(challenge string, r *http.Request) (
*client.OAuth2ConsentRequest,
*models.OIDCClaimsRequest,
*admin.GetConsentRequestOK,
*commonModels.OIDCClaimsRequest,
error,
) {
consentRequest, response, err := h.adminClient.GetOAuth2ConsentRequest(
r.Context(),
).ConsentChallenge(challenge).Execute()
consentData, err := h.adminClient.GetConsentRequest(
admin.NewGetConsentRequestParams().WithConsentChallenge(challenge))
if err != nil {
h.logger.Error("error getting consent information", "error", err)
h.logger.Errorf("error getting consent information: %v", err)
if errorBucket := GetErrorBucket(r); errorBucket != nil {
errorDetails := &ErrorDetails{
@ -321,85 +214,73 @@ func (h *ConsentHandler) getRequestedConsentInformation(challenge string, r *htt
return nil, nil, fmt.Errorf("error getting consent information: %w", err)
}
defer func() { _ = response.Body.Close() }()
var requestedClaims commonModels.OIDCClaimsRequest
h.logger.Debug(
"response for GetOAuth2ConsentRequest",
"response", response.Status, "consent_request", consentRequest,
)
var requestedClaims models.OIDCClaimsRequest
requestURLStr := consentRequest.GetRequestUrl()
requestURL, err := url.Parse(requestURLStr)
requestURL, err := url.Parse(consentData.Payload.RequestURL)
if err != nil {
h.logger.Warn(
"could not parse original request URL",
"error", err, "request_url", requestURLStr,
)
h.logger.Warnf("could not parse original request URL %s: %v", consentData.Payload.RequestURL, err)
} else {
claimsParameter := requestURL.Query().Get("claims")
if claimsParameter != "" {
decoder := json.NewDecoder(strings.NewReader(claimsParameter))
err := decoder.Decode(&requestedClaims)
if err != nil {
h.logger.Warn(
"ignoring claims request parameter that could not be decoded",
"error", err, "claims_parameter", claimsParameter,
h.logger.Warnf(
"ignoring claims request parameter %s that could not be decoded: %v",
claimsParameter,
err,
)
}
}
}
return consentRequest, &requestedClaims, nil
return consentData, &requestedClaims, nil
}
func (h *ConsentHandler) renderConsentForm(
w http.ResponseWriter,
r *http.Request,
consentRequest *client.OAuth2ConsentRequest,
claims *models.OIDCClaimsRequest,
consentData *admin.GetConsentRequestOK,
claims *commonModels.OIDCClaimsRequest,
localizer *i18n.Localizer,
) {
trans := h.trans.LookupMessage
transMarkdown := func(id string, params map[string]interface{}, localizer *i18n.Localizer) template.HTML {
return template.HTML( //nolint:gosec
h.trans.LookupMarkdownMessage(id, params, localizer),
)
) error {
trans := func(id string, values ...map[string]interface{}) string {
if len(values) > 0 {
return h.messageCatalog.LookupMessage(id, values[0], localizer)
}
return h.messageCatalog.LookupMessage(id, nil, localizer)
}
// render consent form
oAuth2Client := consentRequest.Client
clientLogoURI := oAuth2Client.GetLogoUri()
clientName := template.HTMLEscaper(oAuth2Client.GetClientName())
clientURI := oAuth2Client.GetClientUri()
h.templates.render(h.logger, w, Consent, map[string]interface{}{
"Title": trans("TitleRequestConsent", nil, localizer),
csrf.TemplateTag: csrf.TemplateField(r),
"errors": map[string]string{},
"LogoURI": clientLogoURI,
"ClientName": clientName,
"requestedScope": h.mapRequestedScope(consentRequest.RequestedScope, localizer),
"requestedClaims": h.mapRequestedClaims(claims, localizer),
"ButtonTitleConsent": trans("ButtonTitleConsent", nil, localizer),
"ButtonTitleDeny": trans("ButtonTitleDeny", nil, localizer),
"HasMoreInformation": clientURI != "",
"IntroMoreInformation": transMarkdown(
"IntroConsentMoreInformation", map[string]interface{}{
"client": clientName,
"clientLink": clientURI,
}, localizer),
"LabelConsent": transMarkdown("LabelConsent", nil, localizer),
"ClaimsInformation": transMarkdown(
"ClaimsInformation", nil, localizer),
"IntroConsentRequested": transMarkdown(
"IntroConsentRequested", map[string]interface{}{
"client": clientName,
}, localizer),
client := consentData.GetPayload().Client
err := h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{
"Title": trans("TitleRequestConsent"),
csrf.TemplateTag: csrf.TemplateField(r),
"errors": map[string]string{},
"client": client,
"requestedScope": h.mapRequestedScope(consentData.GetPayload().RequestedScope, localizer),
"requestedClaims": h.mapRequestedClaims(claims, localizer),
"LabelSubmit": trans("LabelSubmit"),
"LabelConsent": trans("LabelConsent"),
"IntroMoreInformation": template.HTML( //nolint:gosec
trans("IntroConsentMoreInformation", map[string]interface{}{
"client": client.ClientName,
"clientLink": client.ClientURI,
})),
"ClaimsInformation": template.HTML( //nolint:gosec
trans("ClaimsInformation", nil)),
"IntroConsentRequested": template.HTML( //nolint:gosec
trans("IntroConsentRequested", map[string]interface{}{
"client": client.ClientName,
})),
})
if err != nil {
return fmt.Errorf("rendering failed: %w", err)
}
return nil
}
type scopeWithLabel struct {
@ -408,14 +289,14 @@ type scopeWithLabel struct {
}
func (h *ConsentHandler) mapRequestedScope(
scope []string,
scope models.StringSlicePipeDelimiter,
localizer *i18n.Localizer,
) []*scopeWithLabel {
result := make([]*scopeWithLabel, 0)
for _, scopeName := range scope {
if _, ok := supportedScopes[scopeName]; !ok {
h.logger.Warn("ignoring unsupported scope", "scope", scopeName)
h.logger.Warnf("unsupported scope %s ignored", scopeName)
continue
}
@ -424,7 +305,7 @@ func (h *ConsentHandler) mapRequestedScope(
DefaultMessage: supportedScopes[scopeName],
})
if err != nil {
h.logger.Warn("could not localize scope label", "error", err, "scope", scopeName)
h.logger.Warnf("could not localize label for scope %s: %v", scopeName, err)
label = scopeName
}
@ -441,23 +322,17 @@ type claimWithLabel struct {
}
func (h *ConsentHandler) mapRequestedClaims(
claims *models.OIDCClaimsRequest,
claims *commonModels.OIDCClaimsRequest,
localizer *i18n.Localizer,
) []*claimWithLabel {
result := make([]*claimWithLabel, 0)
known := make(map[string]bool)
for _, claimElement := range []*models.ClaimElement{claims.GetUserInfo(), claims.GetIDToken()} {
for _, claimElement := range []*commonModels.ClaimElement{claims.GetUserInfo(), claims.GetIDToken()} {
if claimElement != nil {
for k, v := range *claimElement {
if v == nil {
h.logger.Warn("claim element is nil", "key", k)
continue
}
if _, ok := supportedClaims[k]; !ok {
h.logger.Warn("ignoring unsupported claim", "claim", k)
h.logger.Warnf("unsupported claim %s ignored", k)
continue
}
@ -466,7 +341,7 @@ func (h *ConsentHandler) mapRequestedClaims(
DefaultMessage: supportedClaims[k],
})
if err != nil {
h.logger.Warn("could not localize claim label", "error", err, "claim", k)
h.logger.Warnf("could not localize label for claim %s: %v", k, err)
label = k
}
@ -486,43 +361,41 @@ func (h *ConsentHandler) mapRequestedClaims(
}
func (h *ConsentHandler) getSessionData(
requestedScopes,
grantedScopes,
selectedClaims []string,
claims *models.OIDCClaimsRequest,
session *sessions.Session,
) (*client.AcceptOAuth2ConsentRequestSession, error) {
idTokenData := make(map[string]interface{})
r *http.Request,
info ConsentInformation,
claims *commonModels.OIDCClaimsRequest,
payload *models.ConsentRequest,
) (*models.ConsentRequestSession, error) {
idTokenData := make(map[string]interface{}, 0)
accessTokenData := make(map[string]interface{}, 0)
if err := h.fillTokenData(
idTokenData,
requestedScopes,
grantedScopes,
claims,
selectedClaims,
session.Values,
); err != nil {
userInfo := h.GetUserInfoFromClientCertificate(r, payload.Subject)
if err := h.fillTokenData(accessTokenData, payload.RequestedScope, claims, info, userInfo); err != nil {
return nil, err
}
consentSession := client.NewAcceptOAuth2ConsentRequestSession()
consentSession.SetIdToken(idTokenData)
if err := h.fillTokenData(idTokenData, payload.RequestedScope, claims, info, userInfo); err != nil {
return nil, err
}
return consentSession, nil
return &models.ConsentRequestSession{
AccessToken: accessTokenData,
IDToken: idTokenData,
}, nil
}
func (h *ConsentHandler) fillTokenData(
m map[string]interface{},
requestedScopes []string,
grantedScopes []string,
claimsRequest *models.OIDCClaimsRequest,
selectedClaims []string,
sessionData map[interface{}]interface{},
requestedScope models.StringSlicePipeDelimiter,
claimsRequest *commonModels.OIDCClaimsRequest,
consentInformation ConsentInformation,
userInfo *UserInfo,
) error {
for _, scope := range requestedScopes {
for _, scope := range requestedScope {
granted := false
for _, k := range grantedScopes {
for _, k := range consentInformation.GrantedScopes {
if k == scope {
granted = true
@ -539,8 +412,8 @@ func (h *ConsentHandler) fillTokenData(
// email
// OPTIONAL. This scope value requests access to the email and
// email_verified Claims.
m[openid.EmailKey] = sessionData[services.SessionEmail]
m[openid.EmailVerifiedKey] = true
m[openid.EmailKey] = userInfo.Email
m[openid.EmailVerifiedKey] = userInfo.EmailVerified
case ScopeProfile:
// profile
// OPTIONAL. This scope value requests access to the
@ -548,12 +421,12 @@ func (h *ConsentHandler) fillTokenData(
// family_name, given_name, middle_name, nickname,
// preferred_username, profile, picture, website, gender,
// birthdate, zoneinfo, locale, and updated_at.
m[openid.NameKey] = sessionData[services.SessionFullName]
m[openid.NameKey] = userInfo.GetFullName()
}
}
if userInfoClaims := claimsRequest.GetUserInfo(); userInfoClaims != nil {
err := h.parseUserInfoClaims(m, userInfoClaims, selectedClaims)
err := h.parseUserInfoClaims(m, userInfoClaims, consentInformation)
if err != nil {
return err
}
@ -564,13 +437,13 @@ func (h *ConsentHandler) fillTokenData(
func (h *ConsentHandler) parseUserInfoClaims(
m map[string]interface{},
userInfoClaims *models.ClaimElement,
selectedClaims []string,
userInfoClaims *commonModels.ClaimElement,
consentInformation ConsentInformation,
) error {
for claimName, claim := range *userInfoClaims {
granted := false
for _, k := range selectedClaims {
for _, k := range consentInformation.SelectedClaims {
if k == claimName {
granted = true
@ -584,7 +457,7 @@ func (h *ConsentHandler) parseUserInfoClaims(
wantedValue, err := claim.WantedValue()
if err != nil {
if !errors.Is(err, models.ErrNoValue) {
if !errors.Is(err, commonModels.ErrNoValue) {
return fmt.Errorf("error handling claim: %w", err)
}
}
@ -596,9 +469,48 @@ func (h *ConsentHandler) parseUserInfoClaims(
}
if claim.IsEssential() {
h.logger.Warn("handling for essential claim not implemented", "claim", claimName)
h.logger.Warnf(
"handling for essential claim name %s not implemented",
claimName,
)
} else {
h.logger.Warn("handling for claim not implemented", "claim", claimName)
h.logger.Warnf(
"handling for claim name %s not implemented",
claimName,
)
}
}
return nil
}
func (h *ConsentHandler) GetUserInfoFromClientCertificate(r *http.Request, subject string) *UserInfo {
if r.TLS != nil && r.TLS.PeerCertificates != nil && len(r.TLS.PeerCertificates) > 0 {
firstCert := r.TLS.PeerCertificates[0]
var verified bool
for _, email := range firstCert.EmailAddresses {
h.logger.Infof("authenticated with a client certificate for email address %s", email)
if subject == email {
verified = true
}
}
if !verified {
h.logger.Warnf(
"authentication attempt with a wrong certificate that did not contain the requested address %s",
subject,
)
return nil
}
return &UserInfo{
Email: subject,
EmailVerified: verified,
CommonName: firstCert.Subject.CommonName,
}
}
@ -606,12 +518,23 @@ func (h *ConsentHandler) parseUserInfoClaims(
}
func NewConsentHandler(
logger *slog.Logger, templateCache TemplateCache, trans *services.I18NService, adminClient client.OAuth2API,
logger *log.Logger,
bundle *i18n.Bundle,
messageCatalog *services.MessageCatalog,
adminClient admin.ClientService,
) *ConsentHandler {
consentTemplate := template.Must(
template.ParseFS(
ui.Templates,
"templates/base.gohtml",
"templates/consent.gohtml",
))
return &ConsentHandler{
logger: logger,
trans: trans,
adminClient: adminClient,
templates: templateCache,
adminClient: adminClient,
bundle: bundle,
consentTemplate: consentTemplate,
logger: logger,
messageCatalog: messageCatalog,
}
}

View file

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

View file

@ -1,5 +1,5 @@
/*
Copyright CAcert Inc.
Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -18,15 +18,16 @@ limitations under the License.
package handlers
import (
"bytes"
"context"
"fmt"
"html/template"
"log/slog"
"io/fs"
"net/http"
"code.cacert.org/cacert/oidc-idp/internal/services"
"code.cacert.org/cacert/oidc-idp/ui"
"github.com/nicksnyder/go-i18n/v2/i18n"
log "github.com/sirupsen/logrus"
"code.cacert.org/cacert/oidc_idp/internal/services"
)
type errorKey int
@ -43,24 +44,34 @@ type ErrorDetails struct {
}
type ErrorBucket struct {
logger *slog.Logger
trans *services.I18NService
errorDetails *ErrorDetails
templates TemplateCache
errorDetails *ErrorDetails
templates *template.Template
logger *log.Logger
bundle *i18n.Bundle
messageCatalog *services.MessageCatalog
}
func (b *ErrorBucket) serveHTTP(w http.ResponseWriter, r *http.Request) {
if b.errorDetails != nil {
localizer := getLocalizer(b.trans, r)
b.templates.render(b.logger, w, Error, map[string]interface{}{
"Title": b.trans.LookupMessage(
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(b.bundle, accept)
err := b.templates.Lookup("base").Execute(w, map[string]interface{}{
"Title": b.messageCatalog.LookupMessage(
"ErrorTitle",
nil,
localizer,
),
"details": b.errorDetails,
})
if err != nil {
log.Errorf("error rendering error template: %v", err)
http.Error(
w,
http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError,
)
}
}
}
@ -89,7 +100,9 @@ func (w *errorResponseWriter) WriteHeader(code int) {
if code >= http.StatusBadRequest {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.errorBucket.AddError(&ErrorDetails{ErrorCode: "HTTP error"})
w.errorBucket.AddError(&ErrorDetails{
ErrorMessage: http.StatusText(code),
})
}
w.ResponseWriter.WriteHeader(code)
@ -117,14 +130,27 @@ func (w *errorResponseWriter) Write(content []byte) (int, error) {
}
func ErrorHandling(
logger *slog.Logger, templateCache TemplateCache, trans *services.I18NService,
logger *log.Logger,
templateFS fs.FS,
bundle *i18n.Bundle,
messageCatalog *services.MessageCatalog,
) (func(http.Handler) http.Handler, error) {
errorTemplates, err := template.ParseFS(
templateFS,
"templates/base.gohtml",
"templates/errors.gohtml",
)
if err != nil {
return nil, fmt.Errorf("could not parse templates: %w", err)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorBucket := &ErrorBucket{
logger: logger,
trans: trans,
templates: templateCache,
templates: errorTemplates,
logger: logger,
bundle: bundle,
messageCatalog: messageCatalog,
}
next.ServeHTTP(
&errorResponseWriter{w, errorBucket, http.StatusOK},
@ -136,61 +162,15 @@ func ErrorHandling(
}
type ErrorHandler struct {
logger *slog.Logger
trans *services.I18NService
template *template.Template
}
func (h *ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
localizer := getLocalizer(h.trans, r)
errorName := r.URL.Query().Get("error")
errorDescription := r.URL.Query().Get("error_description")
h.logger.Debug(
"error from Hydra",
"error_name", errorName, "error_description", errorDescription,
)
rendered := bytes.NewBuffer(make([]byte, 0))
msg := h.trans.LookupMessage
msgMarkdown := h.trans.LookupMarkdownMessage
err := h.template.Lookup("base").Execute(rendered, map[string]interface{}{
"Title": msg("AuthServerErrorTitle", nil, localizer),
"Explanation": template.HTML( //nolint:gosec
msgMarkdown("AuthServerErrorExplanation", nil, localizer),
),
"ErrorMessage": errorDescription,
})
if err != nil {
h.logger.Error("template rendering failed", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Add("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
_, _ = w.Write(rendered.Bytes())
func (e *ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = fmt.Fprintf(w, `
didumm %#v
`, r.URL.Query())
}
func NewErrorHandler(logger *slog.Logger, trans *services.I18NService) *ErrorHandler {
return &ErrorHandler{
logger: logger,
trans: trans,
template: template.Must(template.ParseFS(
ui.Templates,
"templates/base.gohtml",
"templates/hydra_error.gohtml"),
),
}
func NewErrorHandler() *ErrorHandler {
return &ErrorHandler{}
}

View file

@ -1,5 +1,5 @@
/*
Copyright CAcert Inc.
Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -19,40 +19,49 @@ package handlers
import (
"bytes"
"context"
"crypto/x509"
"errors"
"fmt"
"html/template"
"io"
"log/slog"
"net/http"
"strconv"
"time"
"github.com/gorilla/csrf"
"github.com/nicksnyder/go-i18n/v2/i18n"
client "github.com/ory/hydra-client-go/v2"
"github.com/ory/hydra-client-go/client/admin"
"github.com/ory/hydra-client-go/models"
log "github.com/sirupsen/logrus"
"code.cacert.org/cacert/oidc-idp/internal/services"
"code.cacert.org/cacert/oidc_idp/ui"
"code.cacert.org/cacert/oidc_idp/internal/services"
)
type acrType string
const (
// ClientCertificate represents a client certificate login
ClientCertificate acrType = "urn:cacert:1fa:cert"
// ClientCertificateOTP acrType = "urn:cacert:2fa:cert+otp"
// ClientCertificateToken acrType = "urn:cacert:2fa:cert+token"
ClientCertificate acrType = "cert" // client certificate login
// ClientCertificateOTP acrType = "cert+otp"
// ClientCertificateToken acrType = "cert+token"
)
type contextKey int
type templateName string
const (
ctxKeyMessage contextKey = iota
CertificateLogin templateName = "cert"
NoEmailsInClientCertificate templateName = "no_emails"
NoChallengeInRequest templateName = "no_challenge"
)
const TimeoutTen = 10 * time.Second
type LoginHandler struct {
logger *slog.Logger
trans *services.I18NService
adminClient client.OAuth2API
templates TemplateCache
adminClient admin.ClientService
bundle *i18n.Bundle
logger *log.Logger
templates map[templateName]*template.Template
messageCatalog *services.MessageCatalog
}
func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -62,7 +71,8 @@ func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
localizer := getLocalizer(h.trans, r)
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(h.bundle, accept)
challenge := r.URL.Query().Get("login_challenge")
@ -72,12 +82,12 @@ func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
h.logger.Debug("received login challenge", "challenge", challenge)
h.logger.Debugf("received login challenge %s\n", challenge)
certFullName, certEmails := getDataFromClientCert(h.logger, r)
certEmails := h.getEmailAddressesFromClientCertificate(r)
if certEmails == nil {
renderNoEmailsInClientCertificate(h.logger, h.templates, h.trans, w, localizer)
h.renderNoEmailsInClientCertificate(w, localizer)
return
}
@ -85,7 +95,7 @@ func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
h.handleGet(w, r, challenge, certEmails, localizer)
} else {
h.handlePost(w, r, challenge, certFullName, certEmails, localizer)
h.handlePost(w, r, challenge, certEmails, localizer)
}
}
@ -96,217 +106,159 @@ func (h *LoginHandler) handleGet(
certEmails []string,
localizer *i18n.Localizer,
) {
oAuth2LoginRequest, response, err := h.adminClient.GetOAuth2LoginRequest(
r.Context(),
).LoginChallenge(challenge).Execute()
loginRequest, err := h.adminClient.GetLoginRequest(admin.NewGetLoginRequestParams().WithLoginChallenge(challenge))
if err != nil {
h.logger.Warn(
"could not get login request for challenge",
"error", err, "challenge", challenge,
)
h.logger.WithError(err).WithField(
"challenge", challenge,
).Warn("could not get login request for challenge")
var notFound *admin.GetLoginRequestNotFound
if errors.As(err, &notFound) {
w.WriteHeader(http.StatusNotFound)
http.Error(w, notFound.GetPayload().ErrorDescription, http.StatusNotFound)
return
}
var gone *admin.GetLoginRequestGone
if errors.As(err, &gone) {
w.Header().Set("Location", *gone.GetPayload().RedirectTo)
w.WriteHeader(http.StatusGone)
return
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
usableEmails := certEmails
defer func() { _ = response.Body.Close() }()
h.logger.Debug(
"got response for GetOAuth2LoginRequest",
"response", response.Status, "login_request", oAuth2LoginRequest,
)
if subject, ok := oAuth2LoginRequest.GetSubjectOk(); ok && *subject != "" {
h.logger.Info("oauth2LoginRequest expects subject", "subject", *subject)
subjectInCert := false
for _, email := range certEmails {
if *subject == email {
subjectInCert = true
break
}
}
if !subjectInCert {
h.rejectLoginMissingSubject(w, r, challenge, localizer, *subject)
return
}
usableEmails = []string{*subject}
}
h.renderRequestForClientCert(w, r, usableEmails, localizer, oAuth2LoginRequest)
}
type FlashMessage struct {
Type string
Message string
h.renderRequestForClientCert(w, r, certEmails, localizer, loginRequest)
}
func (h *LoginHandler) handlePost(
w http.ResponseWriter,
r *http.Request,
challenge string,
certFullName string,
certEmails []string,
localizer *i18n.Localizer,
) {
if r.FormValue("use-identity") != "accept" {
h.rejectLogin(w, r, challenge, localizer)
return
}
if r.FormValue("email") == "" {
h.handleGet(w, r.WithContext(context.WithValue(
r.Context(),
ctxKeyMessage,
FlashMessage{
Type: "warning",
Message: h.trans.LookupMessage("NoEmailAddressSelected", nil, localizer),
},
)), challenge, certEmails, localizer)
h.rejectLogin(w, challenge, localizer)
return
}
// perform certificate auth
h.logger.Info(
"will perform certificate authentication",
"emails", certEmails, "full_name", certFullName,
)
h.logger.Infof("would perform certificate authentication with: %+v", certEmails)
userID, err := h.performCertificateLogin(certEmails, r)
if err != nil {
h.logger.Error("could not perform certificate login", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session, err := GetSession(r)
if err != nil {
h.logger.Error("could not perform certificate login", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session.Values[services.SessionFullName] = certFullName
session.Values[services.SessionEmail] = userID
session.Options.HttpOnly = true
session.Options.Secure = true
if err = session.Save(r, w); err != nil {
h.logger.Error("could not save session", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// finish login and redirect to target
acceptRequest := client.NewAcceptOAuth2LoginRequest(userID)
acceptRequest.SetRemember(true)
acceptRequest.SetRememberFor(0)
acceptRequest.SetAcr(string(ClientCertificate))
loginRequest, response, err := h.adminClient.AcceptOAuth2LoginRequest(
r.Context(),
).LoginChallenge(challenge).AcceptOAuth2LoginRequest(*acceptRequest).Execute()
loginRequest, err := h.adminClient.AcceptLoginRequest(
admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(
&models.AcceptLoginRequest{
Acr: string(ClientCertificate),
Remember: true,
RememberFor: 0,
Subject: &userID,
}).WithTimeout(TimeoutTen))
if err != nil {
h.logger.Error("error getting login request", "error", err)
h.logger.Errorf("error getting login request: %#v", err)
h.fillAcceptLoginRequestErrorBucket(r, err)
return
}
w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound)
}
func (h *LoginHandler) fillAcceptLoginRequestErrorBucket(r *http.Request, err error) {
if errorBucket := GetErrorBucket(r); errorBucket != nil {
var (
errorDetails *ErrorDetails
acceptLoginRequestNotFound *admin.AcceptLoginRequestNotFound
)
if errors.As(err, &acceptLoginRequestNotFound) {
payload := acceptLoginRequestNotFound.GetPayload()
errorDetails = &ErrorDetails{
ErrorMessage: payload.Error,
ErrorDetails: []string{payload.ErrorDescription},
}
if acceptLoginRequestNotFound.Payload.StatusCode != 0 {
errorDetails.ErrorCode = strconv.Itoa(int(payload.StatusCode))
}
} else {
errorDetails = &ErrorDetails{
ErrorMessage: "could not accept login",
ErrorDetails: []string{err.Error()},
}
}
errorBucket.AddError(errorDetails)
}
}
func (h *LoginHandler) rejectLogin(w http.ResponseWriter, challenge string, localizer *i18n.Localizer) {
const Ten = 10 * time.Second
rejectLoginRequest, err := h.adminClient.RejectLoginRequest(
admin.NewRejectLoginRequestParams().WithLoginChallenge(challenge).WithBody(
&models.RejectRequest{
ErrorDescription: h.messageCatalog.LookupMessage("LoginDeniedByUser", nil, localizer),
ErrorHint: h.messageCatalog.LookupMessage("HintChooseAnIdentityForAuthentication", nil, localizer),
StatusCode: http.StatusForbidden,
},
).WithTimeout(Ten))
if err != nil {
h.logger.Errorf("error getting reject login request: %#v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer func() { _ = response.Body.Close() }()
w.Header().Set("Location", *rejectLoginRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound)
}
h.logger.Debug("got response for AcceptOAuth2LoginRequest",
"response", response.Status, "accept_login_request", loginRequest,
)
func (h *LoginHandler) getEmailAddressesFromClientCertificate(r *http.Request) []string {
if r.TLS != nil && r.TLS.PeerCertificates != nil && len(r.TLS.PeerCertificates) > 0 {
firstCert := r.TLS.PeerCertificates[0]
if h.logger.Enabled(r.Context(), slog.LevelDebug) {
if rb, err := io.ReadAll(response.Body); err == nil {
h.logger.Debug("response body from Hydra", "response_body", rb)
if !isClientCertificate(firstCert) {
return nil
}
for _, email := range firstCert.EmailAddresses {
h.logger.Infof("authenticated with a client certificate for email address %s", email)
}
return firstCert.EmailAddresses
}
return nil
}
func isClientCertificate(cert *x509.Certificate) bool {
for _, ext := range cert.ExtKeyUsage {
if ext == x509.ExtKeyUsageClientAuth {
return true
}
}
w.Header().Add("Location", loginRequest.GetRedirectTo())
w.WriteHeader(http.StatusFound)
}
func (h *LoginHandler) rejectLogin(
w http.ResponseWriter,
r *http.Request,
challenge string,
localizer *i18n.Localizer,
) {
rejectRequest := client.NewRejectOAuth2RequestWithDefaults()
rejectRequest.SetErrorDescription(h.trans.LookupMessage("LoginDeniedByUser", nil, localizer))
rejectRequest.SetErrorHint(h.trans.LookupMessage("HintChooseAnIdentityForAuthentication", nil, localizer))
rejectRequest.SetStatusCode(http.StatusForbidden)
rejectLoginRequest, response, err := h.adminClient.RejectOAuth2LoginRequest(
r.Context(),
).LoginChallenge(challenge).RejectOAuth2Request(*rejectRequest).Execute()
if err != nil {
h.logger.Error("error sending reject login request", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer func() { _ = response.Body.Close() }()
h.logger.DebugContext(
r.Context(),
"got response for RejectOAuth2LoginRequest",
"response", response.Status, "reject_login_request", rejectLoginRequest,
)
w.Header().Set("Location", rejectLoginRequest.GetRedirectTo())
w.WriteHeader(http.StatusFound)
}
func (h *LoginHandler) rejectLoginMissingSubject(
w http.ResponseWriter, r *http.Request, challenge string, localizer *i18n.Localizer, subject string,
) {
rejectRequest := client.NewRejectOAuth2RequestWithDefaults()
rejectRequest.SetErrorDescription(h.trans.LookupMessage(
"LoginDeniedSubjectMissing", map[string]interface{}{"Subject": subject}, localizer),
)
rejectRequest.SetErrorHint(h.trans.LookupMessage("HintChooseDifferentClientCertificate", nil, localizer))
rejectRequest.SetStatusCode(http.StatusForbidden)
rejectLoginRequest, response, err := h.adminClient.RejectOAuth2LoginRequest(
r.Context()).LoginChallenge(challenge).RejectOAuth2Request(
*rejectRequest,
).Execute()
if err != nil {
h.logger.Error("error sending reject login request", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer func() { _ = response.Body.Close() }()
h.logger.DebugContext(
r.Context(),
"got response for RejectOAuth2LoginRequest",
"response", response.Status, "reject_login_request", rejectLoginRequest,
)
w.Header().Set("Location", rejectLoginRequest.GetRedirectTo())
w.WriteHeader(http.StatusFound)
return false
}
func (h *LoginHandler) renderRequestForClientCert(
@ -314,31 +266,30 @@ func (h *LoginHandler) renderRequestForClientCert(
r *http.Request,
emails []string,
localizer *i18n.Localizer,
loginRequest *client.OAuth2LoginRequest,
loginRequest *admin.GetLoginRequestOK,
) {
msg := h.trans.LookupMessage
msgPlural := h.trans.LookupMessagePlural
msgMarkdown := h.trans.LookupMarkdownMessage
trans := func(label string) string {
return h.messageCatalog.LookupMessage(label, nil, localizer)
}
rendered := bytes.NewBuffer(make([]byte, 0))
err := h.templates[CertificateLogin].Lookup("base").Execute(rendered, map[string]interface{}{
"Title": msg("LoginTitle", nil, localizer),
"Title": trans("LoginTitle"),
csrf.TemplateTag: csrf.TemplateField(r),
"IntroText": template.HTML(msgMarkdown( //nolint:gosec
"IntroText": template.HTML(h.messageCatalog.LookupMessage( //nolint:gosec
"CertLoginIntroText",
map[string]interface{}{"ClientName": loginRequest.Client.ClientName},
map[string]interface{}{"ClientName": loginRequest.GetPayload().Client.ClientName},
localizer,
)),
"EmailChoiceText": msgPlural("EmailChoiceText", nil, localizer, len(emails)),
"EmailChoiceText": h.messageCatalog.LookupMessagePlural("EmailChoiceText", nil, localizer, len(emails)),
"emails": emails,
"RequestText": msg("CertLoginRequestText", nil, localizer),
"AcceptLabel": msg("LabelAcceptCertLogin", nil, localizer),
"RejectLabel": msg("LabelRejectCertLogin", nil, localizer),
"FlashMessage": r.Context().Value(ctxKeyMessage),
"RequestText": trans("CertLoginRequestText"),
"AcceptLabel": trans("LabelAcceptCertLogin"),
"RejectLabel": trans("LabelRejectCertLogin"),
})
if err != nil {
h.logger.Error("template rendering failed", "error", err)
h.logger.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@ -360,17 +311,34 @@ func (h *LoginHandler) performCertificateLogin(emails []string, r *http.Request)
return "", fmt.Errorf("no user found")
}
func (h *LoginHandler) renderNoEmailsInClientCertificate(w http.ResponseWriter, localizer *i18n.Localizer) {
trans := func(label string) string {
return h.messageCatalog.LookupMessage(label, nil, localizer)
}
err := h.templates[NoEmailsInClientCertificate].Lookup("base").Execute(w, map[string]interface{}{
"Title": trans("NoEmailsInClientCertificateTitle"),
"Explanation": trans("NoEmailsInClientCertificateExplanation"),
})
if err != nil {
h.logger.WithError(err).Error("template rendering failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
func (h *LoginHandler) renderNoChallengeInRequest(w http.ResponseWriter, localizer *i18n.Localizer) {
err := h.templates[NoChallengeInRequest].Lookup("base").Execute(w, map[string]interface{}{
"Title": h.trans.LookupMessage("NoChallengeInRequestTitle", nil, localizer),
"Explanation": template.HTML(h.trans.LookupMarkdownMessage( //nolint:gosec
"Title": h.messageCatalog.LookupMessage("NoChallengeInRequestTitle", nil, localizer),
"Explanation": template.HTML(h.messageCatalog.LookupMarkdownMessage( //nolint:gosec
"NoChallengeInRequestExplanation",
nil,
localizer,
)),
})
if err != nil {
h.logger.Error("template rendering failed", "error", err)
h.logger.WithError(err).Error("template rendering failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@ -378,12 +346,32 @@ func (h *LoginHandler) renderNoChallengeInRequest(w http.ResponseWriter, localiz
}
func NewLoginHandler(
logger *slog.Logger, tc TemplateCache, trans *services.I18NService, adminClient client.OAuth2API,
logger *log.Logger,
bundle *i18n.Bundle,
messageCatalog *services.MessageCatalog,
adminClient admin.ClientService,
) *LoginHandler {
return &LoginHandler{
logger: logger,
trans: trans,
adminClient: adminClient,
templates: tc,
bundle: bundle,
logger: logger,
templates: map[templateName]*template.Template{
CertificateLogin: template.Must(template.ParseFS(
ui.Templates,
"templates/base.gohtml",
"templates/client_certificate.gohtml",
)),
NoEmailsInClientCertificate: template.Must(template.ParseFS(
ui.Templates,
"templates/base.gohtml",
"templates/no_email_in_client_certificate.gohtml",
)),
NoChallengeInRequest: template.Must(template.ParseFS(
ui.Templates,
"templates/base.gohtml",
"templates/no_challenge_in_request.gohtml",
)),
},
messageCatalog: messageCatalog,
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright CAcert Inc.
Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -18,61 +18,47 @@ limitations under the License.
package handlers
import (
"html/template"
"log/slog"
"net/http"
"time"
client "github.com/ory/hydra-client-go/v2"
"code.cacert.org/cacert/oidc-idp/internal/services"
"github.com/ory/hydra-client-go/client/admin"
log "github.com/sirupsen/logrus"
)
type LogoutHandler struct {
adminClient client.OAuth2API
logger *slog.Logger
adminClient admin.ClientService
logger *log.Logger
}
func (h *LogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
challenge := r.URL.Query().Get("logout_challenge")
h.logger.Debug("received logout challenge", "challenge", challenge)
const Ten = 10 * time.Second
logoutRequest, response, err := h.adminClient.GetOAuth2LogoutRequest(
r.Context(),
).LogoutChallenge(challenge).Execute()
challenge := r.URL.Query().Get("logout_challenge")
h.logger.Debugf("received challenge %s\n", challenge)
logoutRequest, err := h.adminClient.GetLogoutRequest(
admin.NewGetLogoutRequestParams().WithLogoutChallenge(challenge).WithTimeout(Ten))
if err != nil {
h.logger.Error("error getting logout requests", "error", err)
h.logger.Errorf("error getting logout requests: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer func() { _ = response.Body.Close() }()
h.logger.Debugf("received logout request: %#v", logoutRequest.Payload)
h.logger.Debug(
"got response for GetOAuth2LogoutRequest",
"response", response.Status, "logout_request", logoutRequest,
)
acceptLogoutRequest, response, err := h.adminClient.AcceptOAuth2LogoutRequest(
r.Context(),
).LogoutChallenge(challenge).Execute()
acceptLogoutRequest, err := h.adminClient.AcceptLogoutRequest(
admin.NewAcceptLogoutRequestParams().WithLogoutChallenge(challenge))
if err != nil {
h.logger.Error("accept logout request failed", "error", err)
h.logger.Errorf("error accepting logout: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
defer func() { _ = response.Body.Close() }()
h.logger.Debug(
"got response for AcceptOAuth2LogoutRequest",
"response", response.Status, "accept_logout_request", acceptLogoutRequest,
)
w.Header().Set("Location", acceptLogoutRequest.GetRedirectTo())
w.Header().Set("Location", *acceptLogoutRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound)
}
func NewLogout(logger *slog.Logger, adminClient client.OAuth2API) *LogoutHandler {
func NewLogoutHandler(logger *log.Logger, adminClient admin.ClientService) *LogoutHandler {
return &LogoutHandler{
logger: logger,
adminClient: adminClient,
@ -80,32 +66,12 @@ func NewLogout(logger *slog.Logger, adminClient client.OAuth2API) *LogoutHandler
}
type LogoutSuccessHandler struct {
logger *slog.Logger
trans *services.I18NService
templates TemplateCache
}
func (h *LogoutSuccessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
localizer := getLocalizer(h.trans, r)
h.templates.render(h.logger, w, LogoutSuccessful, map[string]interface{}{
"Title": h.trans.LookupMessage("LogoutSuccessfulTitle", nil, localizer),
"Explanation": template.HTML(h.trans.LookupMarkdownMessage( //nolint:gosec
"LogoutSuccessfulText",
nil,
localizer,
)),
})
func (l *LogoutSuccessHandler) ServeHTTP(_ http.ResponseWriter, _ *http.Request) {
panic("implement me")
}
func NewLogoutSuccess(
logger *slog.Logger, templateCache TemplateCache, trans *services.I18NService,
) *LogoutSuccessHandler {
return &LogoutSuccessHandler{logger: logger, trans: trans, templates: templateCache}
func NewLogoutSuccessHandler() *LogoutSuccessHandler {
return &LogoutSuccessHandler{}
}

View file

@ -1,372 +0,0 @@
/*
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package handlers
import (
"context"
"fmt"
"html/template"
"io"
"log/slog"
"net/http"
"sort"
"strings"
"time"
"github.com/gorilla/csrf"
"github.com/nicksnyder/go-i18n/v2/i18n"
client "github.com/ory/hydra-client-go/v2"
"code.cacert.org/cacert/oidc-idp/internal/services"
)
type IndexHandler struct {
logger *slog.Logger
trans *services.I18NService
templates TemplateCache
}
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
}
localizer := getLocalizer(h.trans, r)
if r.URL.Path != "/" {
http.Error(w, h.trans.LookupHTTPErrorMessage(http.StatusNotFound, localizer), http.StatusNotFound)
return
}
h.templates.render(h.logger, w, Index, map[string]interface{}{
"Title": h.trans.LookupMessage("IndexTitle", nil, localizer),
"WelcomeMessage": template.HTML(h.trans.LookupMarkdownMessage( //nolint:gosec
"IndexWelcomeMessage",
map[string]interface{}{"ManageConsentHRef": "/manage-consent"},
localizer,
)),
})
}
func NewIndex(logger *slog.Logger, templateCache TemplateCache, trans *services.I18NService) *IndexHandler {
return &IndexHandler{logger: logger, trans: trans, templates: templateCache}
}
type ManageConsentHandler struct {
logger *slog.Logger
trans *services.I18NService
adminAPI client.OAuth2API
templates TemplateCache
}
func (h *ManageConsentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
localizer := getLocalizer(h.trans, r)
allSessions := make(ConsentSessions, 0)
var (
sessions []client.OAuth2ConsentSession
ok bool
)
for _, s := range GetAuthenticatedAddresses(r) {
if sessions, ok = h.getConsentSessions(w, r, s); !ok {
return
}
for _, session := range sessions {
allSessions = append(allSessions, ConsentSession{Subject: s, Session: session})
}
}
sort.Sort(allSessions)
requestSession, err := GetSession(r)
if err != nil {
h.logger.Error("could not get session for request", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
h.logger.Debug("got session for request", "new_session", requestSession.IsNew)
h.templates.render(h.logger, w, ManageConsent, map[string]interface{}{
"Title": h.trans.LookupMessage("ManageConsentTitle", nil, localizer),
"Description": template.HTML(h.trans.LookupMarkdownMessage( //nolint:gosec
"ManageConsentDescription",
nil,
localizer,
)),
"NoConsentGiven": h.trans.LookupMessage("NoConsentGiven", nil, localizer),
"Flashes": requestSession.Flashes("messages"),
"ConsentSessions": allSessions,
"ButtonTitleRevoke": h.trans.LookupMessage("ButtonTitleRevoke", nil, localizer),
"ApplicationTitle": h.trans.LookupMessage("ColumnNameApplication", nil, localizer),
"ActionsTitle": h.trans.LookupMessage("ColumnNameActions", nil, localizer),
"SubjectTitle": h.trans.LookupMessage("ColumnNameSubject", nil, localizer),
"GrantedTitle": h.trans.LookupMessage("ColumnNameGranted", nil, localizer),
"ExpiresTitle": h.trans.LookupMessage("ColumnNameExpires", nil, localizer),
"LabelUnknown": h.trans.LookupMessage("LabelUnknown", nil, localizer),
"LabelNever": h.trans.LookupMessage("LabelNever", nil, localizer),
})
}
type ConsentSession struct {
Subject string
Session client.OAuth2ConsentSession
}
func (s ConsentSession) GetClientName() string {
if !s.Session.HasConsentRequest() {
return ""
}
request := s.Session.GetConsentRequest()
if !request.HasClient() {
return ""
}
consentClient := request.GetClient()
return consentClient.GetClientName()
}
func (s ConsentSession) GetID() string {
if consent, ok := s.Session.GetConsentRequestOk(); ok {
if app, ok := consent.GetClientOk(); ok {
return app.GetClientId()
}
}
return ""
}
func (s ConsentSession) GrantedAt() *time.Time {
if grantedAt, ok := s.Session.GetHandledAtOk(); ok {
return grantedAt
}
return nil
}
func (s ConsentSession) Expires() *time.Time {
if expiresAt, ok := s.Session.GetExpiresAtOk(); ok {
if grantedAt := s.GrantedAt(); grantedAt != nil {
return expiresAt.AccessToken
}
}
return nil
}
type ConsentSessions []ConsentSession
func (c ConsentSessions) Len() int {
return len(c)
}
func (c ConsentSessions) Less(i, j int) bool {
return c[i].Subject < c[j].Subject && c[i].GetClientName() < c[j].GetClientName()
}
func (c ConsentSessions) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
func (h *ManageConsentHandler) getConsentSessions(
w http.ResponseWriter, r *http.Request, subject string,
) ([]client.OAuth2ConsentSession, bool) {
sessions, response, err := h.adminAPI.ListOAuth2ConsentSessions(r.Context()).Subject(subject).Execute()
if err != nil {
h.logger.Error("error getting consent session list", "error", err)
// h.fillAcceptLoginRequestErrorBucket(r, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return nil, false
}
defer func(response *http.Response) { _ = response.Body.Close() }(response)
h.logger.Debug(
"got response for AcceptOAuth2LoginRequest",
"response", response.Status, "consent_sessions", sessions,
)
if h.logger.Enabled(r.Context(), slog.LevelDebug) {
if rb, err := io.ReadAll(response.Body); err == nil {
h.logger.Debug("response body from Hydra", "response_body", rb)
}
}
return sessions, true
}
func NewManageConsent(
logger *slog.Logger, tc TemplateCache, trans *services.I18NService, adminAPI client.OAuth2API,
) *ManageConsentHandler {
return &ManageConsentHandler{logger: logger, trans: trans, adminAPI: adminAPI, templates: tc}
}
type RevokeConsentHandler struct {
logger *slog.Logger
trans *services.I18NService
adminAPI client.OAuth2API
templates TemplateCache
}
func (h *RevokeConsentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
localizer := getLocalizer(h.trans, r)
if r.Method != http.MethodGet && r.Method != http.MethodPost {
http.Error(
w, h.trans.LookupHTTPErrorMessage(http.StatusMethodNotAllowed, localizer), http.StatusMethodNotAllowed,
)
return
}
clientID, _ := strings.CutPrefix(r.URL.Path, "/revoke-consent/")
subject := r.URL.Query().Get("subject")
if clientID == "" || subject == "" {
http.Error(w, h.trans.LookupHTTPErrorMessage(http.StatusNotFound, localizer), http.StatusNotFound)
return
}
if r.Method == http.MethodGet {
h.handleGet(w, r, clientID, localizer, subject)
return
}
h.handlePost(w, r, clientID, subject, localizer)
}
func (h *RevokeConsentHandler) handleGet(
w http.ResponseWriter, r *http.Request, clientID string, localizer *i18n.Localizer, subject string,
) {
clientApp, found, err := h.getClient(r.Context(), clientID)
if err != nil {
h.logger.Error("could not get client", "error", err)
http.Error(
w, h.trans.LookupHTTPErrorMessage(http.StatusInternalServerError, localizer),
http.StatusInternalServerError,
)
return
}
if !found {
http.Error(w, h.trans.LookupHTTPErrorMessage(http.StatusNotFound, localizer), http.StatusNotFound)
return
}
h.templates.render(h.logger, w, ConfirmRevoke, map[string]interface{}{
"Title": h.trans.LookupMessage("ConfirmRevokeTitle", nil, localizer),
"ButtonTitleRevoke": h.trans.LookupMessage("ButtonTitleConfirmRevoke", nil, localizer),
"ButtonTitleCancel": h.trans.LookupMessage("ButtonTitleCancel", nil, localizer),
"CancelLink": "/manage-consent",
csrf.TemplateTag: csrf.TemplateField(r),
"Explanation": template.HTML(h.trans.LookupMarkdownMessage( //nolint:gosec
"ConfirmRevokeExplanation", map[string]interface{}{
"ApplicationID": template.URLQueryEscaper(clientApp.GetClientId()),
"Application": template.HTMLEscapeString(clientApp.GetClientName()),
"Subject": subject,
}, localizer)),
})
}
func (h *RevokeConsentHandler) handlePost(
w http.ResponseWriter, r *http.Request, clientID string, subject string, localizer *i18n.Localizer,
) {
err := h.revokeConsent(r.Context(), clientID, subject)
if err != nil {
h.logger.Error("could not revoke consent", "error", err)
http.Error(
w, h.trans.LookupHTTPErrorMessage(http.StatusInternalServerError, localizer),
http.StatusInternalServerError,
)
return
}
http.Redirect(w, r, "/manage-consent", http.StatusFound)
}
func (h *RevokeConsentHandler) getClient(ctx context.Context, clientID string) (*client.OAuth2Client, bool, error) {
clientApp, response, err := h.adminAPI.GetOAuth2Client(ctx, clientID).Execute()
if err != nil {
return nil, false, fmt.Errorf("error getting client application: %w", err)
}
defer func(response *http.Response) { _ = response.Body.Close() }(response)
h.logger.Debug(
"got response for GetOAuth2Client",
"response", response.Status, "client_app", clientApp,
)
if h.logger.Enabled(ctx, slog.LevelDebug) {
if rb, err := io.ReadAll(response.Body); err == nil {
h.logger.Debug("response body from Hydra", "response_body", rb)
}
}
return clientApp, true, nil
}
func (h *RevokeConsentHandler) revokeConsent(ctx context.Context, clientID, subject string) error {
response, err := h.adminAPI.RevokeOAuth2ConsentSessions(ctx).Client(clientID).Subject(subject).Execute()
if err != nil {
return fmt.Errorf("could not revoke consent: %w", err)
}
defer func(response *http.Response) { _ = response.Body.Close() }(response)
h.logger.Debug("got response for RevokeOAuth2ConsentSessions", "response", response.Status)
if h.logger.Enabled(ctx, slog.LevelDebug) {
if rb, err := io.ReadAll(response.Body); err == nil {
h.logger.Debug("response body from Hydra", "response_body", rb)
}
}
return nil
}
func NewRevokeConsent(
logger *slog.Logger, tc TemplateCache, trans *services.I18NService, adminAPI client.OAuth2API,
) *RevokeConsentHandler {
return &RevokeConsentHandler{logger: logger, trans: trans, adminAPI: adminAPI, templates: tc}
}

View file

@ -1,5 +1,5 @@
/*
Copyright CAcert Inc.
Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -20,9 +20,10 @@ package handlers
import (
"context"
"fmt"
"log/slog"
"net/http"
"sync/atomic"
log "github.com/sirupsen/logrus"
)
type key int
@ -53,7 +54,7 @@ func (sci *statusCodeInterceptor) Write(content []byte) (int, error) {
return count, nil
}
func Logging(logger *slog.Logger) func(http.Handler) http.Handler {
func Logging(logger *log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
interceptor := &statusCodeInterceptor{w, http.StatusOK, 0}
@ -62,18 +63,15 @@ func Logging(logger *slog.Logger) func(http.Handler) http.Handler {
if !ok {
requestID = "unknown"
}
logger.Info(
fmt.Sprintf(
"[%s] %s \"%s %s\" %d %d \"%s\"",
requestID,
r.RemoteAddr,
r.Method,
r.URL.Path,
interceptor.code,
interceptor.count,
r.UserAgent(),
),
logger.Infof(
"%s %s \"%s %s\" %d %d \"%s\"",
requestID,
r.RemoteAddr,
r.Method,
r.URL.Path,
interceptor.code,
interceptor.count,
r.UserAgent(),
)
}()
next.ServeHTTP(interceptor, r)
@ -88,9 +86,7 @@ func Tracing(nextRequestID func() string) func(http.Handler) http.Handler {
if requestID == "" {
requestID = nextRequestID()
}
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
w.Header().Set("X-Request-Id", requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
@ -100,7 +96,7 @@ func Tracing(nextRequestID func() string) func(http.Handler) http.Handler {
var Healthy int32
func NewHealthHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&Healthy) == 1 {
w.WriteHeader(http.StatusNoContent)

View file

@ -1,5 +1,5 @@
/*
Copyright CAcert Inc.
Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -18,9 +18,7 @@ limitations under the License.
package handlers
import (
"crypto/x509"
"fmt"
"log/slog"
"net/http"
"time"
)
@ -29,37 +27,8 @@ func EnableHSTS() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
const Days180 = 180
w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", int((time.Hour*24*Days180).Seconds())))
next.ServeHTTP(w, r)
})
}
}
func getDataFromClientCert(logger *slog.Logger, r *http.Request) (string, []string) {
if r.TLS != nil && r.TLS.PeerCertificates != nil && len(r.TLS.PeerCertificates) > 0 {
firstCert := r.TLS.PeerCertificates[0]
if !isClientCertificate(firstCert) {
return "", nil
}
for _, email := range firstCert.EmailAddresses {
logger.Info("authenticated with a client certificate for email address", "email", email)
}
return firstCert.Subject.CommonName, firstCert.EmailAddresses
}
return "", nil
}
func isClientCertificate(cert *x509.Certificate) bool {
for _, ext := range cert.ExtKeyUsage {
if ext == x509.ExtKeyUsageClientAuth {
return true
}
}
return false
}

View file

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

View file

@ -1,5 +1,5 @@
/*
Copyright CAcert Inc.
Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -19,7 +19,6 @@ package services
import (
"fmt"
"log/slog"
"os"
"strings"
@ -29,79 +28,58 @@ import (
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
)
const (
defaultServerPort = 3000
defaultFile = "idp.toml"
)
var DefaultConfig = map[string]interface{}{
"server.bind_address": "",
"server.name": "idp.cacert.localhost",
"server.port": defaultServerPort,
"server.key": "idp.cacert.localhost+1-key.pem",
"server.certificate": "idp.cacert.localhost+1.pem",
"security.client.ca-file": "client_ca.pem",
"admin.url": "https://hydra.cacert.localhost:4445/",
"i18n.languages": []string{"en", "de"},
"log.level": "info",
"log.json": true,
}
func ConfigureApplication(
logger *slog.Logger,
logger *logrus.Logger,
appName string,
defaultConfig map[string]interface{},
) (*koanf.Koanf, error) {
f := pflag.NewFlagSet("config", pflag.ContinueOnError)
f.Usage = func() {
logger.Info(f.FlagUsages())
fmt.Println(f.FlagUsages()) //nolint:forbidigo
os.Exit(0)
}
f.StringSlice(
"conf",
[]string{fmt.Sprintf("%s.toml", strings.ToLower(appName))},
"path to one or more .toml files",
)
var err error
if err = f.Parse(os.Args[1:]); err != nil {
return nil, fmt.Errorf("could not parse command line arguments: %w", err)
if err := f.Parse(os.Args[1:]); err != nil {
logger.Fatal(err)
}
config := koanf.New(".")
_ = config.Load(confmap.Provider(defaultConfig, "."), nil)
if err = config.Load(file.Provider(defaultFile), toml.Parser()); err != nil && !os.IsNotExist(err) {
logger.Error("could not load configuration from file", "file", defaultFile)
return nil, fmt.Errorf("error loading configuration: %w", err)
}
cFiles, _ := f.GetStringSlice("conf")
for _, c := range cFiles {
if err = config.Load(file.Provider(c), toml.Parser()); err != nil {
logger.Error("could not load configuration from file", "file", c)
return nil, fmt.Errorf("error loading configuration: %w", err)
if err := config.Load(file.Provider(c), toml.Parser()); err != nil {
logger.Fatalf("error loading config file: %s", err)
}
}
if err = config.Load(posflag.Provider(f, ".", config), nil); err != nil {
return nil, fmt.Errorf("error loading configuration from command line: %w", err)
if err := config.Load(posflag.Provider(f, ".", config), nil); err != nil {
logger.Fatalf("error loading configuration: %s", err)
}
if err := config.Load(
file.Provider("resource_app.toml"),
toml.Parser(),
); err != nil && !os.IsNotExist(err) {
logger.Fatalf("error loading config: %v", err)
}
prefix := fmt.Sprintf("%s_", strings.ToUpper(appName))
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)), "_", ".")
}), nil); err != nil {
return nil, fmt.Errorf("error loading configuration from environment: %w", err)
logger.Fatalf("error loading config: %v", err)
}
return config, nil

View file

@ -1,5 +1,5 @@
/*
Copyright CAcert Inc.
Copyright 2020-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -21,220 +21,23 @@ import (
"bytes"
"errors"
"fmt"
"log/slog"
"net/http"
log "github.com/sirupsen/logrus"
"github.com/yuin/goldmark"
"code.cacert.org/cacert/oidc-idp/translations"
"code.cacert.org/cacert/oidc_idp/translations"
"github.com/BurntSushi/toml"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
type MessageCatalog struct {
messages map[string]*i18n.Message
logger *slog.Logger
}
func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) {
for key, value := range messages {
m.messages[key] = value
}
}
func (s *I18NService) LookupMessage(
id string,
templateData map[string]interface{},
localizer *i18n.Localizer,
) string {
if message, ok := s.catalog.messages[id]; ok {
translation, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
TemplateData: templateData,
})
if err != nil {
return s.catalog.handleLocalizeError(id, translation, err)
}
return translation
}
s.logger.Warn("no translation found for id", "id", id)
return id
}
func (s *I18NService) LookupHTTPErrorMessage(httpStatusCode int, localizer *i18n.Localizer) string {
id := fmt.Sprintf("http%d", httpStatusCode)
translation := s.LookupMessage(id, nil, localizer)
if translation != id {
return translation
}
return http.StatusText(httpStatusCode)
}
func (s *I18NService) LookupMarkdownMessage(
id string,
templateData map[string]interface{},
localizer *i18n.Localizer,
) string {
if message, ok := s.catalog.messages[id]; ok {
translation, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
TemplateData: templateData,
})
if err != nil {
s.logger.Warn("message localization failed", "error", err, "message", message)
}
if translation == "" {
return id
}
buf := &bytes.Buffer{}
err = goldmark.Convert([]byte(translation), buf)
if err != nil {
return s.catalog.handleLocalizeError(id, translation, fmt.Errorf("markdown conversion error: %w", err))
}
return buf.String()
}
s.logger.Warn("no translation found for id", "id", id)
return id
}
func (s *I18NService) LookupMessagePlural(
id string,
templateData map[string]interface{},
localizer *i18n.Localizer,
count int,
) string {
if message, ok := s.catalog.messages[id]; ok {
translation, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
TemplateData: templateData,
PluralCount: count,
})
if err != nil {
return s.catalog.handleLocalizeError(id, translation, err)
}
return translation
}
s.logger.Warn("no translation found for id", "id", id)
return id
}
func (m *MessageCatalog) handleLocalizeError(id string, translation string, err error) string {
var messageNotFound *i18n.MessageNotFoundErr
if errors.As(err, &messageNotFound) {
m.logger.Warn("message not found", "error", err, "message", id)
if translation != "" {
return translation
}
} else {
m.logger.Error("translation error", "error", err, "message", id)
}
return id
}
type I18NService struct {
logger *slog.Logger
bundle *i18n.Bundle
catalog *MessageCatalog
}
func (s *I18NService) AddMessages() error {
func AddMessages(catalog *MessageCatalog) error {
messages := make(map[string]*i18n.Message)
messages["http404"] = &i18n.Message{
ID: "http404",
Description: "HTTP error 404 not found",
Other: "Not found",
}
messages["unknown"] = &i18n.Message{
ID: "ErrorUnknown",
Other: "Unknown error",
}
messages["AuthServerErrorTitle"] = &i18n.Message{
ID: "AuthServerErrorTitle",
Other: "Authorization server returned an error",
}
messages["AuthServerErrorExplanation"] = &i18n.Message{
ID: "AuthServerErrorExplanation",
Other: "A request that your browser sent to the authorization server caused an error." +
" The authorization server returned details about the error that are printed below.",
}
messages["ButtonTitleCancel"] = &i18n.Message{
ID: "ButtonTitleCancel",
Description: "Title for a button to cancel an action",
Other: "Cancel",
}
messages["ButtonTitleConsent"] = &i18n.Message{
ID: "ButtonTitleConsent",
Description: "Title for a button to give consent",
Other: "Consent",
}
messages["ButtonTitleDeny"] = &i18n.Message{
ID: "ButtonTitleDeny",
Description: "Title for a button to deny consent",
Other: "Deny",
}
messages["ButtonTitleRevoke"] = &i18n.Message{
ID: "ButtonTitleRevoke",
Description: "Title for a button to revoke consent",
Other: "Revoke",
}
messages["ButtonTitleConfirmRevoke"] = &i18n.Message{
ID: "ButtonTitleConfirmRevoke",
Description: "Title for a button to confirm consent revocation",
Other: "Yes, Revoke!",
}
messages["ColumnNameApplication"] = &i18n.Message{
ID: "ColumnNameApplication",
Description: "Title for a table column showing application names",
Other: "Application",
}
messages["ColumnNameActions"] = &i18n.Message{
ID: "ColumnNameActions",
Description: "Title for a table column showing available actions",
Other: "Actions",
}
messages["ColumnNameExpires"] = &i18n.Message{
ID: "ColumnNameExpires",
Description: "Title for a table column showing the expiry date for a consent",
Other: "Expires",
}
messages["ColumnNameGranted"] = &i18n.Message{
ID: "ColumnNameGranted",
Description: "Title for a table column showing the time when consent has been granted",
Other: "Granted at",
}
messages["ColumnNameSubject"] = &i18n.Message{
ID: "ColumnNameSubject",
Description: "Title for a table column showing the subject of a consent",
Other: "Subject",
}
messages["ConfirmRevokeTitle"] = &i18n.Message{
ID: "ConfirmRevokeTitle",
Other: "Revoke consent",
}
messages["ConfirmRevokeExplanation"] = &i18n.Message{
ID: "ConfirmRevokeExplanation",
Other: "Do you want to revoke your consent to allow **{{ .Application }}**" +
" access to identity data for **{{ .Subject }}**?",
}
messages["TitleRequestConsent"] = &i18n.Message{
ID: "TitleRequestConsent",
Other: "Application requests your consent",
@ -247,21 +50,15 @@ func (s *I18NService) AddMessages() error {
ID: "LabelConsent",
Other: "I hereby agree that the application may get the requested permissions.",
}
messages["LabelNever"] = &i18n.Message{
ID: "LabelNever",
Other: "Never",
}
messages["LabelUnknown"] = &i18n.Message{
ID: "LabelUnknown",
Other: "Unknown",
}
messages["IntroConsentRequested"] = &i18n.Message{
ID: "IntroConsentRequested",
Other: "The **{{ .client }}** application requested your consent for the following set of permissions:",
ID: "IntroConsentRequested",
Other: "The <strong>{{ .client }}</strong> application requested your consent for the following set of " +
"permissions:",
}
messages["IntroConsentMoreInformation"] = &i18n.Message{
ID: "IntroConsentMoreInformation",
Other: "You can find more information about **{{ .client }}** at [its description page]({{ .clientLink }}).",
ID: "IntroConsentMoreInformation",
Other: "You can find more information about <strong>{{ .client }}</strong> at " +
"<a href=\"{{ .clientLink }}\">its description page</a>.",
}
messages["ClaimsInformation"] = &i18n.Message{
ID: "ClaimsInformation",
@ -273,7 +70,7 @@ func (s *I18NService) AddMessages() error {
}
messages["CertLoginIntroText"] = &i18n.Message{
ID: "CertLoginIntroText",
Other: "The application **{{ .ClientName }}** requests a login.",
Other: "The application <strong>{{ .ClientName }}</strong> requests a login.",
}
messages["EmailChoiceText"] = &i18n.Message{
ID: "EmailChoiceText",
@ -281,15 +78,6 @@ func (s *I18NService) AddMessages() error {
Other: "You have presented a valid client certificate for multiple email addresses. " +
"Please choose which one you want to present to the application:",
}
messages["IndexTitle"] = &i18n.Message{
ID: "IndexTitle",
Other: "Welcome to your identity provider",
}
messages["IndexWelcomeMessage"] = &i18n.Message{
ID: "IndexWelcomeMessage",
Other: "Go to [manage consent]({{ .ManageConsentHRef }}) to show or revoke consent" +
" you have given to client applications.",
}
messages["LoginTitle"] = &i18n.Message{
ID: "LoginTitle",
Other: "Authenticate with a client certificate",
@ -312,42 +100,10 @@ func (s *I18NService) AddMessages() error {
ID: "LoginDeniedByUser",
Other: "Login has been denied by the user.",
}
messages["LoginDeniedSubjectMissing"] = &i18n.Message{
ID: "LoginDeniedSubjectMissing",
Other: "Login has been denied because the requested subject {{ .Subject }} could not be authenticated.",
}
messages["LogoutSuccessfulTitle"] = &i18n.Message{
ID: "LogoutSuccessfulTitle",
Other: "Logout successful",
}
messages["LogoutSuccessfulText"] = &i18n.Message{
ID: "LogoutSuccessfulText",
Other: "You have been logged out successfully.",
}
messages["ManageConsentTitle"] = &i18n.Message{
ID: "ManageConsentTitle",
Other: "Manage consent",
}
messages["ManageConsentDescription"] = &i18n.Message{
ID: "ManageConsentDescription",
Other: "This page allows you to see consent that you have given to client applications in the past.",
}
messages["HintChooseAnIdentityForAuthentication"] = &i18n.Message{
ID: "HintChooseAnIdentityForAuthentication",
Other: "Choose an identity for authentication.",
}
messages["HintChooseDifferentClientCertificate"] = &i18n.Message{
ID: "HintChooseDifferentClientCertificate",
Other: "Choose a different client certificate for authentication.",
}
messages["NoConsentGiven"] = &i18n.Message{
ID: "NoConsentGiven",
Other: "You have not given consent to use your data to any application yet.",
}
messages["NoEmailAddressSelected"] = &i18n.Message{
ID: "NoEmailAddressSelected",
Other: "You did not select an email address. Please select an email address to continue.",
}
messages["NoEmailsInClientCertificateTitle"] = &i18n.Message{
ID: "NoEmailsInClientCertificateTitle",
Other: "No email addresses in client certificate",
@ -368,16 +124,149 @@ An email address is required to authenticate yourself.`,
" [the ORY Hydra documentation](https://www.ory.sh/docs/oauth2-oidc/custom-login-consent/flow).",
}
s.catalog.AddMessages(messages)
catalog.AddMessages(messages)
return nil
}
func (s *I18NService) Localizer(languages string) *i18n.Localizer {
return i18n.NewLocalizer(s.bundle, languages)
type MessageCatalog struct {
messages map[string]*i18n.Message
logger *log.Logger
}
func InitI18n(logger *slog.Logger, languages []string) *I18NService {
func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) {
for key, value := range messages {
m.messages[key] = value
}
}
func (m *MessageCatalog) LookupErrorMessage(tag, field string, value interface{}, localizer *i18n.Localizer) string {
var message *i18n.Message
message, ok := m.messages[fmt.Sprintf("%s-%s", field, tag)]
if !ok {
m.logger.Infof("no specific error message %s-%s", field, tag)
message, ok = m.messages[tag]
if !ok {
m.logger.Infof("no specific error message %s", tag)
message, ok = m.messages["unknown"]
if !ok {
m.logger.Warnf("no default translation found")
return tag
}
}
}
translation, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
TemplateData: map[string]interface{}{
"Value": value,
},
})
if err != nil {
m.logger.Error(err)
return tag
}
return translation
}
func (m *MessageCatalog) LookupMessage(
id string,
templateData map[string]interface{},
localizer *i18n.Localizer,
) string {
if message, ok := m.messages[id]; ok {
translation, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
TemplateData: templateData,
})
if err != nil {
return m.handleLocalizeError(id, translation, err)
}
return translation
}
m.logger.Warnf("no translation found for %s", id)
return id
}
func (m *MessageCatalog) LookupMarkdownMessage(
id string,
templateData map[string]interface{},
localizer *i18n.Localizer,
) string {
if message, ok := m.messages[id]; ok {
translation, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
TemplateData: templateData,
})
if err != nil {
return m.handleLocalizeError(id, translation, err)
}
buf := &bytes.Buffer{}
err = goldmark.Convert([]byte(translation), buf)
if err != nil {
return m.handleLocalizeError(id, translation, fmt.Errorf("markdown conversion error: %w", err))
}
return buf.String()
}
m.logger.Warnf("no translation found for %s", id)
return id
}
func (m *MessageCatalog) LookupMessagePlural(
id string,
templateData map[string]interface{},
localizer *i18n.Localizer,
count int,
) string {
if message, ok := m.messages[id]; ok {
translation, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
TemplateData: templateData,
PluralCount: count,
})
if err != nil {
return m.handleLocalizeError(id, translation, err)
}
return translation
}
m.logger.Warnf("no translation found for %s", id)
return id
}
func (m *MessageCatalog) handleLocalizeError(id string, translation string, err error) string {
var messageNotFound *i18n.MessageNotFoundErr
if errors.As(err, &messageNotFound) {
m.logger.WithError(err).WithField("message", id).Warn("message not found")
if translation != "" {
return translation
}
} else {
m.logger.WithError(err).WithField("message", id).Error("translation error")
}
return id
}
func InitI18n(logger *log.Logger, languages []string) (*i18n.Bundle, *MessageCatalog) {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
@ -386,7 +275,7 @@ func InitI18n(logger *slog.Logger, languages []string) *I18NService {
bundleBytes, err := translations.Bundles.ReadFile(bundleName)
if err != nil {
logger.Warn("message bundle not found", "bundle", bundleName)
logger.Warnf("message bundle %s not found", bundleName)
continue
}
@ -396,10 +285,10 @@ func InitI18n(logger *slog.Logger, languages []string) *I18NService {
catalog := initMessageCatalog(logger)
return &I18NService{logger: logger, bundle: bundle, catalog: catalog}
return bundle, catalog
}
func initMessageCatalog(logger *slog.Logger) *MessageCatalog {
func initMessageCatalog(logger *log.Logger) *MessageCatalog {
messages := make(map[string]*i18n.Message)
messages["ErrorTitle"] = &i18n.Message{
ID: "ErrorTitle",

View file

@ -1,43 +0,0 @@
/*
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package services
import (
"crypto/rand"
"errors"
"log/slog"
)
var errCouldNotGenerateKey = errors.New("could not generate key")
func GenerateKey(length int) ([]byte, error) {
key := make([]byte, length)
read, err := rand.Read(key)
if err != nil {
return nil, errCouldNotGenerateKey
}
if read != length {
slog.Error("read unexpected number of bytes", "read", read, "expected", length)
return nil, errCouldNotGenerateKey
}
return key, nil
}

View file

@ -1,37 +0,0 @@
/*
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package services
import (
"github.com/gorilla/sessions"
)
var store *sessions.CookieStore
const (
SessionFullName = iota
SessionEmail
)
func InitSessionStore(keys ...[]byte) {
store = sessions.NewCookieStore(keys...)
}
func GetSessionStore() *sessions.CookieStore {
return store
}

View file

@ -1,39 +1,6 @@
[AuthServerErrorExplanation]
hash = "sha1-d0abcb1757c2a0ebf154112e0f768d41e2ad494c"
other = "Eine Anfrage, die dein Browser an den Authorization-Server geschickt hat, hat einen Fehler verursacht. Der Authorization-Server hat die unten ausgegebenen Details zum Fehler zurückgegeben."
[AuthServerErrorTitle]
hash = "sha1-fa3294b49220d6de6f68825d03195e3f33e88378"
other = "Fehlermeldung vom Authorization-Server"
[ButtonTitleCancel]
description = "Title for a button to cancel an action"
hash = "sha1-0ea9f9f23f1883bb6642b7ec2f083036497dd56b"
other = "Abbrechen"
[ButtonTitleConfirmRevoke]
description = "Title for a button to confirm consent revocation"
hash = "sha1-bb25839982a1fe044fc2ac39552903c65402ad48"
other = "Ja, Widerrufen!"
[ButtonTitleConsent]
description = "Title for a button to give consent"
hash = "sha1-7f5c5d105e7a9600c7a8d3d092c7e812cf344e95"
other = "Zustimmen"
[ButtonTitleDeny]
description = "Title for a button to deny consent"
hash = "sha1-d5245e4b19ed6f0af002aee7ab488b8f822d53c7"
other = "Ablehnen"
[ButtonTitleRevoke]
description = "Title for a button to revoke consent"
hash = "sha1-b4524373ff63f37e062bd5f496e8ba04ee5c678d"
other = "Widerrufen"
[CertLoginIntroText]
hash = "sha1-02871569c8d36e522cdb0716081ef62b7c7a71ec"
other = "Die Anwendung **{{ .ClientName }}** fragt nach einer Anmeldung."
hash = "sha1-e9f7c0522e49ffacc49e3fc35c6ffd31e495baf6"
other = "Die Anwendung <strong>{{ .ClientName }}</strong> fragt nach einer Anmeldung."
[CertLoginRequestText]
hash = "sha1-1b20eea0f6fbb4ff139ecfe6b7a93c98cb14b8d7"
@ -43,43 +10,10 @@ other = "Willst du die ausgewählte Identität für die Anmeldung verwenden?"
hash = "sha1-4a6721995b5d87c02be77695910af642ca30b18a"
other = "Zusätzlich möchte die Anwendung Zugriff auf folgende Informationen:"
[ColumnNameActions]
description = "Title for a table column showing available actions"
hash = "sha1-b1b5e5530afa75d2f7c088486018266a1fbc3456"
other = "Aktionen"
[ColumnNameApplication]
description = "Title for a table column showing application names"
hash = "sha1-28a9f11a0be09f7d8df6e324f3222e105965d315"
other = "Applikation"
[ColumnNameExpires]
description = "Title for a table column showing the expiry date for a consent"
hash = "sha1-6c61015982d7748e6549cf56ee372a8bc130e83a"
other = "Läuft ab"
[ColumnNameGranted]
description = "Title for a table column showing the time when consent has been granted"
hash = "sha1-4ace00a57b59cf745b97f1434d5605ec75722909"
other = "Zugestimmt"
[ColumnNameSubject]
description = "Title for a table column showing the subject of a consent"
hash = "sha1-048ba3497a13fa58c474cf5a400240584b0ee5d5"
other = "Subjekt"
[ConfirmRevokeExplanation]
hash = "sha1-664fae4095fce4ff49ea3eed60293eaea99004a0"
other = "Willst du die Freigabe für den Zugriff auf deine Identitätsdaten für **{{ .Subject }}** für **{{ .Application }}** widerrufen?"
[ConfirmRevokeTitle]
hash = "sha1-13255b407052cfd4a359f6e8e175137b48388a31"
other = "Freigabe widerrufen"
[EmailChoiceText]
hash = "sha1-8bba8cd3a8724d8c5b75da9b7d2ac084b6e9df90"
one = "Du hast ein gültiges Client-Zertifikat für die folgende E-Mail-Adresse vorgelegt:"
other = "Du hast ein gültiges Client-Zertifikate für mehrere E-Mail-Adressen vorgelegt. Bitte wähle aus, welche davon Du der Anwendung zeigen möchtest:"
other = "Du hast ein gültiges Client-Zertifikate für mehrere E-Mail-Adressen vorgelegt. Bitte wähle aus, welches Du der Anwendung vorlegen möchtest:"
[ErrorTitle]
hash = "sha1-736aec25a98f5ec5b71400bb0163f891f509b566"
@ -93,25 +27,13 @@ other = "Unbekannter Fehler"
hash = "sha1-7ee5b067009bbedc061274ee50a3027b50a06163"
other = "Wähle eine Identität für die Anmeldung."
[HintChooseDifferentClientCertificate]
hash = "sha1-22002121759ae58afccfccb88383dbe54f94f0a9"
other = "Wähle ein anderes Client-Zertifikat für die Anmeldung."
[IndexTitle]
hash = "sha1-c763022b69a8ad58ab42d8ea708192abd85fd8f6"
other = "Willkommen bei deinem Identitätsprovider"
[IndexWelcomeMessage]
hash = "sha1-00632c6562df53c62861c33e468e729887816419"
other = "Besuche [die Freigabeverwaltung]({{ .ManageConsentHRef }}), um deine Freigaben für Applikationen einzusehen oder zu widerrufen."
[IntroConsentMoreInformation]
hash = "sha1-836b2931b417e98db4102731604443ee2700123c"
other = "Auf der [Beschreibungsseite]({{ .clientLink }}) findest du mehr Informationen zu **{{ .client }}**."
hash = "sha1-f58b8378238bd433deef3c3e6b0b70d0fd0dd59e"
other = "Auf der <a href=\"{{ .clientLink }}\">Beschreibungsseite</a> findest du mehr Informationen zu <strong>{{ .client }}</strong>."
[IntroConsentRequested]
hash = "sha1-db5e67a16c181c4d27baf5d0e3bf255224b0fffc"
other = "Die Anwendung **{{ .client }}** hat deine Zustimmung für die Erteilung der folgenden Berechtigungen angefragt:"
hash = "sha1-3ac6a3583d40b5e8930c57531f0be9706f1e0194"
other = "Die Anwendung <strong>{{ .client }}</strong> hat deine Zustimmung für die Erteilung der folgenden Berechtigungen angefragt:"
[LabelAcceptCertLogin]
description = "Label for a button to accept certificate login"
@ -122,10 +44,6 @@ other = "Ja, bitte"
hash = "sha1-5e56a367cf99015bbe98488845541db00b7e04f6"
other = "Ich erteile hiermit meine Einwilligung, dass die Anwendung die angefragten Berechtigungen erhalten darf."
[LabelNever]
hash = "sha1-80c3052d33ccdee15ffaaa110c5c39072495fe63"
other = "Nie"
[LabelRejectCertLogin]
description = "Label for a button to reject certificate login"
hash = "sha1-49c4d0d1da1c0a7d7e9bf491b28a6e6825f4c2cd"
@ -135,38 +53,14 @@ other = "Nein, schick mich zurück"
hash = "sha1-2dacf65959849884a011f36f76a04eebea94c5ea"
other = "Abschicken"
[LabelUnknown]
hash = "sha1-bc7819b34ff87570745fbe461e36a16f80e562ce"
other = "Unbekannt"
[LoginDeniedByUser]
hash = "sha1-bbad650536bfb091ad55d576262bbe4358277c73"
other = "Die Anmeldung wurde durch den Nutzer abgelehnt."
[LoginDeniedSubjectMissing]
hash = "sha1-c787e750515612fd695d6a6beeb3a07ec381f3e7"
other = "Die Anmeldung wurde abgelehnt, weil die angeforderte Identität {{ .Subject }} nicht bestätigt werden konnte."
[LoginTitle]
hash = "sha1-9a24c8b64e047edc13f3c41ef7785bb2044a6d69"
other = "Anmelden mit einem Client-Zertifikat"
[LogoutSuccessfulText]
hash = "sha1-ff5492eab296ca3dd1a783512095be6f195e0acb"
other = "Du wurdest erfolreich abgemeldet."
[LogoutSuccessfulTitle]
hash = "sha1-c92ba3b5f47d0b37b43ba499793a09baa94e5b9d"
other = "Abmeldung erfolgreich"
[ManageConsentDescription]
hash = "sha1-c0f25f17d557ce029f826322a3b625905966e0ea"
other = "Diese Seite zeigt dir die Freigaben für Client-Applikationen, die du in der Vergangenheit erteilt hast und ermöglicht dir, diese zu widerrufen."
[ManageConsentTitle]
hash = "sha1-1c63dd057bea5bfc86befa82e041ab08122f2e52"
other = "Freigaben verwalten"
[NoChallengeInRequestExplanation]
hash = "sha1-b26a3ef99ecadfbceb62b62eb52e34d197c56c02"
other = "In Deinem Anmelde-Request fehlt der notwendige `login_challenge`-Parameter. Mehr Informationen zu diesem Parameter findest du in [der ORY Hydra-Dokumentation](https://www.ory.sh/docs/oauth2-oidc/custom-login-consent/flow)."
@ -175,14 +69,6 @@ other = "In Deinem Anmelde-Request fehlt der notwendige `login_challenge`-Parame
hash = "sha1-b039c647fea0e42bcb0c877c58da499d082f5319"
other = "Kein Challenge-Parameter im Anmelde-Request"
[NoConsentGiven]
hash = "sha1-6843ba66b1103d3eed6403f7e276301b852e7ac0"
other = "Du hast noch keiner Applikation eine Freigabe erteilt."
[NoEmailAddressSelected]
hash = "sha1-09fdefe67eae9915e32b18c50baf985d5bd27d36"
other = "Du hast keine E-Mail-Adresse ausgewählt. Bitte wähle eine E-Mail-Adresse, um die Anmeldung fortzusetzen."
[NoEmailsInClientCertificateExplanation]
hash = "sha1-ab76bd1bed709ebb19a1b9e48c611271551f3343"
other = "Das genutzte Client-Zertifikate enthielt keine E-Mail-Adressen.\nEine E-Mail-Adresse ist erforderlich, um dich anzumelden."
@ -214,8 +100,3 @@ other = "Anwendung erbittet deine Zustimmung"
[WrongOrLockedUserOrInvalidPassword]
hash = "sha1-87e0a0ac67c6c3a06bed184e10b22aae4d075b64"
other = "Du hast einen ungültigen Nutzernamen oder ein ungültiges Passwort eingegeben oder dein Benutzerkonto wurde gesperrt."
[http404]
description = "HTTP error 404 not found"
hash = "sha1-8d7e56f17b2686bc10641795b8785c03b77581cf"
other = "Nicht gefunden"

View file

@ -1,33 +1,17 @@
AuthServerErrorExplanation = "A request that your browser sent to the authorization server caused an error. The authorization server returned details about the error that are printed below."
AuthServerErrorTitle = "Authorization server returned an error"
CertLoginIntroText = "The application **{{ .ClientName }}** requests a login."
CertLoginIntroText = "The application <strong>{{ .ClientName }}</strong> requests a login."
CertLoginRequestText = "Do you want to use the chosen identity from the certificate for authentication?"
ClaimsInformation = "In addition the application wants access to the following information:"
ConfirmRevokeExplanation = "Do you want to revoke your consent to allow **{{ .Application }}** access to identity data for **{{ .Subject }}**?"
ConfirmRevokeTitle = "Revoke consent"
ErrorTitle = "An error has occurred"
ErrorUnknown = "Unknown error"
HintChooseAnIdentityForAuthentication = "Choose an identity for authentication."
HintChooseDifferentClientCertificate = "Choose a different client certificate for authentication."
IndexTitle = "Welcome to your identity provider"
IndexWelcomeMessage = "Go to [manage consent]({{ .ManageConsentHRef }}) to show or revoke consent you have given to client applications."
IntroConsentMoreInformation = "You can find more information about **{{ .client }}** at [its description page]({{ .clientLink }})."
IntroConsentRequested = "The **{{ .client }}** application requested your consent for the following set of permissions:"
IntroConsentMoreInformation = "You can find more information about <strong>{{ .client }}</strong> at <a href=\"{{ .clientLink }}\">its description page</a>."
IntroConsentRequested = "The <strong>{{ .client }}</strong> application requested your consent for the following set of permissions:"
LabelConsent = "I hereby agree that the application may get the requested permissions."
LabelNever = "Never"
LabelSubmit = "Submit"
LabelUnknown = "Unknown"
LoginDeniedByUser = "Login has been denied by the user."
LoginDeniedSubjectMissing = "Login has been denied because the requested subject {{ .Subject }} could not be authenticated."
LoginTitle = "Authenticate with a client certificate"
LogoutSuccessfulText = "You have been logged out successfully."
LogoutSuccessfulTitle = "Logout successful"
ManageConsentDescription = "This page allows you to see consent that you have given to client applications in the past."
ManageConsentTitle = "Manage consent"
NoChallengeInRequestExplanation = "Your authentication request did not contain the necessary `login_challenge` parameter. You can find more information about this parameter in [the ORY Hydra documentation](https://www.ory.sh/docs/oauth2-oidc/custom-login-consent/flow)."
NoChallengeInRequestTitle = "No challenge parameter in your authentication request"
NoConsentGiven = "You have not given consent to use your data to any application yet."
NoEmailAddressSelected = "You did not select an email address. Please select an email address to continue."
NoEmailsInClientCertificateExplanation = "The presented client certificate does not contain any email address value.\nAn email address is required to authenticate yourself."
NoEmailsInClientCertificateTitle = "No email addresses in client certificate"
Scope-email-Description = "Access your email address."
@ -37,46 +21,6 @@ Scope-profile-Description = "Access your user profile information (your name)."
TitleRequestConsent = "Application requests your consent"
WrongOrLockedUserOrInvalidPassword = "You entered an invalid username or password or your account has been locked."
[ButtonTitleCancel]
description = "Title for a button to cancel an action"
other = "Cancel"
[ButtonTitleConfirmRevoke]
description = "Title for a button to confirm consent revocation"
other = "Yes, Revoke!"
[ButtonTitleConsent]
description = "Title for a button to give consent"
other = "Consent"
[ButtonTitleDeny]
description = "Title for a button to deny consent"
other = "Deny"
[ButtonTitleRevoke]
description = "Title for a button to revoke consent"
other = "Revoke"
[ColumnNameActions]
description = "Title for a table column showing available actions"
other = "Actions"
[ColumnNameApplication]
description = "Title for a table column showing application names"
other = "Application"
[ColumnNameExpires]
description = "Title for a table column showing the expiry date for a consent"
other = "Expires"
[ColumnNameGranted]
description = "Title for a table column showing the time when consent has been granted"
other = "Granted at"
[ColumnNameSubject]
description = "Title for a table column showing the subject of a consent"
other = "Subject"
[EmailChoiceText]
one = "You have presented a valid client certificate for the following email address:"
other = "You have presented a valid client certificate for multiple email addresses. Please choose which one you want to present to the application:"
@ -88,7 +32,3 @@ other = "Yes, please use this identity"
[LabelRejectCertLogin]
description = "Label for a button to reject certificate login"
other = "No, please send me back"
[http404]
description = "HTTP error 404 not found"
other = "Not found"

View file

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

View file

@ -26,14 +26,13 @@
<meta name="theme-color" content="#11568c">
<title>{{ .Title }}</title>
</head>
<body class="idp d-flex flex-column h-100">
<header class="container flex-shrink-0">
<a href="/"><img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4"></a>
</header>
{{ template "content" . }}
<body class="text-center idp d-flex flex-column h-100">
<main role="main" class="flex-shrink-0">
{{ template "content" . }}
</main>
<footer class="footer mt-auto py-3">
<div class="container">
<span class="text-muted small">© <a href="https://www.cacert.org/">CAcert</a></span>
<span class="text-muted small">© 2020-2023 <a href="https://www.cacert.org/">CAcert</a></span>
</div>
</footer>
<script type="text/javascript" src="/js/cacert.bundle.js"></script>

View file

@ -1,37 +1,30 @@
{{ define "content" }}
<main role="main" class="container">
<h1>{{ .Title }}</h1>
{{ .IntroText }}
{{ with .FlashMessage }}
<div class="alert alert-{{ .Type }}" role="alert">
{{ .Message }}
</div>
{{ end }}
<form method="post">
<p>{{ .EmailChoiceText }}</p>
<div class="mb-3">
{{ if eq (len .emails) 1 }}
{{ $email_address := index .emails 0 }}
<input type="hidden" name="email" value="{{ $email_address }}" id="email_0">
<label for="email_0">{{ $email_address }}</label>
{{ else }}
{{ range $index, $element := .emails }}
<div class="form-check">
<input class="form-check-input" type="radio" name="email"
value="{{ $element }}" id="email_{{ $index }}"><label
class="form-check-label" for="email_{{ $index }}">{{ $element }}</label>
</div>
{{ end }}
<form class="form-signin" method="post">
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
<h1 class="h3 mb-3">{{ .Title }}</h1>
<p class="text-left">{{ .IntroText }}</p>
<p class="text-left">{{ .EmailChoiceText }}</p>
<div class="mb-3">
{{ if eq (len .emails) 1 }}
{{ $email_address := index .emails 0 }}
<input type="hidden" name="email" value="{{ $email_address }}" id="email_0">
<label for="email_0">{{ $email_address }}</label>
{{ else }}
{{ range $index, $element := .emails }}
<input type="radio" name="email" value="{{ $element }}" id="email_{{ $index }}"><label
for="email_{{ $index }}">{{ $element }}</label>
{{ end }}
{{ .csrfField }}
</div>
<p class="text-left">{{ .RequestText }}</p>
<div class="mb-2">
<button class="btn btn-primary" type="submit" name="use-identity"
value="accept">{{ .AcceptLabel }}</button>
<button class="btn btn-outline-secondary" type="submit" name="use-identity"
value="reject">{{ .RejectLabel }}</button>
</div>
</form>
</main>
{{ end }}
{{ .csrfField }}
</div>
<p class="text-left">{{ .RequestText }}</p>
<div class="mb-2">
<button class="btn btn-lg btn-primary" type="submit" name="use-identity"
value="accept">{{ .AcceptLabel }}</button>
</div>
<div class="mb-2">
<button class="btn btn-outline-secondary" type="submit" name="use-identity"
value="reject">{{ .RejectLabel }}</button>
</div>
</form>
{{ end }}

View file

@ -1,11 +0,0 @@
{{ define "content" }}
<main role="main" class="container">
<h1 class="h3 mb-3">{{ .Title }}</h1>
<p class="text-left">{{ .Explanation }}</p>
<form method="post">
{{ .csrfField }}
<button class="btn btn-danger" type="submit">{{ .ButtonTitleRevoke }}</button>
<a class="btn btn-outline-secondary" role="button" href="{{ .CancelLink }}">{{ .ButtonTitleCancel }}</a>
</form>
</main>
{{ end }}

View file

@ -1,41 +1,40 @@
{{ define "content" }}
<main role="main" class="container">
<form class="form-consent" method="post">
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
<h1 class="h3 mb-3">{{ .Title }}</h1>
{{ if .LogoURI }}
<p class="text-center">
<img src="{{ .LogoURI }}" alt="{{ .ClientName }}" width="240" height="240" />
{{ if .client.LogoURI }}
<p>
<img src="{{ .client.LogoURI }}" alt="{{ .client.ClientName }}"/>
</p>
{{ end }}
{{ .IntroConsentRequested }}
<form method="post">
<p class="text-left">{{ .IntroConsentRequested }}</p>
<ul class="list-group text-left small mb-3">
{{ range $i, $scope := .requestedScope }}
<li class="list-group-item">
<input type="hidden" name="scope[{{ $i }}]" value="{{ $scope.Name }}">
{{ $scope.Label }}</li>
{{ end }}
</ul>
{{ if .requestedClaims }}
<p class="text-left">{{ .ClaimsInformation }}</p>
<ul class="list-group text-left small mb-3">
{{ range $i, $scope := .requestedScope }}
{{ range $i, $claim := .requestedClaims }}
<li class="list-group-item">
<input type="hidden" name="scope[{{ $i }}]" value="{{ $scope.Name }}">
{{ $scope.Label }}</li>
{{ end }}
<input type="hidden" name="claims[{{ $i }}]" value="{{ $claim.Name }}">
{{ $claim.Label }}{{ if $claim.Essential }} *{{ end}}
</li>
{{ end}}
</ul>
{{ if .requestedClaims }}
<p class="text-left">{{ .ClaimsInformation }}</p>
<ul class="list-group text-left small mb-3">
{{ range $i, $claim := .requestedClaims }}
<li class="list-group-item">
<input type="hidden" name="claims[{{ $i }}]" value="{{ $claim.Name }}">
{{ $claim.Label }}{{ if $claim.Essential }} *{{ end}}
</li>
{{ end}}
</ul>
{{ end }}
{{ if .HasMoreInformation }}
{{ .IntroMoreInformation }}
{{ end }}
{{ end }}
<p class="text-left">{{ .IntroMoreInformation }}</p>
{{ .csrfField }}
{{ .LabelConsent }}
{{ .csrfField }}
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="consent" id="consent" value="true"/>
{{ .LabelConsent }}</label>
</div>
<button class="btn btn-primary" type="submit" name="consent"
value="consent">{{ .ButtonTitleConsent }}</button>
<button class="btn btn-outline-secondary" type="submit" name="consent" value="deny">{{ .ButtonTitleDeny }}</button>
</form>
</main>
<button class="btn btn-lg btn-primary btn-block" type="submit">{{ .LabelSubmit }}</button>
</form>
{{ end }}

View file

@ -1,15 +1,13 @@
{{ define "content" }}
<main role="main" class="container">
<div class="container">
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
<h1>{{ .Title }}</h1>
<div class="alert alert-danger">
<h2 class="alert-heading">{{ if .details.ErrorCode }}
<strong>{{ .details.ErrorCode }}</strong> {{ end }}{{ .details.ErrorMessage }}
</h2>
{{ if .details.ErrorDetails }}
{{ range .details.ErrorDetails }}
{{ . }}
{{ end }}
<h2>{{ if .details.ErrorCode }}
<strong>{{ .details.ErrorCode }}</strong> {{ end }}{{ .details.ErrorMessage }}</h2>
{{ if .details.ErrorDetails }}
{{ range .details.ErrorDetails }}
<p>{{ . }}</p>
{{ end }}
</div>
</main>
{{ end }}
</div>
{{ end }}

View file

@ -1,9 +0,0 @@
{{ define "content" }}
<main class="container">
<h1>{{ .Title }}</h1>
{{ .Explanation }}
{{ if .ErrorMessage }}
<div class="alert alert-danger">{{ .ErrorMessage }}</div>
{{ end }}
</main>
{{ end }}

View file

@ -1,6 +0,0 @@
{{ define "content" }}
<main role="main" class="container">
<h1>{{ .Title }}</h1>
<p>{{ .WelcomeMessage }}</p>
</main>
{{ end }}

View file

@ -1,7 +0,0 @@
{{ define "content" }}
<div class="container">
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
<h1 class="h3 mb-3">{{ .Title }}</h1>
<p class="text-left">{{ .Explanation }}</p>
</div>
{{ end }}

View file

@ -1,47 +0,0 @@
{{ define "content" }}
<main role="main" class="container">
<h1>{{ .Title }}</h1>
<p class="text-left">{{ .Description }}</p>
{{ range .Flashes}}
<div class="alert alert-{{ .Type }}" role="alert">
{{ .Message }}
</div>
{{ end }}
{{ $buttonTitleRevoke := .ButtonTitleRevoke }}
{{ $unknownLabel := .LabelUnknown }}
{{ $neverLabel := .LabelNever }}
{{ if .ConsentSessions }}
<table class="table table-striped">
<thead>
<tr>
<th>{{ .ApplicationTitle }}</th>
<th>{{ .SubjectTitle }}</th>
<th>{{ .GrantedTitle }}</th>
<th>{{ .ExpiresTitle }}</th>
<th>{{ .ActionsTitle }}</th>
</tr>
</thead>
<tbody>
{{ range .ConsentSessions }}
<tr>
<td>{{ .GetClientName }}</td>
<td>{{ .Subject }}</td>
<td>{{ if .GrantedAt }}<span
title="{{ .GrantedAt }}">{{ .GrantedAt | humantime }}</span>{{ else }}{{ $unknownLabel }}{{ end }}
</td>
<td>{{ if .Expires }}<span
title="{{ .Expires }}">{{ .Expires | humantime }}</span>{{ else }}{{ $neverLabel }}{{ end }}
</td>
<td><a href="/revoke-consent/{{ .GetID }}?subject={{ .Subject }}" class="btn btn-danger"
role="button">{{ $buttonTitleRevoke }}</a></td>
</tr>
{{ end }}
</tbody>
</table>
{{ else}}
<div class="alert alert-info">
{{ .NoConsentGiven }}
</div>
{{ end}}
</main>
{{ end }}

View file

@ -1,6 +1,7 @@
{{ define "content" }}
<main role="main" class="container">
<h1>{{ .Title }}</h1>
<p>{{ .Explanation }}</p>
</main>
<div class="container">
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
<h1 class="h3 mb-3">{{ .Title }}</h1>
<p class="text-left">{{ .Explanation }}</p>
</div>
{{ end }}

View file

@ -1,6 +1,7 @@
{{ define "content" }}
<main role="main" class="container">
<h1>{{ .Title }}</h1>
<p>{{ .Explanation }}</p>
</main>
<div class="container">
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
<h1 class="h3 mb-3">{{ .Title }}</h1>
<p class="text-left">{{ .Explanation }}</p>
</div>
{{ end }}

View file

@ -1,20 +1,3 @@
/*
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 ui
import "embed"