Compare commits

...

10 Commits
0.2.0 ... main

Author SHA1 Message Date
Jan Dittberner be15b18259 Fix linter warnings 6 days ago
Jan Dittberner 9f53e37693 Remove copyright years from base template 6 days ago
Jan Dittberner c4724723b6 Switch from logrus to log/slog
Use log/slog from the Go standard library to reduce dependencies.
6 days ago
Jan Dittberner 291c1857c6 Update dependencies 6 days ago
Jan Dittberner c607cdae94 Update linter, remove copyright years
Git has all history information, copyright years don't provide useful additional information.
6 days ago
Jan Dittberner ae86e52d40 Release 0.3.0 10 months ago
Jan Dittberner 815c8e792a Use oauth2.AuthCodeURL to simplify Authenticate 10 months ago
Jan Dittberner 7ec9e393e0 Add separate protected resource page
This commit adds a separate protected resource page to demonstrate how
to selectively require logins.

Add code to improve client performance by providing modification timestamps
and Cache-Control headers for embedded static files.
10 months ago
Jan Dittberner 0a4cc75bd3 HTML improvements
- link from logo to start page
- move common page header to templates/base.gohtml
10 months ago
Jan Dittberner 9ad06a2935 Improve token handling
- add identity information to the index page
- let the session expire when the token expires
10 months ago

1
.gitignore vendored

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

@ -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

@ -18,13 +18,12 @@ go.sum: go.mod
go mod tidy -v
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

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

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

@ -1,27 +1,25 @@
module code.cacert.org/cacert/oidc-demo-app
go 1.19
go 1.22
require (
github.com/BurntSushi/toml v1.3.2
github.com/gorilla/sessions v1.2.1
github.com/gorilla/sessions v1.2.2
github.com/knadh/koanf v1.5.0
github.com/lestrrat-go/jwx v1.2.26
github.com/nicksnyder/go-i18n/v2 v2.2.1
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/spf13/pflag v1.0.5
golang.org/x/oauth2 v0.10.0
golang.org/x/text v0.11.0
golang.org/x/oauth2 v0.20.0
golang.org/x/text v0.15.0
)
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // 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/golang/protobuf v1.5.3 // indirect
github.com/gorilla/securecookie v1.1.1 // 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
@ -30,9 +28,6 @@ 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.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
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/sys v0.20.0 // indirect
)

@ -1,7 +1,6 @@
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=
@ -39,8 +38,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/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.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=
@ -84,8 +84,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.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=
@ -98,12 +96,15 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.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.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/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=
@ -165,14 +166,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/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=
@ -209,8 +210,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/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/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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@ -251,14 +252,13 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.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,8 +267,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.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=
@ -285,9 +286,10 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-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.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=
@ -306,7 +308,6 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-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=
@ -320,13 +321,12 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.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.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
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.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
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=
@ -370,17 +370,19 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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=
@ -388,11 +390,11 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.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.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=
@ -413,8 +415,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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,8 +440,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.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=

