Compare commits

...

20 commits
0.2.0 ... main

Author SHA1 Message Date
3e92caf52f Add check to avoid crashes when a claim element's value is nil 2024-08-16 18:05:08 +02:00
cdaed2f4e4 Prepare changelog for 0.4.0 release 2024-05-20 11:47:00 +02:00
9f44a00c63 Fix linter warnings 2024-05-19 16:05:26 +02:00
7ef12da4fa Fix subject handling for login requests
This change implements handling for the case that a login request
retrieved from Hydra has a pre-defined subject. The login request is
rejected if the requested subject is not part of the presented client
certificate.
2024-05-19 08:12:39 +02:00
bdf37493d0 Debug logging for session 2024-05-12 12:16:54 +02:00
407e9acfcc Remove comment 2024-05-12 12:16:18 +02:00
e576d981f9 Reduce minimum CSRF key length to 256 bits 2024-05-12 12:15:58 +02:00
9e54bcabbe Remove copyright years from base template 2024-05-12 12:14:20 +02:00
1e676e8cf1 Switch logging to slog
This commit replaces logrus with slog from the Go standard library.
2024-05-12 01:07:34 +02:00
f22f8ff902 Update golangci-lint, fix warnings
- remove copyright years
- mark unused parameter with _
- add missing empty lines before expressions
2024-05-11 22:42:21 +02:00
f3dc4d71d1 Update dependencies 2024-05-11 22:27:22 +02:00
9aeca21faa Remove duplicate logo from hydra_error template 2023-08-08 15:20:42 +02:00
a5c583f1f6 Render client logo at specific size 2023-08-07 17:58:45 +02:00
56ff01600f Improve consent handling
- hide client logo if there is no logo URI
- hide client information link if there is no client URI
- use buttons instead of a checkbox for consent
- use Markdown for messages
2023-08-07 17:54:54 +02:00
73735d47b6 Release 0.3.0 2023-08-07 15:19:17 +02:00
44e18ca3a5 Implement consent management
The primary change in this commit is the introduction of consent management.

A few minor improvements have been made:

- move common header to ui/templates/base.gohtml
- add an I18NService to unify localization
- add a handlers.getLocalizer function
- fix translation extraction and merging in Makefile
- add a new AuthMiddleware to centralize client certificate authentication
- move client certificate handling to internal/handlers/security.go
- improver error handling, allow localization of HTTP error messages
2023-08-07 15:15:45 +02:00
679dcb27ce Adapt to Hydra 2.x
- use new SDK package
- add session to transport user information from login to consent
2023-08-03 23:51:57 +02:00
cdb7257f7e Release 0.2.1 2023-08-03 10:47:36 +02:00
e2de4243a9 Enforce email address selection
This commit makes sure that an email address is selected when the user
presents a client certificate with multiple email addresses.
2023-08-03 10:45:21 +02:00
55530d23e4 Improve login page
- improve formatting of login page
- improve german translation of message that is shown if a certificate with multiple
  email addresses is used
2023-08-03 10:43:37 +02:00
36 changed files with 2109 additions and 1076 deletions

5
.gitignore vendored
View file

@ -1,10 +1,11 @@
*.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 {{ YEAR-RANGE }} {{ ORGANIZATION }}
Copyright {{ ORGANIZATION }}
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -47,7 +47,7 @@ linters:
- gofmt
- goheader
- goimports
- gomnd
- mnd
- gosec
- lll
- makezero

View file

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

View file

