Compare commits

..

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

32 changed files with 382 additions and 909 deletions

1
.gitignore vendored
View file

@ -5,7 +5,6 @@
/resource_app*.toml
/sessions
/static
/translations/translate.*.toml
/ui/css/
/ui/images/
/ui/js/

View file

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

View file

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

View file

@ -5,16 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.0] - 2023-08-07
### Changed
- let the session expire when the token expires
- link from logo to start page
- move common page header to templates/base.gohtml
- implement caching for static resources
### Added
- add identity output to index page
- add a separate protected resource page
## [0.2.0]
### Changed
- re-order configuration precedence

View file

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

29
go.mod
View file

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

74
go.sum
View file

@ -1,6 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.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/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -38,9 +39,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/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/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=
@ -51,8 +51,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -84,6 +84,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -96,15 +98,12 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/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/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
@ -166,14 +165,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ=
github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8=
github.com/lestrrat-go/jwx v1.2.26 h1:4iFo8FPRZGDYe1t19mQP0zTRqA7n8HnJ5lkIiDvJcB0=
github.com/lestrrat-go/jwx v1.2.26/go.mod h1:MaiCdGbn3/cckbOFSCluJlJMmp9dmZm5hDuIkx8ftpQ=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
@ -210,8 +209,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@ -252,13 +251,14 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.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/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=
@ -267,9 +267,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
@ -286,10 +285,9 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.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/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=
@ -308,6 +306,7 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -321,12 +320,13 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.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/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-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/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -370,19 +370,17 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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.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/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
@ -390,11 +388,11 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.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.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.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/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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=
@ -415,6 +413,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
@ -440,6 +440,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/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=

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,21 +27,12 @@
<title>{{ .Title }}</title>
</head>
<body class="resource-app 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>
<ul class="nav">
<li class="nav-item">
<a class="nav-link" href="/">{{ .WelcomeNavLabel }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/protected">{{ .ProtectedNavLabel }}</a>
</li>
</ul>
</header>
<main role="main" class="flex-shrink-0">
{{ template "content" . }}
</main>
<footer class="footer mt-auto py-3">
<div class="container">
<span class="text-muted small">© <a href="https://www.cacert.org/">CAcert</a></span>
<span class="text-muted small">© 2020-2023 <a href="https://www.cacert.org/">CAcert</a></span>
</div>
</footer>
<script type="text/javascript" src="/js/cacert.bundle.js"></script>

View file

@ -1,5 +1,6 @@
{{ define "content" }}
<main class="container error-message">
<div class="container text-center error-message">
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
<h1>{{ .Title }}</h1>
<h2>{{ if .details.ErrorCode }}
<strong>{{ .details.ErrorCode }}</strong> {{ end }}{{ .details.ErrorMessage }}</h2>
@ -8,5 +9,5 @@
<p>{{ . }}</p>
{{ end }}
{{ end }}
</main>
</div>
{{ end }}

View file

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

View file

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

View file

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