feat(authn): add templatized error page
Cadoles/bouncer/pipeline/head Something is wrong with the build of this commit Details

ref CNOUS/mse#3907
This commit is contained in:
wpetit 2024-06-05 15:46:59 +02:00
parent c7ac331b10
commit d5fed4c2ac
10 changed files with 187 additions and 23 deletions

View File

@ -31,6 +31,7 @@ type QueueLayerConfig struct {
} }
type AuthnLayerConfig struct { type AuthnLayerConfig struct {
Debug InterpolatedBool `yaml:"debug"`
TemplateDir InterpolatedString `yaml:"templateDir"` TemplateDir InterpolatedString `yaml:"templateDir"`
OIDC AuthnOIDCLayerConfig `yaml:"oidc"` OIDC AuthnOIDCLayerConfig `yaml:"oidc"`
} }

View File

@ -17,6 +17,7 @@ import (
type Layer struct { type Layer struct {
layerType store.LayerType layerType store.LayerType
auth Authenticator auth Authenticator
debug bool
templateDir string templateDir string
} }
@ -40,8 +41,9 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
logger.Error(ctx, "could not execute pre-auth hook", logger.E(errors.WithStack(err))) err = errors.WithStack(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) logger.Error(ctx, "could not execute pre-auth hook", logger.E(err))
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -65,8 +67,9 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
logger.Error(ctx, "could not authenticate user", logger.E(errors.WithStack(err))) err = errors.WithStack(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) logger.Error(ctx, "could not authenticate user", logger.E(err))
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -77,8 +80,9 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
logger.Error(ctx, "could not apply rules", logger.E(errors.WithStack(err))) err = errors.WithStack(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) logger.Error(ctx, "could not apply rules", logger.E(err))
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -94,8 +98,9 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
logger.Error(ctx, "could not execute post-auth hook", logger.E(errors.WithStack(err))) err = errors.WithStack(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) logger.Error(ctx, "could not execute post-auth hook", logger.E(err))
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -108,12 +113,47 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
} }
} }
func (l *Layer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, user *User) { type baseTemplateData struct {
w.WriteHeader(http.StatusForbidden) Layer *store.Layer
l.renderPage(w, r, layer, "forbidden", options.Templates.Forbidden.Block, user) Debug bool
Request *http.Request
} }
func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, page string, block string, user *User) { func (l *Layer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, user *User) {
templateData := struct {
baseTemplateData
User *User
}{
baseTemplateData: baseTemplateData{
Layer: layer,
Debug: l.debug,
Request: r,
},
User: user,
}
w.WriteHeader(http.StatusForbidden)
l.renderPage(w, r, "forbidden", options.Templates.Forbidden.Block, templateData)
}
func (l *Layer) renderErrorPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, err error) {
templateData := struct {
baseTemplateData
Err error
}{
baseTemplateData: baseTemplateData{
Layer: layer,
Debug: l.debug,
Request: r,
},
Err: err,
}
w.WriteHeader(http.StatusInternalServerError)
l.renderPage(w, r, "error", options.Templates.Error.Block, templateData)
}
func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, page string, block string, templateData any) {
ctx := r.Context() ctx := r.Context()
pattern := filepath.Join(l.templateDir, page+".gohtml") pattern := filepath.Join(l.templateDir, page+".gohtml")
@ -128,18 +168,8 @@ func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, layer *store.
return return
} }
templateData := struct {
Layer *store.Layer
User *User
}{
Layer: layer,
User: user,
}
w.Header().Add("Cache-Control", "no-cache") w.Header().Add("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
if err := tmpl.ExecuteTemplate(w, block, templateData); err != nil { if err := tmpl.ExecuteTemplate(w, block, templateData); err != nil {
logger.Error(ctx, "could not render authn page", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not render authn page", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -160,6 +190,7 @@ func NewLayer(layerType store.LayerType, auth Authenticator, funcs ...OptionFunc
layerType: layerType, layerType: layerType,
auth: auth, auth: auth,
templateDir: opts.TemplateDir, templateDir: opts.TemplateDir,
debug: opts.Debug,
} }
} }

View File

@ -17,6 +17,7 @@ type LayerOptions struct {
type TemplatesOptions struct { type TemplatesOptions struct {
Forbidden TemplateOptions `mapstructure:"forbidden"` Forbidden TemplateOptions `mapstructure:"forbidden"`
Error TemplateOptions `mapstructure:"error"`
} }
type TemplateOptions struct { type TemplateOptions struct {
@ -43,6 +44,9 @@ func DefaultLayerOptions() LayerOptions {
Forbidden: TemplateOptions{ Forbidden: TemplateOptions{
Block: "default", Block: "default",
}, },
Error: TemplateOptions{
Block: "default",
},
}, },
} }

View File

@ -14,5 +14,5 @@ func NewLayer(store sessions.Store, funcs ...OptionFunc) *authn.Layer {
httpTransport: opts.HTTPTransport, httpTransport: opts.HTTPTransport,
httpClientTimeout: opts.HTTPClientTimeout, httpClientTimeout: opts.HTTPClientTimeout,
store: store, store: store,
}) }, opts.AuthnOptions...)
} }

View File