@ -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,21 +18,18 @@ 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 *logrus.Logger
logger *slog.Logger
}
func (h *AfterLogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
session, err := services.GetSessionStore().Get(r, sessionName)
session, err := GetSession(r)
if err != nil {
h.logger.WithError(err).Error("could not get session")
h.logger.Error("could not get session", "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -41,13 +38,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.WithError(err).Error("could not save session")
h.logger.Error("could not save session", "error", err)
}
w.Header().Set("Location", "/")
w.WriteHeader(http.StatusFound)
}
func NewAfterLogoutHandler(logger *logrus.Logger) *AfterLogoutHandler {
func NewAfterLogoutHandler(logger *slog.Logger) *AfterLogoutHandler {
return &AfterLogoutHandler{logger: logger}
}

@ -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,107 +18,94 @@ limitations under the License.
package handlers
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/jwt/openid"
log "github.com/sirupsen/logrus"
"github.com/gorilla/sessions"
"github.com/nicksnyder/go-i18n/v2/i18n"
"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 *log.Logger, oauth2Config *oauth2.Config, clientID string) func(http.Handler) http.Handler {
func Authenticate(logger *slog.Logger, oauth2Config *oauth2.Config) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := services.GetSessionStore().Get(r, sessionName)
session, err := GetSession(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.ErrorContext(r.Context(), "failed to get session", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if _, ok := session.Values[sessionKeyIDToken]; ok {
if _, ok := session.Values[services.SessionIDToken]; ok {
next.ServeHTTP(w, r)
return
}
session.Values[sessionRedirectTarget] = r.URL.String()
session.Values[services.SessionRedirectTarget] = r.URL.String()
if err = session.Save(r, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.ErrorContext(r.Context(), "failed to save session", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
var authURL *url.URL
if authURL, err = url.Parse(oauth2Config.Endpoint.AuthURL); err != nil {
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)
}
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()
authURL := oauth2Config.AuthCodeURL(base64.URLEncoding.EncodeToString(state))
w.Header().Set("Location", authURL.String())
w.Header().Set("Location", authURL)
w.WriteHeader(http.StatusFound)
})
}
}
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
target := make([]byte, 0)
buf := bytes.NewBuffer(target)
enc := json.NewEncoder(buf)
func GetSession(r *http.Request) (*sessions.Session, error) {
session, err := services.GetSessionStore().Get(r, sessionName)
if err := enc.Encode(claims); err != nil {
logger.WithError(err).Warn("could not encode claims request parameter")
if err != nil {
return nil, fmt.Errorf("could not get session")
}
return buf.String()
return session, nil
}
func ParseIDToken(token string, keySet jwk.Set) (openid.Token, error) {
var (
parsedIDToken jwt.Token
err error
)
type I18NHandler interface {
GetBundle() *i18n.Bundle
GetCatalog() *services.MessageCatalog
}
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)
}
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
if v, ok := parsedIDToken.(openid.Token); ok {
return v, nil
data := map[string]interface{}{
"WelcomeNavLabel": msg("IndexNavLabel", nil, localizer),
"ProtectedNavLabel": msg("ProtectedNavLabel", nil, localizer),
}
return nil, errors.New("ID token is no OpenID Connect Identity Token")
return data
}

@ -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,11 +21,12 @@ 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-demo-app/ui"
"code.cacert.org/cacert/oidc-demo-app/internal/services"
)
@ -46,26 +47,34 @@ type ErrorDetails struct {
type ErrorBucket struct {
errorDetails *ErrorDetails
templates *template.Template
logger *log.Logger
logger *slog.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 {
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(b.bundle, accept)
err := b.templates.Lookup("base").Execute(w, map[string]interface{}{
"Title": b.messageCatalog.LookupMessage(
"ErrorTitle",
nil,
localizer,
),
"details": b.errorDetails,
})
localizer := GetLocalizer(b, r)
data := BaseTemplateData(b, localizer)
data["Title"] = b.messageCatalog.LookupMessage(
"ErrorTitle",
nil,
localizer,
)
data["details"] = b.errorDetails
err := b.templates.Lookup("base").Execute(w, data)
if err != nil {
log.WithError(err).Error("error rendering error template")
b.logger.Error("error rendering error template", "error", err)
http.Error(
w,
http.StatusText(http.StatusInternalServerError),
@ -134,13 +143,12 @@ func (w *errorResponseWriter) Write(content []byte) (int, error) {
}
func ErrorHandling(
logger *log.Logger,
templateFS fs.FS,
logger *slog.Logger,
bundle *i18n.Bundle,
messageCatalog *services.MessageCatalog,
) (func(http.Handler) http.Handler, error) {
errorTemplates, err := template.ParseFS(
templateFS,
ui.Templates,
"templates/base.gohtml",
"templates/errors.gohtml",
)

@ -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,117 +20,136 @@ package handlers
import (
"fmt"
"html/template"
"io/fs"
"log/slog"
"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
keySet jwk.Set
logger *slog.Logger
logoutURL string
messageCatalog *services.MessageCatalog
publicURL string
tokenInfo *services.TokenInfoService
}
func (h *IndexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodGet {
http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
func (h *IndexHandler) GetBundle() *i18n.Bundle {
return h.bundle
}
return
}
func (h *IndexHandler) GetCatalog() *services.MessageCatalog {
return h.messageCatalog
}
if request.URL.Path != "/" {
http.NotFound(writer, request)
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
}
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)
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
localizer := GetLocalizer(h, r)
logoutURL, err := url.Parse(h.logoutURL)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var (
idToken string
ok bool
)
session, err := GetSession(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
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)
tokenInfo, err := h.tokenInfo.GetTokenInfo(session)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
h.logger.Error("failed to get token info for request", "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writer.Header().Add("Content-Type", "text/html")
if !tokenInfo.Expires.IsZero() {
h.logger.Info("id token expires at", "expires", tokenInfo.Expires)
}
w.Header().Add("Content-Type", "text/html")
msgLookup := h.messageCatalog.LookupMessage
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(),
})
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)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
http.Error(w, 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(
templateFS,
ui.Templates,
"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

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

@ -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"
"net/http"
"sync/atomic"
log "github.com/sirupsen/logrus"
)
type key int
@ -64,7 +63,7 @@ func Logging(logger *log.Logger) func(http.Handler) http.Handler {
requestID = "unknown"
}
logger.Infof(
logger.Printf(
"[%s] %s \"%s %s\" %d %d \"%s\"",
requestID,
r.RemoteAddr,
@ -97,7 +96,7 @@ func Tracing(nextRequestID func() string) func(http.Handler) http.Handler {
var Healthy int32
func NewHealthHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if atomic.LoadInt32(&Healthy) == 1 {
w.WriteHeader(http.StatusNoContent)

@ -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,26 +19,20 @@ 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 *log.Logger
logger *slog.Logger
oauth2Config *oauth2.Config
}
@ -65,22 +59,22 @@ func (c *OidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
tok, err := c.oauth2Config.Exchange(r.Context(), code)
if err != nil {
c.logger.WithError(err).Error("could not perform token exchange")
c.logger.Error("could not perform token exchange", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session, err := services.GetSessionStore().Get(r, "resource_session")
session, err := GetSession(r)
if err != nil {
c.logger.WithError(err).Error("could not get session store")
c.logger.Error("could not get session", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if err = c.storeTokens(session, tok); err != nil {
c.logger.WithError(err).Error("could not store token in session")
c.logger.Error("could not store token in session", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -88,14 +82,14 @@ func (c *OidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
}
if err = session.Save(r, w); err != nil {
c.logger.WithError(err).Error("could not save session")
c.logger.Error("could not save session", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if redirectTarget, ok := session.Values[sessionRedirectTarget]; ok {
if redirectTarget, ok := session.Values[services.SessionRedirectTarget]; ok {
if v, ok := redirectTarget.(string); ok {
w.Header().Set("Location", v)
w.WriteHeader(http.StatusFound)
@ -130,8 +124,8 @@ func (c *OidcCallbackHandler) storeTokens(
session *sessions.Session,
tok *oauth2.Token,
) error {
session.Values[sessionKeyAccessToken] = tok.AccessToken
session.Values[sessionKeyRefreshToken] = tok.RefreshToken
session.Values[services.SessionAccessToken] = tok.AccessToken
session.Values[services.SessionRefreshToken] = tok.RefreshToken
idTokenValue := tok.Extra("id_token")
@ -140,26 +134,29 @@ func (c *OidcCallbackHandler) storeTokens(
return fmt.Errorf("ID token value %v is not a string", idTokenValue)
}
session.Values[sessionKeyIDToken] = idToken
session.Values[services.SessionIDToken] = idToken
session.Options.MaxAge = int(time.Until(tok.Expiry).Seconds())
oidcToken, err := ParseIDToken(idToken, c.keySet)
oidcToken, err := services.ParseIDToken(idToken, c.keySet)
if err != nil {
return fmt.Errorf("could not parse ID token: %w", err)
}
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")
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(),
)
return nil
}
func NewCallbackHandler(logger *log.Logger, keySet jwk.Set, oauth2Config *oauth2.Config) *OidcCallbackHandler {
func NewCallbackHandler(logger *slog.Logger, keySet jwk.Set, oauth2Config *oauth2.Config) *OidcCallbackHandler {
return &OidcCallbackHandler{
keySet: keySet,
logger: logger,

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

@ -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");

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

@ -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");

@ -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"
"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,13 +51,12 @@ 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() {
logger.Info(f.FlagUsages())
log.Print(f.FlagUsages())
os.Exit(0)
}
@ -70,7 +69,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 +77,18 @@ 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")
return nil, fmt.Errorf("could not load configuration from file %s: %w", defaultFile, 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")
return nil, fmt.Errorf("error loading configuration from file %s: %w", c, 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 +96,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 variables: %w", err)
}
return config, nil

@ -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,6 +20,7 @@ package services
import (
"errors"
"fmt"
"log/slog"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
@ -27,19 +28,32 @@ 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["IndexGreeting"] = &i18n.Message{
ID: "IndexGreeting",
Other: "Hello {{ .User }}",
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["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",
@ -47,7 +61,21 @@ func AddMessages(catalog *MessageCatalog) {
}
messages["IndexIntroductionText"] = &i18n.Message{
ID: "IndexIntroductionText",
Other: "This is an authorization protected resource",
Other: "This is a public resource.",
}
messages["IndexNavLabel"] = &i18n.Message{
ID: "IndexNavLabel",
Description: "Label for the index page in the top navigation",
Other: "Welcome",
}
messages["ProtectedIntroductionText"] = &i18n.Message{
ID: "ProtectedIntroductionText",
Other: "This is an authorization protected resource.",
}
messages["ProtectedNavLabel"] = &i18n.Message{
ID: "ProtectedNavLabel",
Description: "Label for the protected resource page in the top navigation",
Other: "Protected area",
}
catalog.AddMessages(messages)
@ -55,7 +83,7 @@ func AddMessages(catalog *MessageCatalog) {
type MessageCatalog struct {
messages map[string]*i18n.Message
logger *log.Logger
logger *slog.Logger
}
func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) {
@ -74,11 +102,11 @@ func (m *MessageCatalog) LookupErrorMessage(
message, ok := m.messages[fieldTag]
if !ok {
m.logger.WithField("field_tag", fieldTag).Info("no specific error message for field and tag")
m.logger.Info("no specific error message for field and tag", "field_tag", fieldTag)
message, ok = m.messages[tag]
if !ok {
m.logger.WithField("tag", tag).Info("no specific error message for tag")
m.logger.Info("no specific error message for tag", "tag", tag)
message, ok = m.messages["unknown"]
if !ok {
@ -96,7 +124,7 @@ func (m *MessageCatalog) LookupErrorMessage(
},
})
if err != nil {
m.logger.WithError(err).Error("localization failed")
m.logger.Error("localization failed", "error", err)
return tag
}
@ -121,7 +149,7 @@ func (m *MessageCatalog) LookupMessage(
return translation
}
m.logger.WithField("id", id).Warn("no translation found for id")
m.logger.Warn("no translation found for id", "id", id)
return id
}
@ -130,19 +158,19 @@ func (m *MessageCatalog) handleLocalizeError(id string, translation string, err
var messageNotFound *i18n.MessageNotFoundErr
if errors.As(err, &messageNotFound) {
m.logger.WithError(err).WithField("message", id).Warn("message not found")
m.logger.Warn("message not found", "error", err, "message", id)
if translation != "" {
return translation
}
} else {
m.logger.WithError(err).WithField("message", id).Error("translation error")
m.logger.Error("translation error", "error", err, "message", id)
}
return id
}
func InitI18n(logger *log.Logger, languages []string) (*i18n.Bundle, *MessageCatalog) {
func InitI18n(logger *slog.Logger, languages []string) (*i18n.Bundle, *MessageCatalog) {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
@ -151,7 +179,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
}
@ -164,7 +192,7 @@ func InitI18n(logger *log.Logger, languages []string) (*i18n.Bundle, *MessageCat
return bundle, 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",

@ -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,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,17 +57,19 @@ type OIDCInformation struct {
// retrieved by GetOAuth2Config.
//
// The JSON Web Key Set can be retrieved by GetJwkSet.
func DiscoverOIDC(logger *log.Logger, params *OidcParams) (*OIDCInformation, error) {
func DiscoverOIDC(logger *slog.Logger, params *OidcParams) (*OIDCInformation, error) {
discoveryURL, err := url.Parse(params.OidcServer)
if err != nil {
logger.WithError(err).WithField(
"oidc.server",
params.OidcServer,
).Fatal("could not parse parameter value")
} else {
discoveryURL.Path = "/.well-known/openid-configuration"
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)
}
discoveryURL.Path = "/.well-known/openid-configuration"
var (
body []byte
req *http.Request
@ -104,7 +106,7 @@ func DiscoverOIDC(logger *log.Logger, params *OidcParams) (*OIDCInformation, err
AuthURL: discoveryResponse.AuthorizationEndpoint,
TokenURL: discoveryResponse.TokenEndpoint,
},
Scopes: []string{"openid", "offline"},
Scopes: []string{"openid", "email", "profile"},
}
const jwkFetchTimeout = 10 * time.Second

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

@ -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,24 +18,33 @@ limitations under the License.
package services
import (
"fmt"
"os"
"github.com/gorilla/sessions"
log "github.com/sirupsen/logrus"
)
var store *sessions.FilesystemStore
func InitSessionStore(logger *log.Logger, sessionPath string, keys ...[]byte) {
const (
SessionAccessToken = iota
SessionRefreshToken
SessionIDToken
SessionRedirectTarget
)
func InitSessionStore(sessionPath string, keys ...[]byte) error {
store = sessions.NewFilesystemStore(sessionPath, keys...)
if _, err := os.Stat(sessionPath); err != nil {
if os.IsNotExist(err) {
if err = os.MkdirAll(sessionPath, 0700); err != nil { //nolint:gomnd
logger.WithError(err).Fatal("could not create session store director")
if err = os.MkdirAll(sessionPath, 0700); err != nil { //nolint:mnd
return fmt.Errorf("could not create session store director: %w", err)
}
}
}
return nil
}
func GetSessionStore() *sessions.FilesystemStore {

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

@ -1,20 +1,47 @@
[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"
[IndexGreeting]
hash = "sha1-d4a13058e497fa24143ea96d50d82b818455ef61"
other = "Hallo {{ .User }}"
[GreetingAnonymous]
hash = "sha1-f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
other = "Hallo"
[GreetingAuthenticated]
hash = "sha1-22e08bfee49f285ac06df7d582c2a65bab86fa35"
other = "Hallo {{ .Name }}"
[IndexIntroductionText]
hash = "sha1-c2c530e263fc9c38482338ed290aafb496794179"
other = "Das ist eine zugriffsgeschützte Resource"
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"
[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"

@ -1,8 +1,23 @@
AuthenticatedAs = "The identity provider authenticated your identity as {{ .Name }} with the email address {{ .Email }}."
ErrorTitle = "An error has occurred"
IndexGreeting = "Hello {{ .User }}"
IndexIntroductionText = "This is an authorization protected resource"
GreetingAnonymous = "Hello"
GreetingAuthenticated = "Hello {{ .Name }}"
IndexIntroductionText = "This is a public 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"

@ -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");

@ -27,12 +27,21 @@
<title>{{ .Title }}</title>
</head>
<body class="resource-app d-flex flex-column h-100">
<main role="main" class="flex-shrink-0">
{{ template "content" . }}
</main>
<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>
{{ template "content" . }}
<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>

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

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

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

@ -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");

Loading…
Cancel
Save