@ -66,26 +66,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
```
goi18n extract .
```shell
goi18n extract -outdir translations .
```
Then use
```
goi18n merge active.*.toml
```shell
goi18n merge -outdir translations translations/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,6 +5,31 @@ 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

View file

@ -1,5 +1,5 @@
/*
Copyright 2020-2023 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -24,6 +24,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"os"
@ -31,11 +32,11 @@ import (
"sync/atomic"
"time"
"github.com/go-openapi/runtime/client"
"github.com/gorilla/csrf"
"github.com/knadh/koanf"
hydra "github.com/ory/hydra-client-go/client"
log "github.com/sirupsen/logrus"
"github.com/knadh/koanf/parsers/toml"
"github.com/knadh/koanf/providers/confmap"
hydra "github.com/ory/hydra-client-go/v2"
"code.cacert.org/cacert/oidc-idp/internal/handlers"
"code.cacert.org/cacert/oidc-idp/internal/services"
@ -51,6 +52,11 @@ const (
DefaultCSRFMaxAge = 600
httpsDefaultPort = 443
sessionKeyLength = 32
sessionAuthKeyLength = 64
minCSRFKeyLength = 32
)
var (
@ -60,52 +66,83 @@ var (
)
func main() {
logger := log.New()
var (
logLevel = new(slog.LevelVar)
logHandler slog.Handler
logger *slog.Logger
)
logHandler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})
logger = slog.New(logHandler)
slog.SetDefault(logger)
config, err := services.ConfigureApplication(logger, "IDP", services.DefaultConfig)
if err != nil {
logger.WithError(err).Fatal("error loading configuration")
logger.Error("error loading configuration", "err", err)
os.Exit(1)
}
if level := config.String("log.level"); level != "" {
logLevel, err := log.ParseLevel(level)
if level := config.Bytes("log.level"); level != nil {
err := logLevel.UnmarshalText(level)
if err != nil {
logger.WithError(err).Fatal("could not parse log level")
logger.Error("could not parse log level", "error", err)
os.Exit(1)
}
logger.SetLevel(logLevel)
slog.SetLogLoggerLevel(logLevel.Level())
}
if config.Bool("log.json") {
logger.SetFormatter(&log.JSONFormatter{})
logHandler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})
logger = slog.New(logHandler)
slog.SetDefault(logger)
}
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"))
logger.Info("Starting CAcert OpenID Connect Identity Provider",
"version", version, "commit", commit, "date", date,
)
logger.Info("Server is starting")
i18nService := services.InitI18n(logger, config.Strings("i18n.languages"))
if err = services.AddMessages(catalog); err != nil {
logger.WithError(err).Fatal("could not add messages for i18n")
if err = i18nService.AddMessages(); err != nil {
logger.Error("could not add messages for i18n", "error", err)
os.Exit(1)
}
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.WithError(err).Fatal("could not configure Hydra admin client")
logger.Error("could not configure Hydra admin client", "error", err)
os.Exit(1)
}
adminClient := hydra.New(clientTransport, nil)
tc := handlers.PopulateTemplateCache()
loginHandler := handlers.NewLoginHandler(logger, bundle, catalog, adminClient.Admin)
consentHandler := handlers.NewConsentHandler(logger, bundle, catalog, adminClient.Admin)
logoutHandler := handlers.NewLogoutHandler(logger, adminClient.Admin)
needsAuth := handlers.NewAuthMiddleware(logger, tc, i18nService).NeedsAuth
logoutSuccessHandler := handlers.NewLogoutSuccessHandler(logger, bundle, catalog)
errorHandler := handlers.NewErrorHandler(logger, bundle, catalog)
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)
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)
@ -116,11 +153,6 @@ func main() {
router.Handle("/css/", staticFiles)
router.Handle("/js/", staticFiles)
csrfKey, err := base64.StdEncoding.DecodeString(config.MustString("security.csrf.key"))
if err != nil {
logger.WithError(err).Fatal("could not parse CSRF key bytes")
}
nextRequestID := func() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
@ -134,23 +166,35 @@ func main() {
csrf.SameSite(csrf.SameSiteStrictMode),
csrf.MaxAge(DefaultCSRFMaxAge))
errorMiddleware, err := handlers.ErrorHandling(logger, ui.Templates, bundle, catalog)
errorMiddleware, err := handlers.ErrorHandling(logger, tc, i18nService)
if err != nil {
logger.WithError(err).Fatal("could not initialize request error handling")
logger.Error("could not initialize request error handling", "error", err)
os.Exit(1)
}
handlerChain := tracing(logging(hsts(errorMiddleware(csrfProtect(router)))))
startServer(logger, config, handlerChain)
err = startServer(logger, config, handlerChain)
if err != nil {
logger.Error("server start failed", "error", err)
os.Exit(1)
}
}
func configureAdminClient(config *koanf.Koanf) (*client.Runtime, error) {
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)
}
tlsClientConfig := &tls.Config{MinVersion: tls.VersionTLS12}
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")
@ -166,18 +210,15 @@ func configureAdminClient(config *koanf.Koanf) (*client.Runtime, error) {
}
tlsClientTransport := &http.Transport{TLSClientConfig: tlsClientConfig}
httpClient := &http.Client{Transport: tlsClientTransport}
clientTransport := client.NewWithClient(
adminURL.Host,
adminURL.Path,
[]string{adminURL.Scheme},
httpClient,
)
return clientTransport, nil
c := hydra.NewAPIClient(configuration)
configuration.HTTPClient.Transport = tlsClientTransport
return c, nil
}
func startServer(logger *log.Logger, config *koanf.Koanf, handlerChain http.Handler) {
func startServer(logger *slog.Logger, config *koanf.Koanf, handlerChain http.Handler) error {
clientCertificateCAFile := config.MustString("security.client.ca-file")
serverBindAddress := config.String("server.bind_address")
serverName := config.String("server.name")
@ -187,7 +228,7 @@ func startServer(logger *log.Logger, config *koanf.Koanf, handlerChain http.Hand
pemBytes, err := os.ReadFile(clientCertificateCAFile)
if err != nil {
logger.WithError(err).Fatal("could not load client CA certificates")
return fmt.Errorf("could not load client CA certificates: %w", err)
}
clientCertPool.AppendCertsFromPEM(pemBytes)
@ -213,7 +254,7 @@ func startServer(logger *log.Logger, config *koanf.Koanf, handlerChain http.Hand
go func() {
<-quit
logger.Infoln("Server is shutting down...")
logger.Info("Server is shutting down...")
atomic.StoreInt32(&handlers.Healthy, 0)
ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout)
@ -222,27 +263,30 @@ func startServer(logger *log.Logger, config *koanf.Koanf, handlerChain http.Hand
server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil {
logger.WithError(err).Fatal("Could not gracefully shutdown the server")
logger.Error("could not shutdown server gracefully", "error", err)
}
close(done)
}()
logger.WithFields(log.Fields{
"address": server.Addr, "url": publicAddress(serverName, serverPort),
}).Info("Server is ready to handle requests")
logger.Info(
"Server is ready to handle requests",
"address", server.Addr, "url", publicAddress(serverName, serverPort),
)
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.WithError(err).WithField(
"server_addr", server.Addr,
).Fatal("Could not listen on configured server address")
logger.Error("could not listen on configured server address", "server_addr", server.Addr)
return fmt.Errorf("could not create listener: %w", err)
}
<-done
logger.Infoln("Server stopped")
logger.Info("Server stopped")
return nil
}
func publicAddress(serverName string, serverPort int) string {
@ -252,3 +296,67 @@ func publicAddress(serverName string, serverPort int) string {
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
}

54
go.mod
View file

@ -1,60 +1,38 @@
module code.cacert.org/cacert/oidc-idp
go 1.19
go 1.22
require (
github.com/BurntSushi/toml v1.3.2
github.com/go-openapi/runtime v0.26.0
github.com/dustin/go-humanize v1.0.1
github.com/go-playground/form/v4 v4.2.1
github.com/gorilla/csrf v1.7.1
github.com/gorilla/csrf v1.7.2
github.com/gorilla/sessions v1.2.2
github.com/knadh/koanf v1.5.0
github.com/lestrrat-go/jwx v1.2.26
github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/ory/hydra-client-go v1.10.6
github.com/sirupsen/logrus v1.9.3
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/spf13/pflag v1.0.5
github.com/yuin/goldmark v1.5.5
golang.org/x/text v0.11.0
github.com/yuin/goldmark v1.7.1
golang.org/x/text v0.15.0
)
require (
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.4 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // 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.4 // indirect
github.com/go-openapi/validate v0.22.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/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
github.com/rogpeppe/go-internal v1.9.0 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/sys v0.10.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.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
)

231
go.sum
View file

@ -1,11 +1,8 @@
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.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/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=
@ -16,9 +13,6 @@ 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=
@ -40,14 +34,16 @@ 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/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=
@ -57,8 +53,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.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
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/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=
@ -67,80 +63,12 @@ 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.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M=
github.com/go-openapi/errors v0.20.4/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/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
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/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/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-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.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=
@ -169,20 +97,22 @@ 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/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.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/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/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=
@ -221,58 +151,40 @@ 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.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
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/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.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.26 h1:4iFo8FPRZGDYe1t19mQP0zTRqA7n8HnJ5lkIiDvJcB0=
github.com/lestrrat-go/jwx v1.2.26/go.mod h1:MaiCdGbn3/cckbOFSCluJlJMmp9dmZm5hDuIkx8ftpQ=
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/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=
@ -295,8 +207,6 @@ 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=
@ -306,20 +216,14 @@ 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.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/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/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/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/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/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=
@ -351,30 +255,20 @@ 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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/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=
@ -383,54 +277,31 @@ 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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/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/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
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/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.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.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
github.com/yuin/goldmark v1.5.5/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=
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.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE=
go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
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/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=
@ -459,19 +330,19 @@ 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=
@ -488,9 +359,7 @@ 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=
@ -509,23 +378,23 @@ 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.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.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=
@ -533,22 +402,17 @@ 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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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=
@ -591,12 +455,7 @@ 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=
@ -608,8 +467,6 @@ 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=

187
internal/handlers/common.go Normal file
View file

@ -0,0 +1,187 @@
/*
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 2020-2023 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -22,6 +22,7 @@ import (
"errors"
"fmt"
"html/template"
"log/slog"
"net/http"
"net/url"
"strings"
@ -29,30 +30,26 @@ 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"
"github.com/ory/hydra-client-go/client/admin"
hydra "github.com/ory/hydra-client-go/models"
log "github.com/sirupsen/logrus"
client "github.com/ory/hydra-client-go/v2"
"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 {
adminClient admin.ClientService
bundle *i18n.Bundle
consentTemplate *template.Template
logger *log.Logger
messageCatalog *services.MessageCatalog
logger *slog.Logger
trans *services.I18NService
adminClient client.OAuth2API
templates TemplateCache
}
type ConsentInformation struct {
GrantedScopes []string `form:"scope"`
SelectedClaims []string `form:"claims"`
ConsentChecked bool `form:"consent"`
ConsentAction string `form:"consent"`
}
type UserInfo struct {
@ -71,8 +68,6 @@ const (
ScopeEmail = "email"
)
const OneDayInSeconds = 86400
func init() {
supportedScopes = make(map[string]*i18n.Message)
supportedScopes[ScopeOpenID] = &i18n.Message{
@ -113,10 +108,9 @@ func (i *UserInfo) GetFullName() string {
func (h *ConsentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
challenge := r.URL.Query().Get("consent_challenge")
h.logger.WithField("consent_challenge", challenge).Debug("received consent challenge")
h.logger.Debug("received consent challenge", "consent_challenge", challenge)
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(h.bundle, accept)
localizer := getLocalizer(h.trans, r)
// retrieve consent information
consentData, requestedClaims, err := h.getRequestedConsentInformation(challenge, r)
@ -125,82 +119,195 @@ func (h *ConsentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
switch r.Method {
case http.MethodGet:
if err := h.renderConsentForm(w, r, consentData, requestedClaims, localizer); err != nil {
h.logger.WithError(err).Error("could not render consent form")
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)
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)
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
}
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.WithError(err).Error("could not decode consent form")
h.logger.Error("could not decode consent form", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if consentInfo.ConsentChecked {
sessionData, err := h.getSessionData(r, consentInfo, requestedClaims, consentData.Payload)
if consentInfo.ConsentAction == "consent" {
consentRequest, err := h.rememberNewConsent(consentData, consentInfo, requestedClaims, session)
if err != nil {
h.logger.WithError(err).Error("could not get session data")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
h.logger.Error("could not accept consent", "error", err)
return
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
consentRequest, err := h.adminClient.AcceptConsentRequest(
admin.NewAcceptConsentRequestParams().WithConsentChallenge(challenge).WithBody(
&hydra.AcceptConsentRequest{
GrantAccessTokenAudience: nil,
GrantScope: consentInfo.GrantedScopes,
HandledAt: hydra.NullTime(time.Now()),
Remember: true,
RememberFor: OneDayInSeconds,
Session: sessionData,
}).WithTimeout(TimeoutTen))
err = h.acceptConsent(w, r, challenge, consentRequest)
if err != nil {
h.logger.WithError(err).Error("accept consent request failed")
h.logger.Error("could not accept consent", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
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
}
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
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
}
return nil
}
consentRequest, err := h.adminClient.RejectConsentRequest(
admin.NewRejectConsentRequestParams().WithConsentChallenge(challenge).WithBody(
&hydra.RejectRequest{}))
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 {
h.logger.WithError(err).Error("reject consent request failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
return nil, fmt.Errorf("could not get session data: %w", err)
}
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound)
}
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) (
*admin.GetConsentRequestOK,
*client.OAuth2ConsentRequest,
*models.OIDCClaimsRequest,
error,
) {
consentData, err := h.adminClient.GetConsentRequest(
admin.NewGetConsentRequestParams().WithConsentChallenge(challenge))
consentRequest, response, err := h.adminClient.GetOAuth2ConsentRequest(
r.Context(),
).ConsentChallenge(challenge).Execute()
if err != nil {
h.logger.WithError(err).Error("error getting consent information")
h.logger.Error("error getting consent information", "error", err)
if errorBucket := GetErrorBucket(r); errorBucket != nil {
errorDetails := &ErrorDetails{
@ -214,73 +321,85 @@ func (h *ConsentHandler) getRequestedConsentInformation(challenge string, r *htt
return nil, nil, fmt.Errorf("error getting consent information: %w", err)
}
defer func() { _ = response.Body.Close() }()
h.logger.Debug(
"response for GetOAuth2ConsentRequest",
"response", response.Status, "consent_request", consentRequest,
)
var requestedClaims models.OIDCClaimsRequest
requestURL, err := url.Parse(consentData.Payload.RequestURL)
requestURLStr := consentRequest.GetRequestUrl()
requestURL, err := url.Parse(requestURLStr)
if err != nil {
h.logger.WithError(err).WithField(
"request_url", consentData.Payload.RequestURL,
).Warn("could not parse original request URL")
h.logger.Warn(
"could not parse original request URL",
"error", err, "request_url", requestURLStr,
)
} else {
claimsParameter := requestURL.Query().Get("claims")
if claimsParameter != "" {
decoder := json.NewDecoder(strings.NewReader(claimsParameter))
err := decoder.Decode(&requestedClaims)
if err != nil {
h.logger.WithError(err).WithField(
"claims_parameter", claimsParameter,
).Warn("ignoring claims request parameter that could not be decoded")
h.logger.Warn(
"ignoring claims request parameter that could not be decoded",
"error", err, "claims_parameter", claimsParameter,
)
}
}
}
return consentData, &requestedClaims, nil
return consentRequest, &requestedClaims, nil
}
func (h *ConsentHandler) renderConsentForm(
w http.ResponseWriter,
r *http.Request,
consentData *admin.GetConsentRequestOK,
consentRequest *client.OAuth2ConsentRequest,
claims *models.OIDCClaimsRequest,
localizer *i18n.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)
) {
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),
)
}
// render consent form
client := consentData.GetPayload().Client
err := h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{
"Title": trans("TitleRequestConsent"),
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{},
"client": client,
"requestedScope": h.mapRequestedScope(consentData.GetPayload().RequestedScope, localizer),
"LogoURI": clientLogoURI,
"ClientName": clientName,
"requestedScope": h.mapRequestedScope(consentRequest.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,
})),
"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),
})
if err != nil {
return fmt.Errorf("rendering failed: %w", err)
}
return nil
}
type scopeWithLabel struct {
@ -289,14 +408,14 @@ type scopeWithLabel struct {
}
func (h *ConsentHandler) mapRequestedScope(
scope hydra.StringSlicePipeDelimiter,
scope []string,
localizer *i18n.Localizer,
) []*scopeWithLabel {
result := make([]*scopeWithLabel, 0)
for _, scopeName := range scope {
if _, ok := supportedScopes[scopeName]; !ok {
h.logger.WithField("scope", scopeName).Warn("ignoring unsupported scope")
h.logger.Warn("ignoring unsupported scope", "scope", scopeName)
continue
}
@ -305,7 +424,7 @@ func (h *ConsentHandler) mapRequestedScope(
DefaultMessage: supportedScopes[scopeName],
})
if err != nil {
h.logger.WithError(err).WithField("scope", scopeName).Warn("could not localize scope label")
h.logger.Warn("could not localize scope label", "error", err, "scope", scopeName)
label = scopeName
}
@ -331,8 +450,14 @@ func (h *ConsentHandler) mapRequestedClaims(
for _, claimElement := range []*models.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.WithField("claim", k).Warn("ignoring unsupported claim")
h.logger.Warn("ignoring unsupported claim", "claim", k)
continue
}
@ -341,7 +466,7 @@ func (h *ConsentHandler) mapRequestedClaims(
DefaultMessage: supportedClaims[k],
})
if err != nil {
h.logger.WithError(err).WithField("claim", k).Warn("could not localize claim label")
h.logger.Warn("could not localize claim label", "error", err, "claim", k)
label = k
}
@ -361,41 +486,43 @@ func (h *ConsentHandler) mapRequestedClaims(
}
func (h *ConsentHandler) getSessionData(
r *http.Request,
info ConsentInformation,
requestedScopes,
grantedScopes,
selectedClaims []string,
claims *models.OIDCClaimsRequest,
payload *hydra.ConsentRequest,
) (*hydra.ConsentRequestSession, error) {
idTokenData := make(map[string]interface{}, 0)
accessTokenData := make(map[string]interface{}, 0)
session *sessions.Session,
) (*client.AcceptOAuth2ConsentRequestSession, error) {
idTokenData := make(map[string]interface{})
userInfo := h.GetUserInfoFromClientCertificate(r, payload.Subject)
if err := h.fillTokenData(accessTokenData, payload.RequestedScope, claims, info, userInfo); err != nil {
if err := h.fillTokenData(
idTokenData,
requestedScopes,
grantedScopes,
claims,
selectedClaims,
session.Values,
); err != nil {
return nil, err
}
if err := h.fillTokenData(idTokenData, payload.RequestedScope, claims, info, userInfo); err != nil {
return nil, err
}
consentSession := client.NewAcceptOAuth2ConsentRequestSession()
consentSession.SetIdToken(idTokenData)
return &hydra.ConsentRequestSession{
AccessToken: accessTokenData,
IDToken: idTokenData,
}, nil
return consentSession, nil
}
func (h *ConsentHandler) fillTokenData(
m map[string]interface{},
requestedScope hydra.StringSlicePipeDelimiter,
requestedScopes []string,
grantedScopes []string,
claimsRequest *models.OIDCClaimsRequest,
consentInformation ConsentInformation,
userInfo *UserInfo,
selectedClaims []string,
sessionData map[interface{}]interface{},
) error {
for _, scope := range requestedScope {
for _, scope := range requestedScopes {
granted := false
for _, k := range consentInformation.GrantedScopes {
for _, k := range grantedScopes {
if k == scope {
granted = true
@ -412,8 +539,8 @@ func (h *ConsentHandler) fillTokenData(
// email
// OPTIONAL. This scope value requests access to the email and
// email_verified Claims.
m[openid.EmailKey] = userInfo.Email
m[openid.EmailVerifiedKey] = userInfo.EmailVerified
m[openid.EmailKey] = sessionData[services.SessionEmail]
m[openid.EmailVerifiedKey] = true
case ScopeProfile:
// profile
// OPTIONAL. This scope value requests access to the
@ -421,12 +548,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] = userInfo.GetFullName()
m[openid.NameKey] = sessionData[services.SessionFullName]
}
}
if userInfoClaims := claimsRequest.GetUserInfo(); userInfoClaims != nil {
err := h.parseUserInfoClaims(m, userInfoClaims, consentInformation)
err := h.parseUserInfoClaims(m, userInfoClaims, selectedClaims)
if err != nil {
return err
}
@ -438,12 +565,12 @@ func (h *ConsentHandler) fillTokenData(
func (h *ConsentHandler) parseUserInfoClaims(
m map[string]interface{},
userInfoClaims *models.ClaimElement,
consentInformation ConsentInformation,
selectedClaims []string,
) error {
for claimName, claim := range *userInfoClaims {
granted := false
for _, k := range consentInformation.SelectedClaims {
for _, k := range selectedClaims {
if k == claimName {
granted = true
@ -469,43 +596,9 @@ func (h *ConsentHandler) parseUserInfoClaims(
}
if claim.IsEssential() {
h.logger.WithField("claim", claimName).Warn("handling for essential claim not implemented")
h.logger.Warn("handling for essential claim not implemented", "claim", claimName)
} else {
h.logger.WithField("claim", claimName).Warn("handling for claim not implemented")
}
}
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.WithField(
"email", email,
).Info("authenticated with client certificate for email address")
if subject == email {
verified = true
}
}
if !verified {
h.logger.WithField(
"subject", subject,
).Warn("authentication attempt with a wrong certificate that did not contain the requested address")
return nil
}
return &UserInfo{
Email: subject,
EmailVerified: verified,
CommonName: firstCert.Subject.CommonName,
h.logger.Warn("handling for claim not implemented", "claim", claimName)
}
}
@ -513,23 +606,12 @@ func (h *ConsentHandler) GetUserInfoFromClientCertificate(r *http.Request, subje
}
func NewConsentHandler(
logger *log.Logger,
bundle *i18n.Bundle,
messageCatalog *services.MessageCatalog,
adminClient admin.ClientService,
logger *slog.Logger, templateCache TemplateCache, trans *services.I18NService, adminClient client.OAuth2API,
) *ConsentHandler {
consentTemplate := template.Must(
template.ParseFS(
ui.Templates,
"templates/base.gohtml",
"templates/consent.gohtml",
))
return &ConsentHandler{
adminClient: adminClient,
bundle: bundle,
consentTemplate: consentTemplate,
logger: logger,
messageCatalog: messageCatalog,
trans: trans,
adminClient: adminClient,
templates: templateCache,
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2020-2023 CAcert Inc.
Copyright 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 2020-2023 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -22,12 +22,9 @@ import (
"context"
"fmt"
"html/template"
"io/fs"
"log/slog"
"net/http"
"github.com/nicksnyder/go-i18n/v2/i18n"
log "github.com/sirupsen/logrus"
"code.cacert.org/cacert/oidc-idp/internal/services"
"code.cacert.org/cacert/oidc-idp/ui"
)
@ -46,34 +43,24 @@ type ErrorDetails struct {
}
type ErrorBucket struct {
logger *slog.Logger
trans *services.I18NService
errorDetails *ErrorDetails
templates *template.Template
logger *log.Logger
bundle *i18n.Bundle
messageCatalog *services.MessageCatalog
templates TemplateCache
}
func (b *ErrorBucket) serveHTTP(w http.ResponseWriter, r *http.Request) {
if b.errorDetails != nil {
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(b.bundle, accept)
err := b.templates.Lookup("base").Execute(w, map[string]interface{}{
"Title": b.messageCatalog.LookupMessage(
localizer := getLocalizer(b.trans, r)
b.templates.render(b.logger, w, Error, map[string]interface{}{
"Title": b.trans.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,
)
}
}
}
@ -102,9 +89,7 @@ func (w *errorResponseWriter) WriteHeader(code int) {
if code >= http.StatusBadRequest {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.errorBucket.AddError(&ErrorDetails{
ErrorMessage: http.StatusText(code),
})
w.errorBucket.AddError(&ErrorDetails{ErrorCode: "HTTP error"})
}
w.ResponseWriter.WriteHeader(code)
@ -132,27 +117,14 @@ func (w *errorResponseWriter) Write(content []byte) (int, error) {
}
func ErrorHandling(
logger *log.Logger,
templateFS fs.FS,
bundle *i18n.Bundle,
messageCatalog *services.MessageCatalog,
logger *slog.Logger, templateCache TemplateCache, trans *services.I18NService,
) (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{
templates: errorTemplates,
logger: logger,
bundle: bundle,
messageCatalog: messageCatalog,
trans: trans,
templates: templateCache,
}
next.ServeHTTP(
&errorResponseWriter{w, errorBucket, http.StatusOK},
@ -164,9 +136,8 @@ func ErrorHandling(
}
type ErrorHandler struct {
logger *log.Logger
bundle *i18n.Bundle
messageCatalog *services.MessageCatalog
logger *slog.Logger
trans *services.I18NService
template *template.Template
}
@ -177,21 +148,20 @@ func (h *ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(h.bundle, accept)
localizer := getLocalizer(h.trans, r)
errorName := r.URL.Query().Get("error")
errorDescription := r.URL.Query().Get("error_description")
h.logger.WithFields(log.Fields{
"error_name": errorName,
"error_description": errorDescription,
}).Debug("error from Hydra")
h.logger.Debug(
"error from Hydra",
"error_name", errorName, "error_description", errorDescription,
)
rendered := bytes.NewBuffer(make([]byte, 0))
msg := h.messageCatalog.LookupMessage
msgMarkdown := h.messageCatalog.LookupMarkdownMessage
msg := h.trans.LookupMessage
msgMarkdown := h.trans.LookupMarkdownMessage
err := h.template.Lookup("base").Execute(rendered, map[string]interface{}{
"Title": msg("AuthServerErrorTitle", nil, localizer),
@ -201,7 +171,7 @@ func (h *ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
"ErrorMessage": errorDescription,
})
if err != nil {
h.logger.WithError(err).Error("template rendering failed")
h.logger.Error("template rendering failed", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@ -213,11 +183,10 @@ func (h *ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(rendered.Bytes())
}
func NewErrorHandler(logger *log.Logger, bundle *i18n.Bundle, messageCatalog *services.MessageCatalog) *ErrorHandler {
func NewErrorHandler(logger *slog.Logger, trans *services.I18NService) *ErrorHandler {
return &ErrorHandler{
logger: logger,
bundle: bundle,
messageCatalog: messageCatalog,
trans: trans,
template: template.Must(template.ParseFS(
ui.Templates,
"templates/base.gohtml",

View file

@ -1,5 +1,5 @@
/*
Copyright 2020-2023 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -19,48 +19,40 @@ package handlers
import (
"bytes"
"crypto/x509"
"errors"
"context"
"fmt"
"html/template"
"io"
"log/slog"
"net/http"
"strconv"
"time"
"github.com/gorilla/csrf"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/ory/hydra-client-go/client/admin"
hydra "github.com/ory/hydra-client-go/models"
log "github.com/sirupsen/logrus"
client "github.com/ory/hydra-client-go/v2"
"code.cacert.org/cacert/oidc-idp/internal/services"
"code.cacert.org/cacert/oidc-idp/ui"
)
type acrType string
const (
ClientCertificate acrType = "cert" // client certificate login
// ClientCertificateOTP acrType = "cert+otp"
// ClientCertificateToken acrType = "cert+token"
// 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"
)
type templateName string
type contextKey int
const (
CertificateLogin templateName = "cert"
NoEmailsInClientCertificate templateName = "no_emails"
NoChallengeInRequest templateName = "no_challenge"
ctxKeyMessage contextKey = iota
)
const TimeoutTen = 10 * time.Second
type LoginHandler struct {
adminClient admin.ClientService
bundle *i18n.Bundle
logger *log.Logger
templates map[templateName]*template.Template
messageCatalog *services.MessageCatalog
logger *slog.Logger
trans *services.I18NService
adminClient client.OAuth2API
templates TemplateCache
}
func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -70,8 +62,7 @@ func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(h.bundle, accept)
localizer := getLocalizer(h.trans, r)
challenge := r.URL.Query().Get("login_challenge")
@ -81,12 +72,12 @@ func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
h.logger.WithField("challenge", challenge).Debug("received login challenge")
h.logger.Debug("received login challenge", "challenge", challenge)
certEmails := h.getEmailAddressesFromClientCertificate(r)
certFullName, certEmails := getDataFromClientCert(h.logger, r)
if certEmails == nil {
h.renderNoEmailsInClientCertificate(w, localizer)
renderNoEmailsInClientCertificate(h.logger, h.templates, h.trans, w, localizer)
return
}
@ -94,7 +85,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, certEmails, localizer)
h.handlePost(w, r, challenge, certFullName, certEmails, localizer)
}
}
@ -105,161 +96,217 @@ func (h *LoginHandler) handleGet(
certEmails []string,
localizer *i18n.Localizer,
) {
loginRequest, err := h.adminClient.GetLoginRequest(admin.NewGetLoginRequestParams().WithLoginChallenge(challenge))
oAuth2LoginRequest, response, err := h.adminClient.GetOAuth2LoginRequest(
r.Context(),
).LoginChallenge(challenge).Execute()
if err != nil {
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
}
h.logger.Warn(
"could not get login request for challenge",
"error", err, "challenge", challenge,
)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
h.renderRequestForClientCert(w, r, certEmails, localizer, loginRequest)
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
}
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, challenge, localizer)
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)
return
}
// perform certificate auth
h.logger.WithField("emails", certEmails).Info("will perform certificate authentication")
h.logger.Info(
"will perform certificate authentication",
"emails", certEmails, "full_name", certFullName,
)
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
loginRequest, err := h.adminClient.AcceptLoginRequest(
admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(
&hydra.AcceptLoginRequest{
Acr: string(ClientCertificate),
Remember: true,
RememberFor: 0,
Subject: &userID,
}).WithTimeout(TimeoutTen))
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()
if err != nil {
h.logger.WithError(err).Error("error getting login request")
h.logger.Error("error getting login request", "error", 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(
&hydra.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.WithError(err).Error("error getting reject login request")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Set("Location", *rejectLoginRequest.GetPayload().RedirectTo)
defer func() { _ = response.Body.Close() }()
h.logger.Debug("got response for AcceptOAuth2LoginRequest",
"response", response.Status, "accept_login_request", loginRequest,
)
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)
}
}
w.Header().Add("Location", loginRequest.GetRedirectTo())
w.WriteHeader(http.StatusFound)
}
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]
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)
if !isClientCertificate(firstCert) {
return nil
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
}
for _, email := range firstCert.EmailAddresses {
h.logger.WithField(
"email", email,
).Info("authenticated with a client certificate for email address")
}
defer func() { _ = response.Body.Close() }()
return firstCert.EmailAddresses
}
h.logger.DebugContext(
r.Context(),
"got response for RejectOAuth2LoginRequest",
"response", response.Status, "reject_login_request", rejectLoginRequest,
)
return nil
w.Header().Set("Location", rejectLoginRequest.GetRedirectTo())
w.WriteHeader(http.StatusFound)
}
func isClientCertificate(cert *x509.Certificate) bool {
for _, ext := range cert.ExtKeyUsage {
if ext == x509.ExtKeyUsageClientAuth {
return true
}
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
}
return false
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) renderRequestForClientCert(
@ -267,11 +314,11 @@ func (h *LoginHandler) renderRequestForClientCert(
r *http.Request,
emails []string,
localizer *i18n.Localizer,
loginRequest *admin.GetLoginRequestOK,
loginRequest *client.OAuth2LoginRequest,
) {
msg := h.messageCatalog.LookupMessage
msgPlural := h.messageCatalog.LookupMessagePlural
msgMarkdown := h.messageCatalog.LookupMarkdownMessage
msg := h.trans.LookupMessage
msgPlural := h.trans.LookupMessagePlural
msgMarkdown := h.trans.LookupMarkdownMessage
rendered := bytes.NewBuffer(make([]byte, 0))
@ -280,7 +327,7 @@ func (h *LoginHandler) renderRequestForClientCert(
csrf.TemplateTag: csrf.TemplateField(r),
"IntroText": template.HTML(msgMarkdown( //nolint:gosec
"CertLoginIntroText",
map[string]interface{}{"ClientName": loginRequest.GetPayload().Client.ClientName},
map[string]interface{}{"ClientName": loginRequest.Client.ClientName},
localizer,
)),
"EmailChoiceText": msgPlural("EmailChoiceText", nil, localizer, len(emails)),
@ -288,9 +335,10 @@ func (h *LoginHandler) renderRequestForClientCert(
"RequestText": msg("CertLoginRequestText", nil, localizer),
"AcceptLabel": msg("LabelAcceptCertLogin", nil, localizer),
"RejectLabel": msg("LabelRejectCertLogin", nil, localizer),
"FlashMessage": r.Context().Value(ctxKeyMessage),
})
if err != nil {
h.logger.WithError(err).Error("template rendering failed")
h.logger.Error("template rendering failed", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@ -312,32 +360,17 @@ 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) {
msg := h.messageCatalog.LookupMessage
err := h.templates[NoEmailsInClientCertificate].Lookup("base").Execute(w, map[string]interface{}{
"Title": msg("NoEmailsInClientCertificateTitle", nil, localizer),
"Explanation": msg("NoEmailsInClientCertificateExplanation", nil, localizer),
})
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.messageCatalog.LookupMessage("NoChallengeInRequestTitle", nil, localizer),
"Explanation": template.HTML(h.messageCatalog.LookupMarkdownMessage( //nolint:gosec
"Title": h.trans.LookupMessage("NoChallengeInRequestTitle", nil, localizer),
"Explanation": template.HTML(h.trans.LookupMarkdownMessage( //nolint:gosec
"NoChallengeInRequestExplanation",
nil,
localizer,
)),
})
if err != nil {
h.logger.WithError(err).Error("template rendering failed")
h.logger.Error("template rendering failed", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@ -345,32 +378,12 @@ func (h *LoginHandler) renderNoChallengeInRequest(w http.ResponseWriter, localiz
}
func NewLoginHandler(
logger *log.Logger,
bundle *i18n.Bundle,
messageCatalog *services.MessageCatalog,
adminClient admin.ClientService,
logger *slog.Logger, tc TemplateCache, trans *services.I18NService, adminClient client.OAuth2API,
) *LoginHandler {
return &LoginHandler{
adminClient: adminClient,
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,
trans: trans,
adminClient: adminClient,
templates: tc,
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2020-2023 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -18,53 +18,61 @@ limitations under the License.
package handlers
import (
"bytes"
"html/template"
"log/slog"
"net/http"
"time"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/ory/hydra-client-go/client/admin"
log "github.com/sirupsen/logrus"
client "github.com/ory/hydra-client-go/v2"
"code.cacert.org/cacert/oidc-idp/internal/services"
"code.cacert.org/cacert/oidc-idp/ui"
)
type LogoutHandler struct {
adminClient admin.ClientService
logger *log.Logger
adminClient client.OAuth2API
logger *slog.Logger
}
func (h *LogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
const Ten = 10 * time.Second
challenge := r.URL.Query().Get("logout_challenge")
h.logger.WithField("challenge", challenge).Debug("received logout challenge")
h.logger.Debug("received logout challenge", "challenge", challenge)
logoutRequest, err := h.adminClient.GetLogoutRequest(
admin.NewGetLogoutRequestParams().WithLogoutChallenge(challenge).WithTimeout(Ten))
logoutRequest, response, err := h.adminClient.GetOAuth2LogoutRequest(
r.Context(),
).LogoutChallenge(challenge).Execute()
if err != nil {
h.logger.WithError(err).Error("error getting logout requests")
h.logger.Error("error getting logout requests", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
h.logger.WithField("logout_request", logoutRequest.Payload).Debug("received logout request")
defer func() { _ = response.Body.Close() }()
acceptLogoutRequest, err := h.adminClient.AcceptLogoutRequest(
admin.NewAcceptLogoutRequestParams().WithLogoutChallenge(challenge))
h.logger.Debug(
"got response for GetOAuth2LogoutRequest",
"response", response.Status, "logout_request", logoutRequest,
)
acceptLogoutRequest, response, err := h.adminClient.AcceptOAuth2LogoutRequest(
r.Context(),
).LogoutChallenge(challenge).Execute()
if err != nil {
h.logger.WithError(err).Error("accept logout request failed")
h.logger.Error("accept logout request failed", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
w.Header().Set("Location", *acceptLogoutRequest.GetPayload().RedirectTo)
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.WriteHeader(http.StatusFound)
}
func NewLogoutHandler(logger *log.Logger, adminClient admin.ClientService) *LogoutHandler {
func NewLogout(logger *slog.Logger, adminClient client.OAuth2API) *LogoutHandler {
return &LogoutHandler{
logger: logger,
adminClient: adminClient,
@ -72,10 +80,9 @@ func NewLogoutHandler(logger *log.Logger, adminClient admin.ClientService) *Logo
}
type LogoutSuccessHandler struct {
bundle *i18n.Bundle
logger *log.Logger
messageCatalog *services.MessageCatalog
template *template.Template
logger *slog.Logger
trans *services.I18NService
templates TemplateCache
}
func (h *LogoutSuccessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -85,41 +92,20 @@ func (h *LogoutSuccessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
return
}
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(h.bundle, accept)
localizer := getLocalizer(h.trans, r)
rendered := bytes.NewBuffer(make([]byte, 0))
err := h.template.Lookup("base").Execute(rendered, map[string]interface{}{
"Title": h.messageCatalog.LookupMessage("LogoutSuccessfulTitle", nil, localizer),
"Explanation": template.HTML(h.messageCatalog.LookupMarkdownMessage( //nolint:gosec
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,
)),
})
if err != nil {
h.logger.WithError(err).Error("template rendering failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
_, _ = w.Write(rendered.Bytes())
}
func NewLogoutSuccessHandler(
logger *log.Logger,
bundle *i18n.Bundle,
messageCatalog *services.MessageCatalog,
func NewLogoutSuccess(
logger *slog.Logger, templateCache TemplateCache, trans *services.I18NService,
) *LogoutSuccessHandler {
return &LogoutSuccessHandler{
bundle: bundle,
logger: logger,
messageCatalog: messageCatalog,
template: template.Must(template.ParseFS(ui.Templates,
"templates/base.gohtml",
"templates/logout_successful.gohtml",
)),
}
return &LogoutSuccessHandler{logger: logger, trans: trans, templates: templateCache}
}

372
internal/handlers/manage.go Normal file
View file

@ -0,0 +1,372 @@
/*
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 2020-2023 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -20,10 +20,9 @@ package handlers
import (
"context"
"fmt"
"log/slog"
"net/http"
"sync/atomic"
log "github.com/sirupsen/logrus"
)
type key int
@ -54,7 +53,7 @@ func (sci *statusCodeInterceptor) Write(content []byte) (int, error) {
return count, nil
}
func Logging(logger *log.Logger) func(http.Handler) http.Handler {
func Logging(logger *slog.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}
@ -63,7 +62,9 @@ func Logging(logger *log.Logger) func(http.Handler) http.Handler {
if !ok {
requestID = "unknown"
}
logger.Infof(
logger.Info(
fmt.Sprintf(
"[%s] %s \"%s %s\" %d %d \"%s\"",
requestID,
r.RemoteAddr,
@ -72,6 +73,7 @@ func Logging(logger *log.Logger) func(http.Handler) http.Handler {
interceptor.code,
interceptor.count,
r.UserAgent(),
),
)
}()
next.ServeHTTP(interceptor, r)
@ -86,7 +88,9 @@ 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))
})
@ -96,7 +100,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, r *http.Request) {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if atomic.LoadInt32(&Healthy) == 1 {
w.WriteHeader(http.StatusNoContent)

View file

@ -1,5 +1,5 @@
/*
Copyright 2020-2023 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -18,7 +18,9 @@ limitations under the License.
package handlers
import (
"crypto/x509"
"fmt"
"log/slog"
"net/http"
"time"
)
@ -27,8 +29,37 @@ 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 2020-2023 CAcert Inc.
Copyright 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 2020-2023 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -19,6 +19,7 @@ package services
import (
"fmt"
"log/slog"
"os"
"strings"
@ -28,7 +29,6 @@ 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"
)
@ -51,7 +51,7 @@ var DefaultConfig = map[string]interface{}{
}
func ConfigureApplication(
logger *logrus.Logger,
logger *slog.Logger,
appName string,
defaultConfig map[string]interface{},
) (*koanf.Koanf, error) {
@ -70,7 +70,7 @@ func ConfigureApplication(
var err error
if err = f.Parse(os.Args[1:]); err != nil {
logger.WithError(err).Fatal("could not parse command line arguments")
return nil, fmt.Errorf("could not parse command line arguments: %w", err)
}
config := koanf.New(".")
@ -78,18 +78,22 @@ func ConfigureApplication(
_ = config.Load(confmap.Provider(defaultConfig, "."), nil)
if err = config.Load(file.Provider(defaultFile), toml.Parser()); err != nil && !os.IsNotExist(err) {
logrus.WithError(err).WithField("file", defaultFile).Fatal("error loading configuration from file")
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.WithError(err).WithField("file", c).Fatal("error loading configuration from file")
logger.Error("could not load configuration from file", "file", c)
return nil, fmt.Errorf("error loading configuration: %w", err)
}
}
if err = config.Load(posflag.Provider(f, ".", config), nil); err != nil {
logger.WithError(err).Fatal("error loading configuration from command line")
return nil, fmt.Errorf("error loading configuration from command line: %w", err)
}
prefix := fmt.Sprintf("%s_", strings.ToUpper(appName))
@ -97,7 +101,7 @@ func ConfigureApplication(
if err = config.Load(env.Provider(prefix, ".", func(s string) string {
return strings.ReplaceAll(strings.ToLower(strings.TrimPrefix(s, prefix)), "_", ".")
}), nil); err != nil {
logrus.WithError(err).Fatal("error loading configuration from environment")
return nil, fmt.Errorf("error loading configuration from environment: %w", err)
}
return config, nil

View file

@ -1,5 +1,5 @@
/*
Copyright 2020-2023 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -21,8 +21,9 @@ import (
"bytes"
"errors"
"fmt"
"log/slog"
"net/http"
log "github.com/sirupsen/logrus"
"github.com/yuin/goldmark"
"code.cacert.org/cacert/oidc-idp/translations"
@ -32,8 +33,136 @@ import (
"golang.org/x/text/language"
)
func AddMessages(catalog *MessageCatalog) error {
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 {
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",
@ -47,6 +176,65 @@ func AddMessages(catalog *MessageCatalog) error {
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",
@ -59,15 +247,21 @@ func AddMessages(catalog *MessageCatalog) 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 <strong>{{ .client }}</strong> application requested your consent for the following set of " +
"permissions:",
Other: "The **{{ .client }}** application requested your consent for the following set of permissions:",
}
messages["IntroConsentMoreInformation"] = &i18n.Message{
ID: "IntroConsentMoreInformation",
Other: "You can find more information about <strong>{{ .client }}</strong> at " +
"<a href=\"{{ .clientLink }}\">its description page</a>.",
Other: "You can find more information about **{{ .client }}** at [its description page]({{ .clientLink }}).",
}
messages["ClaimsInformation"] = &i18n.Message{
ID: "ClaimsInformation",
@ -79,7 +273,7 @@ func AddMessages(catalog *MessageCatalog) error {
}
messages["CertLoginIntroText"] = &i18n.Message{
ID: "CertLoginIntroText",
Other: "The application <strong>{{ .ClientName }}</strong> requests a login.",
Other: "The application **{{ .ClientName }}** requests a login.",
}
messages["EmailChoiceText"] = &i18n.Message{
ID: "EmailChoiceText",
@ -87,6 +281,15 @@ func AddMessages(catalog *MessageCatalog) 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",
@ -109,6 +312,10 @@ func AddMessages(catalog *MessageCatalog) 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",
@ -117,10 +324,30 @@ func AddMessages(catalog *MessageCatalog) error {
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",
@ -141,154 +368,16 @@ An email address is required to authenticate yourself.`,
" [the ORY Hydra documentation](https://www.ory.sh/docs/oauth2-oidc/custom-login-consent/flow).",
}
catalog.AddMessages(messages)
s.catalog.AddMessages(messages)
return nil
}
type MessageCatalog struct {
messages map[string]*i18n.Message
logger *log.Logger
func (s *I18NService) Localizer(languages string) *i18n.Localizer {
return i18n.NewLocalizer(s.bundle, languages)
}
func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) {
for key, value := range messages {
m.messages[key] = value
}
}
func (m *MessageCatalog) LookupErrorMessage(
tag string,
field string,
value interface{},
localizer *i18n.Localizer,
) string {
fieldTag := fmt.Sprintf("%s-%s", field, tag)
message, ok := m.messages[fieldTag]
if !ok {
m.logger.WithField("field_tag", fieldTag).Info("no specific error message for field and tag")
message, ok = m.messages[tag]
if !ok {
m.logger.WithField("tag", tag).Info("no specific error message for tag")
message, ok = m.messages["unknown"]
if !ok {
m.logger.Warn("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.WithError(err).Error("localization failed")
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.WithField("id", id).Warn("no translation found for 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.WithField("id", id).Warn("no translation found for 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.WithField("id", id).Warn("no translation found for 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) {
func InitI18n(logger *slog.Logger, languages []string) *I18NService {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
@ -297,7 +386,7 @@ func InitI18n(logger *log.Logger, languages []string) (*i18n.Bundle, *MessageCat
bundleBytes, err := translations.Bundles.ReadFile(bundleName)
if err != nil {
logger.WithField("bundle", bundleName).Warn("message bundle not found")
logger.Warn("message bundle not found", "bundle", bundleName)
continue
}
@ -307,10 +396,10 @@ func InitI18n(logger *log.Logger, languages []string) (*i18n.Bundle, *MessageCat
catalog := initMessageCatalog(logger)
return bundle, catalog
return &I18NService{logger: logger, bundle: bundle, catalog: catalog}
}
func initMessageCatalog(logger *log.Logger) *MessageCatalog {
func initMessageCatalog(logger *slog.Logger) *MessageCatalog {
messages := make(map[string]*i18n.Message)
messages["ErrorTitle"] = &i18n.Message{
ID: "ErrorTitle",

View file

@ -0,0 +1,43 @@
/*
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

@ -0,0 +1,37 @@
/*
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

@ -6,9 +6,34 @@ other = "Eine Anfrage, die dein Browser an den Authorization-Server geschickt ha
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-e9f7c0522e49ffacc49e3fc35c6ffd31e495baf6"
other = "Die Anwendung <strong>{{ .ClientName }}</strong> fragt nach einer Anmeldung."
hash = "sha1-02871569c8d36e522cdb0716081ef62b7c7a71ec"
other = "Die Anwendung **{{ .ClientName }}** fragt nach einer Anmeldung."
[CertLoginRequestText]
hash = "sha1-1b20eea0f6fbb4ff139ecfe6b7a93c98cb14b8d7"
@ -18,10 +43,43 @@ 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, welches Du der Anwendung vorlegen möchtest:"
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:"
[ErrorTitle]
hash = "sha1-736aec25a98f5ec5b71400bb0163f891f509b566"
@ -35,13 +93,25 @@ 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-f58b8378238bd433deef3c3e6b0b70d0fd0dd59e"
other = "Auf der <a href=\"{{ .clientLink }}\">Beschreibungsseite</a> findest du mehr Informationen zu <strong>{{ .client }}</strong>."
hash = "sha1-836b2931b417e98db4102731604443ee2700123c"
other = "Auf der [Beschreibungsseite]({{ .clientLink }}) findest du mehr Informationen zu **{{ .client }}**."
[IntroConsentRequested]
hash = "sha1-3ac6a3583d40b5e8930c57531f0be9706f1e0194"
other = "Die Anwendung <strong>{{ .client }}</strong> hat deine Zustimmung für die Erteilung der folgenden Berechtigungen angefragt:"
hash = "sha1-db5e67a16c181c4d27baf5d0e3bf255224b0fffc"
other = "Die Anwendung **{{ .client }}** hat deine Zustimmung für die Erteilung der folgenden Berechtigungen angefragt:"
[LabelAcceptCertLogin]
description = "Label for a button to accept certificate login"
@ -52,6 +122,10 @@ 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"
@ -61,10 +135,18 @@ 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"
@ -77,6 +159,14 @@ other = "Du wurdest erfolreich abgemeldet."
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)."
@ -85,6 +175,14 @@ 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."
@ -116,3 +214,8 @@ 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,21 +1,33 @@
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 <strong>{{ .ClientName }}</strong> requests a login."
CertLoginIntroText = "The application **{{ .ClientName }}** 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."
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:"
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:"
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."
@ -25,6 +37,46 @@ 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:"
@ -36,3 +88,7 @@ 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 2020-2023 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");

View file

@ -26,13 +26,14 @@
<meta name="theme-color" content="#11568c">
<title>{{ .Title }}</title>
</head>
<body class="text-center idp d-flex flex-column h-100">
<main role="main" class="flex-shrink-0">
<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" . }}
</main>
<footer class="footer mt-auto py-3">
<div class="container">
<span class="text-muted small">© 2020-2023 <a href="https://www.cacert.org/">CAcert</a></span>
<span class="text-muted small">© <a href="https://www.cacert.org/">CAcert</a></span>
</div>
</footer>
<script type="text/javascript" src="/js/cacert.bundle.js"></script>

View file

@ -1,9 +1,14 @@
{{ define "content" }}
<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>
<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 }}
@ -11,20 +16,22 @@
<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>
<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 }}
{{ 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"
<button class="btn 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>
</main>
{{ end }}

View file

@ -0,0 +1,11 @@
{{ 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,13 +1,13 @@
{{ define "content" }}
<form class="form-consent" method="post">
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
<main role="main" class="container">
<h1 class="h3 mb-3">{{ .Title }}</h1>
{{ if .client.LogoURI }}
<p>
<img src="{{ .client.LogoURI }}" alt="{{ .client.ClientName }}"/>
{{ if .LogoURI }}
<p class="text-center">
<img src="{{ .LogoURI }}" alt="{{ .ClientName }}" width="240" height="240" />
</p>
{{ end }}
<p class="text-left">{{ .IntroConsentRequested }}</p>
{{ .IntroConsentRequested }}
<form method="post">
<ul class="list-group text-left small mb-3">
{{ range $i, $scope := .requestedScope }}
<li class="list-group-item">
@ -26,15 +26,16 @@
{{ end}}
</ul>
{{ end }}
<p class="text-left">{{ .IntroMoreInformation }}</p>
{{ if .HasMoreInformation }}
{{ .IntroMoreInformation }}
{{ end }}
{{ .csrfField }}
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="consent" id="consent" value="true"/>
{{ .LabelConsent }}</label>
</div>
{{ .LabelConsent }}
<button class="btn btn-lg btn-primary btn-block" type="submit">{{ .LabelSubmit }}</button>
<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>
{{ end }}

View file

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

View file

@ -1,10 +1,9 @@
{{ define "content" }}
<div class="container">
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
<main class="container">
<h1>{{ .Title }}</h1>
{{ .Explanation }}
{{ if .ErrorMessage }}
<div class="alert alert-danger">{{ .ErrorMessage }}</div>
{{ end }}
</div>
</main>
{{ end }}

View file

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

View file

@ -0,0 +1,47 @@
{{ 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,7 +1,6 @@
{{ 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>
<main role="main" class="container">
<h1>{{ .Title }}</h1>
<p>{{ .Explanation }}</p>
</main>
{{ end }}

View file

@ -1,7 +1,6 @@
{{ 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>
<main role="main" class="container">
<h1>{{ .Title }}</h1>
<p>{{ .Explanation }}</p>
</main>
{{ end }}

View file

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