@ -3,11 +3,14 @@ package oidc
import ( import (
"net/http" "net/http"
"time" "time"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
) )
type Options struct { type Options struct {
HTTPTransport *http.Transport HTTPTransport *http.Transport
HTTPClientTimeout time.Duration HTTPClientTimeout time.Duration
AuthnOptions []authn.OptionFunc
} }
type OptionFunc func(opts *Options) type OptionFunc func(opts *Options)
@ -24,10 +27,17 @@ func WithHTTPClientTimeout(timeout time.Duration) OptionFunc {
} }
} }
func WithAuthnOptions(funcs ...authn.OptionFunc) OptionFunc {
return func(opts *Options) {
opts.AuthnOptions = funcs
}
}
func NewOptions(funcs ...OptionFunc) *Options { func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{ opts := &Options{
HTTPTransport: http.DefaultTransport.(*http.Transport), HTTPTransport: http.DefaultTransport.(*http.Transport),
HTTPClientTimeout: 30 * time.Second, HTTPClientTimeout: 30 * time.Second,
AuthnOptions: make([]authn.OptionFunc, 0),
} }
for _, fn := range funcs { for _, fn := range funcs {

View File

@ -2,6 +2,7 @@ package authn
type Options struct { type Options struct {
TemplateDir string TemplateDir string
Debug bool
} }
type OptionFunc func(*Options) type OptionFunc func(*Options)
@ -9,6 +10,7 @@ type OptionFunc func(*Options)
func NewOptions(funcs ...OptionFunc) *Options { func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{ opts := &Options{
TemplateDir: "./templates", TemplateDir: "./templates",
Debug: false,
} }
for _, fn := range funcs { for _, fn := range funcs {
@ -23,3 +25,9 @@ func WithTemplateDir(templateDir string) OptionFunc {
o.TemplateDir = templateDir o.TemplateDir = templateDir
} }
} }
func WithDebug(debug bool) OptionFunc {
return func(o *Options) {
o.Debug = debug
}
}

View File

@ -21,6 +21,7 @@ func init() {
func setupAuthnBasicLayer(conf *config.Config) (director.Layer, error) { func setupAuthnBasicLayer(conf *config.Config) (director.Layer, error) {
options := []authn.OptionFunc{ options := []authn.OptionFunc{
authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)), authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)),
authn.WithDebug(bool(conf.Layers.Authn.Debug)),
} }
return basic.NewLayer(options...), nil return basic.NewLayer(options...), nil

View File

@ -21,6 +21,7 @@ func init() {
func setupAuthnNetworkLayer(conf *config.Config) (director.Layer, error) { func setupAuthnNetworkLayer(conf *config.Config) (director.Layer, error) {
options := []authn.OptionFunc{ options := []authn.OptionFunc{
authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)), authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)),
authn.WithDebug(bool(conf.Layers.Authn.Debug)),
} }
return network.NewLayer(options...), nil return network.NewLayer(options...), nil

View File

@ -33,5 +33,9 @@ func setupAuthnOIDCLayer(conf *config.Config) (director.Layer, error) {
store, store,
oidc.WithHTTPTransport(transport), oidc.WithHTTPTransport(transport),
oidc.WithHTTPClientTimeout(time.Duration(*conf.Layers.Authn.OIDC.HTTPClient.Timeout)), oidc.WithHTTPClientTimeout(time.Duration(*conf.Layers.Authn.OIDC.HTTPClient.Timeout)),
oidc.WithAuthnOptions(
authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)),
authn.WithDebug(bool(conf.Layers.Authn.Debug)),
),
), nil ), nil
} }

View File

@ -0,0 +1,104 @@
{{ define "default" }}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Erreur - {{ .Layer.Name }}</title>
<style>
html {
box-sizing: border-box;
font-size: 16px;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ol,
ul {
margin: 0;
padding: 0;
font-weight: normal;
}
html,
body {
width: 100%;
height: 100%;
font-family: Arial, Helvetica, sans-serif;
background-color: #ffe4e4;
}
#container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
}
#card {
padding: 1.5em 1em;
border: 1px solid #d90202;
background-color: #feb0b0;
border-radius: 5px;
box-shadow: 2px 2px #cccccc1c;
color: #810000 !important;
}
.title {
margin-bottom: 1.2em;
}
p {
margin-bottom: 0.5em;
}
.footer {
font-size: 0.7em;
margin-top: 2em;
text-align: right;
}
.back-link {
text-align: center;
}
</style>
</head>
<body>
<div id="container">
<div id="card">
<h2 class="title">Une erreur est survenue !</h2>
{{ if .Debug }}
<pre>{{ .Err }}</pre>
{{ end }}
{{/* if a public base url is provided, show navigation link */}}
{{ $oidc := ( index .Layer.Options "oidc" ) }}
{{ $publicBaseURL := ( index $oidc "publicBaseURL" ) }}
{{ if $publicBaseURL }}
<div class="back-link">
<a href="{{ $publicBaseURL }}" title="Retourner sur l'application"
>Retourner sur l'application</a
>
</div>
{{ end }}
<p class="footer">
Propulsé par
<a href="https://forge.cadoles.com/Cadoles/bouncer">Bouncer</a>.
</p>
</div>
</div>
</body>
</html>
{{ end }}