git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #1043 from oauth2-proxy/sign-in-page

Refactor Sign In Page rendering and capture all page rendering code in pagewriter package

commitce29b16d8464376809b5aec99121cc69de0bf3e1 authorJoel Speed <Joel.speed@hotmail.co.uk> dateSun 14 Feb 2021, 10:26 (6 years ago) parent1e3d854425cff7
11 files changed+467−159
CHANGELOG.md+1 oauthproxy.go+72−103 pkg/app/error_page.go → pkg/app/pagewriter/error_page.go+24−24 pkg/app/error_page_test.go → pkg/app/pagewriter/error_page_test.go+16−16 pkg/app/pagewriter/pagewriter.go+85 pkg/app/app_suite_test.go → pkg/app/pagewriter/pagewriter_suite_test.go+1−1 pkg/app/pagewriter/pagewriter_test.go+126 pkg/app/pagewriter/sign_in_page.go+66 pkg/app/pagewriter/sign_in_page_test.go+61 pkg/app/templates.go → pkg/app/pagewriter/templates.go+3−3 pkg/app/templates_test.go → pkg/app/pagewriter/templates_test.go+12−12
CHANGELOG.md+1
@@ -8,6 +8,7 @@
88
99## Changes since v7.0.1
1010
11+- [#1043](https://github.com/oauth2-proxy/oauth2-proxy/pull/1043) Refactor Sign In Page rendering and capture all page rendering code in pagewriter package (@JoelSpeed)
1112- [#1029](https://github.com/oauth2-proxy/oauth2-proxy/pull/1029) Refactor error page rendering and allow debug messages on error (@JoelSpeed)
1213- [#1028](https://github.com/oauth2-proxy/oauth2-proxy/pull/1028) Refactor templates, update theme and provide styled error pages (@JoelSpeed)
1314- [#1039](https://github.com/oauth2-proxy/oauth2-proxy/pull/1039) Ensure errors in tests are logged to the GinkgoWriter (@JoelSpeed)
oauthproxy.go+72−103
@@ -5,7 +5,6 @@ import (
55 "encoding/json"
66 "errors"
77 "fmt"
8− "html/template"
98 "net"
109 "net/http"
1110 "net/url"
@@ -18,7 +17,7 @@ import (
1817 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
1918 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
2019 sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
21− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app"
20+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
2221 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
2322 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
2423 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
@@ -76,39 +75,33 @@ type OAuthProxy struct {
7675 AuthOnlyPath string
7776 UserInfoPath string
7877
79− allowedRoutes []allowedRoute
80− redirectURL *url.URL // the url to receive requests at
81− whitelistDomains []string
82− provider providers.Provider
83− providerNameOverride string
84− sessionStore sessionsapi.SessionStore
85− ProxyPrefix string
86− SignInMessage string
87− basicAuthValidator basic.Validator
88− displayHtpasswdForm bool
89− serveMux http.Handler
90− SetXAuthRequest bool
91− PassBasicAuth bool
92− SetBasicAuth bool
93− SkipProviderButton bool
94− PassUserHeaders bool
95− BasicAuthPassword string
96− PassAccessToken bool
97− SetAuthorization bool
98− PassAuthorization bool
99− PreferEmailToUser bool
100− skipAuthPreflight bool
101− skipJwtBearerTokens bool
102− templates *template.Template
103− realClientIPParser ipapi.RealClientIPParser
104− trustedIPs *ip.NetSet
105− Banner string
106− Footer string
78+ allowedRoutes []allowedRoute
79+ redirectURL *url.URL // the url to receive requests at
80+ whitelistDomains []string
81+ provider providers.Provider
82+ sessionStore sessionsapi.SessionStore
83+ ProxyPrefix string
84+ basicAuthValidator basic.Validator
85+ serveMux http.Handler
86+ SetXAuthRequest bool
87+ PassBasicAuth bool
88+ SetBasicAuth bool
89+ SkipProviderButton bool
90+ PassUserHeaders bool
91+ BasicAuthPassword string
92+ PassAccessToken bool
93+ SetAuthorization bool
94+ PassAuthorization bool
95+ PreferEmailToUser bool
96+ skipAuthPreflight bool
97+ skipJwtBearerTokens bool
98+ realClientIPParser ipapi.RealClientIPParser
99+ trustedIPs *ip.NetSet
107100
108101 sessionChain alice.Chain
109102 headersChain alice.Chain
110103 preAuthChain alice.Chain
111− errorPage *app.ErrorPage
104+ pageWriter pagewriter.Writer
112105}
113106
114107// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
@@ -118,20 +111,31 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
118111 return nil, fmt.Errorf("error initialising session store: %v", err)
119112 }
120113
121− templates, err := app.LoadTemplates(opts.Templates.Path)
122− if err != nil {
123− return nil, fmt.Errorf("error loading templates: %v", err)
114+ var basicAuthValidator basic.Validator
115+ if opts.HtpasswdFile != "" {
116+ logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
117+ var err error
118+ basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
119+ if err != nil {
120+ return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
121+ }
124122 }
125123
126− errorPage := &app.ErrorPage{
127− Template: templates.Lookup("error.html"),
128− ProxyPrefix: opts.ProxyPrefix,
129− Footer: opts.Templates.Footer,
130− Version: VERSION,
131− Debug: opts.Templates.Debug,
124+ pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{
125+ TemplatesPath: opts.Templates.Path,
126+ ProxyPrefix: opts.ProxyPrefix,
127+ Footer: opts.Templates.Footer,
128+ Version: VERSION,
129+ Debug: opts.Templates.Debug,
130+ ProviderName: buildProviderName(opts.GetProvider(), opts.ProviderName),
131+ SignInMessage: buildSignInMessage(opts),
132+ DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
133+ })
134+ if err != nil {
135+ return nil, fmt.Errorf("error initialising page writer: %v", err)
132136 }
133137
134− upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), errorPage.ProxyErrorHandler)
138+ upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter.ProxyErrorHandler)
135139 if err != nil {
136140 return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
137141 }
@@ -164,16 +168,6 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
164168 }
165169 }
166170
167− var basicAuthValidator basic.Validator
168− if opts.HtpasswdFile != "" {
169− logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
170− var err error
171− basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
172− if err != nil {
173− return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
174− }
175− }
176−
177171 allowedRoutes, err := buildRoutesAllowlist(opts)
178172 if err != nil {
179173 return nil, err
@@ -210,30 +204,24 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
210204 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
211205 UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
212206
213− ProxyPrefix: opts.ProxyPrefix,
214− provider: opts.GetProvider(),
215− providerNameOverride: opts.ProviderName,
216− sessionStore: sessionStore,
217− serveMux: upstreamProxy,
218− redirectURL: redirectURL,
219− allowedRoutes: allowedRoutes,
220− whitelistDomains: opts.WhitelistDomains,
221− skipAuthPreflight: opts.SkipAuthPreflight,
222− skipJwtBearerTokens: opts.SkipJwtBearerTokens,
223− realClientIPParser: opts.GetRealClientIPParser(),
224− SkipProviderButton: opts.SkipProviderButton,
225− templates: templates,
226− trustedIPs: trustedIPs,
227− Banner: opts.Templates.Banner,
228− Footer: opts.Templates.Footer,
229− SignInMessage: buildSignInMessage(opts),
230−
231− basicAuthValidator: basicAuthValidator,
232− displayHtpasswdForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
233− sessionChain: sessionChain,
234− headersChain: headersChain,
235− preAuthChain: preAuthChain,
236− errorPage: errorPage,
207+ ProxyPrefix: opts.ProxyPrefix,
208+ provider: opts.GetProvider(),
209+ sessionStore: sessionStore,
210+ serveMux: upstreamProxy,
211+ redirectURL: redirectURL,
212+ allowedRoutes: allowedRoutes,
213+ whitelistDomains: opts.WhitelistDomains,
214+ skipAuthPreflight: opts.SkipAuthPreflight,
215+ skipJwtBearerTokens: opts.SkipJwtBearerTokens,
216+ realClientIPParser: opts.GetRealClientIPParser(),
217+ SkipProviderButton: opts.SkipProviderButton,
218+ trustedIPs: trustedIPs,
219+
220+ basicAuthValidator: basicAuthValidator,
221+ sessionChain: sessionChain,
222+ headersChain: headersChain,
223+ preAuthChain: preAuthChain,
224+ pageWriter: pageWriter,
237225 }, nil
238226}
239227
@@ -331,6 +319,13 @@ func buildSignInMessage(opts *options.Options) string {
331319 return msg
332320}
333321
322+func buildProviderName(p providers.Provider, override string) string {
323+ if override != "" {
324+ return override
325+ }
326+ return p.Data().ProviderName
327+}
328+
334329// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
335330// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
336331// (method=path support)
@@ -533,7 +528,7 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code i
533528 redirectURL = "/"
534529 }
535530
536− p.errorPage.Render(rw, code, redirectURL, appError, messages...)
531+ p.pageWriter.WriteErrorPage(rw, code, redirectURL, appError, messages...)
537532}
538533
539534// IsAllowedRequest is used to check if auth should be skipped for this request
@@ -594,33 +589,7 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code
594589 redirectURL = "/"
595590 }
596591
597− // We allow unescaped template.HTML since it is user configured options
598− /* #nosec G203 */
599− t := struct {
600− ProviderName string
601− SignInMessage template.HTML
602− CustomLogin bool
603− Redirect string
604− Version string
605− ProxyPrefix string
606− Footer template.HTML
607− }{
608− ProviderName: p.provider.Data().ProviderName,
609− SignInMessage: template.HTML(p.SignInMessage),
610− CustomLogin: p.displayHtpasswdForm,
611− Redirect: redirectURL,
612− Version: VERSION,
613− ProxyPrefix: p.ProxyPrefix,
614− Footer: template.HTML(p.Footer),
615− }
616− if p.providerNameOverride != "" {
617− t.ProviderName = p.providerNameOverride
618− }
619− err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
620− if err != nil {
621− logger.Printf("Error rendering sign_in.html template: %v", err)
622− p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
623− }
592+ p.pageWriter.WriteSignInPage(rw, redirectURL)
624593}
625594
626595// ManualSignIn handles basic auth logins to the proxy
pkg/app/error_page.go → pkg/app/pagewriter/error_page.gorenamed+24−24
@@ -1,4 +1,4 @@
1−package app
1+package pagewriter
22
33import (
44 "fmt"
@@ -17,30 +17,30 @@ var errorMessages = map[int]string{
1717 http.StatusUnauthorized: "You need to be logged in to access this resource.",
1818}
1919
20−// ErrorPage is used to render error pages.
21−type ErrorPage struct {
22− // Template is the error page HTML template.
23− Template *template.Template
20+// errorPageWriter is used to render error pages.
21+type errorPageWriter struct {
22+ // template is the error page HTML template.
23+ template *template.Template
2424
25− // ProxyPrefix is the prefix under which OAuth2 Proxy pages are served.
26− ProxyPrefix string
25+ // proxyPrefix is the prefix under which OAuth2 Proxy pages are served.
26+ proxyPrefix string
2727
28− // Footer is the footer to be displayed at the bottom of the page.
28+ // footer is the footer to be displayed at the bottom of the page.
2929 // If not set, a default footer will be used.
30− Footer string
30+ footer string
3131
32− // Version is the OAuth2 Proxy version to be used in the default footer.
33− Version string
32+ // version is the OAuth2 Proxy version to be used in the default footer.
33+ version string
3434
35− // Debug determines whether errors pages should be rendered with detailed
35+ // debug determines whether errors pages should be rendered with detailed
3636 // errors.
37− Debug bool
37+ debug bool
3838}
3939
40−// Render writes an error page to the given response writer.
40+// WriteErrorPage writes an error page to the given response writer.
4141// It uses the passed redirectURL to give users the option to go back to where
4242// they originally came from or try signing in again.
43−func (e *ErrorPage) Render(rw http.ResponseWriter, status int, redirectURL string, appError string, messages ...interface{}) {
43+func (e *errorPageWriter) WriteErrorPage(rw http.ResponseWriter, status int, redirectURL string, appError string, messages ...interface{}) {
4444 rw.WriteHeader(status)
4545
4646 // We allow unescaped template.HTML since it is user configured options
@@ -56,14 +56,14 @@ func (e *ErrorPage) Render(rw http.ResponseWriter, status int, redirectURL strin
5656 }{
5757 Title: http.StatusText(status),
5858 Message: e.getMessage(status, appError, messages...),
59− ProxyPrefix: e.ProxyPrefix,
59+ ProxyPrefix: e.proxyPrefix,
6060 StatusCode: status,
6161 Redirect: redirectURL,
62− Footer: template.HTML(e.Footer),
63− Version: e.Version,
62+ Footer: template.HTML(e.footer),
63+ Version: e.version,
6464 }
6565
66− if err := e.Template.Execute(rw, data); err != nil {
66+ if err := e.template.Execute(rw, data); err != nil {
6767 logger.Printf("Error rendering error template: %v", err)
6868 http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
6969 }
@@ -72,18 +72,18 @@ func (e *ErrorPage) Render(rw http.ResponseWriter, status int, redirectURL strin
7272// ProxyErrorHandler is used by the upstream ReverseProxy to render error pages
7373// when there are issues with upstream servers.
7474// It is expected to always render a bad gateway error.
75−func (e *ErrorPage) ProxyErrorHandler(rw http.ResponseWriter, req *http.Request, proxyErr error) {
75+func (e *errorPageWriter) ProxyErrorHandler(rw http.ResponseWriter, req *http.Request, proxyErr error) {
7676 logger.Errorf("Error proxying to upstream server: %v", proxyErr)
77− e.Render(rw, http.StatusBadGateway, "", proxyErr.Error(), "There was a problem connecting to the upstream server.")
77+ e.WriteErrorPage(rw, http.StatusBadGateway, "", proxyErr.Error(), "There was a problem connecting to the upstream server.")
7878}
7979
8080// getMessage creates the message for the template parameters.
81−// If the ErrorPage.Debug is enabled, the application error takes precedence.
81+// If the errorPagewriter.Debug is enabled, the application error takes precedence.
8282// Otherwise, any messages will be used.
8383// The first message is expected to be a format string.
8484// If no messages are supplied, a default error message will be used.
85−func (e *ErrorPage) getMessage(status int, appError string, messages ...interface{}) string {
86− if e.Debug {
85+func (e *errorPageWriter) getMessage(status int, appError string, messages ...interface{}) string {
86+ if e.debug {
8787 return appError
8888 }
8989 if len(messages) > 0 {
pkg/app/error_page_test.go → pkg/app/pagewriter/error_page_test.gorenamed+16−16
@@ -1,4 +1,4 @@
1−package app
1+package pagewriter
22
33import (
44 "errors"
@@ -10,25 +10,25 @@ import (
1010 . "github.com/onsi/gomega"
1111)
1212
13−var _ = Describe("Error Page", func() {
14− var errorPage *ErrorPage
13+var _ = Describe("Error Page Writer", func() {
14+ var errorPage *errorPageWriter
1515
1616 BeforeEach(func() {
1717 tmpl, err := template.New("").Parse("{{.Title}} {{.Message}} {{.ProxyPrefix}} {{.StatusCode}} {{.Redirect}} {{.Footer}} {{.Version}}")
1818 Expect(err).ToNot(HaveOccurred())
1919
20− errorPage = &ErrorPage{
21− Template: tmpl,
22− ProxyPrefix: "/prefix/",
23− Footer: "Custom Footer Text",
24− Version: "v0.0.0-test",
20+ errorPage = &errorPageWriter{
21+ template: tmpl,
22+ proxyPrefix: "/prefix/",
23+ footer: "Custom Footer Text",
24+ version: "v0.0.0-test",
2525 }
2626 })
2727
28− Context("Render", func() {
28+ Context("WriteErrorPage", func() {
2929 It("Writes the template to the response writer", func() {
3030 recorder := httptest.NewRecorder()
31− errorPage.Render(recorder, 403, "/redirect", "Access Denied")
31+ errorPage.WriteErrorPage(recorder, 403, "/redirect", "Access Denied")
3232
3333 body, err := ioutil.ReadAll(recorder.Result().Body)
3434 Expect(err).ToNot(HaveOccurred())
@@ -37,7 +37,7 @@ var _ = Describe("Error Page", func() {
3737
3838 It("With a different code, uses the stock message for the correct code", func() {
3939 recorder := httptest.NewRecorder()
40− errorPage.Render(recorder, 500, "/redirect", "Access Denied")
40+ errorPage.WriteErrorPage(recorder, 500, "/redirect", "Access Denied")
4141
4242 body, err := ioutil.ReadAll(recorder.Result().Body)
4343 Expect(err).ToNot(HaveOccurred())
@@ -46,7 +46,7 @@ var _ = Describe("Error Page", func() {
4646
4747 It("With a message override, uses the message", func() {
4848 recorder := httptest.NewRecorder()
49− errorPage.Render(recorder, 403, "/redirect", "Access Denied", "An extra message: %s", "with more context.")
49+ errorPage.WriteErrorPage(recorder, 403, "/redirect", "Access Denied", "An extra message: %s", "with more context.")
5050
5151 body, err := ioutil.ReadAll(recorder.Result().Body)
5252 Expect(err).ToNot(HaveOccurred())
@@ -71,14 +71,14 @@ var _ = Describe("Error Page", func() {
7171 tmpl, err := template.New("").Parse("{{.Message}}")
7272 Expect(err).ToNot(HaveOccurred())
7373
74− errorPage.Template = tmpl
75− errorPage.Debug = true
74+ errorPage.template = tmpl
75+ errorPage.debug = true
7676 })
7777
78− Context("Render", func() {
78+ Context("WriteErrorPage", func() {
7979 It("Writes the detailed error in place of the message", func() {
8080 recorder := httptest.NewRecorder()
81− errorPage.Render(recorder, 403, "/redirect", "Debug error")
81+ errorPage.WriteErrorPage(recorder, 403, "/redirect", "Debug error")
8282
8383 body, err := ioutil.ReadAll(recorder.Result().Body)
8484 Expect(err).ToNot(HaveOccurred())
pkg/app/pagewriter/pagewriter.gonew file+85
@@ -0,0 +1,85 @@
1+package pagewriter
2+
3+import (
4+ "fmt"
5+ "net/http"
6+)
7+
8+// Writer is an interface for rendering html templates for both sign-in and
9+// error pages.
10+// It can also be used to write errors for the http.ReverseProxy used in the
11+// upstream package.
12+type Writer interface {
13+ WriteSignInPage(rw http.ResponseWriter, redirectURL string)
14+ WriteErrorPage(rw http.ResponseWriter, status int, redirectURL string, appError string, messages ...interface{})
15+ ProxyErrorHandler(rw http.ResponseWriter, req *http.Request, proxyErr error)
16+}
17+
18+// pageWriter implements the Writer interface
19+type pageWriter struct {
20+ *errorPageWriter
21+ *signInPageWriter
22+}
23+
24+// Opts contains all options required to configure the template
25+// rendering within OAuth2 Proxy.
26+type Opts struct {
27+ // TemplatesPath is the path from which to load custom templates for the sign-in and error pages.
28+ TemplatesPath string
29+
30+ // ProxyPrefix is the prefix under which OAuth2 Proxy pages are served.
31+ ProxyPrefix string
32+
33+ // Footer is the footer to be displayed at the bottom of the page.
34+ // If not set, a default footer will be used.
35+ Footer string
36+
37+ // Version is the OAuth2 Proxy version to be used in the default footer.
38+ Version string
39+
40+ // Debug determines whether errors pages should be rendered with detailed
41+ // errors.
42+ Debug bool
43+
44+ // DisplayLoginForm determines whether or not the basic auth password form is displayed on the sign-in page.
45+ DisplayLoginForm bool
46+
47+ // ProviderName is the name of the provider that should be displayed on the login button.
48+ ProviderName string
49+
50+ // SignInMessage is the messge displayed above the login button.
51+ SignInMessage string
52+}
53+
54+// NewWriter constructs a Writer from the options given to allow
55+// rendering of sign-in and error pages.
56+func NewWriter(opts Opts) (Writer, error) {
57+ templates, err := loadTemplates(opts.TemplatesPath)
58+ if err != nil {
59+ return nil, fmt.Errorf("error loading templates: %v", err)
60+ }
61+
62+ errorPage := &errorPageWriter{
63+ template: templates.Lookup("error.html"),
64+ proxyPrefix: opts.ProxyPrefix,
65+ footer: opts.Footer,
66+ version: opts.Version,
67+ debug: opts.Debug,
68+ }
69+
70+ signInPage := &signInPageWriter{
71+ template: templates.Lookup("sign_in.html"),
72+ errorPageWriter: errorPage,
73+ proxyPrefix: opts.ProxyPrefix,
74+ providerName: opts.ProviderName,
75+ signInMessage: opts.SignInMessage,
76+ footer: opts.Footer,
77+ version: opts.Version,
78+ displayLoginForm: opts.DisplayLoginForm,
79+ }
80+
81+ return &pageWriter{
82+ errorPageWriter: errorPage,
83+ signInPageWriter: signInPage,
84+ }, nil
85+}
pkg/app/app_suite_test.go → pkg/app/pagewriter/pagewriter_suite_test.gorenamed+1−1
@@ -1,4 +1,4 @@
1−package app
1+package pagewriter
22
33import (
44 "testing"
pkg/app/pagewriter/pagewriter_test.gonew file+126
@@ -0,0 +1,126 @@
1+package pagewriter
2+
3+import (
4+ "io/ioutil"
5+ "net/http/httptest"
6+ "os"
7+ "path/filepath"
8+
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/gomega"
11+)
12+
13+var _ = Describe("Writer", func() {
14+ Context("NewWriter", func() {
15+ var writer Writer
16+ var opts Opts
17+
18+ BeforeEach(func() {
19+ opts = Opts{
20+ TemplatesPath: "",
21+ ProxyPrefix: "/prefix",
22+ Footer: "<Footer>",
23+ Version: "<Version>",
24+ Debug: false,
25+ DisplayLoginForm: false,
26+ ProviderName: "<ProviderName>",
27+ SignInMessage: "<SignInMessage>",
28+ }
29+ })
30+
31+ Context("With no custom templates", func() {
32+ BeforeEach(func() {
33+ var err error
34+ writer, err = NewWriter(opts)
35+ Expect(err).ToNot(HaveOccurred())
36+ })
37+
38+ It("Writes the default error template", func() {
39+ recorder := httptest.NewRecorder()
40+ writer.WriteErrorPage(recorder, 500, "/redirect", "Some debug error")
41+
42+ body, err := ioutil.ReadAll(recorder.Result().Body)
43+ Expect(err).ToNot(HaveOccurred())
44+ Expect(string(body)).To(HavePrefix("\n<!DOCTYPE html>"))
45+ })
46+
47+ It("Writes the default sign in template", func() {
48+ recorder := httptest.NewRecorder()
49+ writer.WriteSignInPage(recorder, "/redirect")
50+
51+ body, err := ioutil.ReadAll(recorder.Result().Body)
52+ Expect(err).ToNot(HaveOccurred())
53+ Expect(string(body)).To(HavePrefix("\n<!DOCTYPE html>"))
54+ })
55+ })
56+
57+ Context("With custom templates", func() {
58+ var customDir string
59+
60+ BeforeEach(func() {
61+ var err error
62+ customDir, err = ioutil.TempDir("", "oauth2-proxy-pagewriter-test")
63+ Expect(err).ToNot(HaveOccurred())
64+
65+ templateHTML := `Custom Template`
66+ signInFile := filepath.Join(customDir, signInTemplateName)
67+ Expect(ioutil.WriteFile(signInFile, []byte(templateHTML), 0600)).To(Succeed())
68+ errorFile := filepath.Join(customDir, errorTemplateName)
69+ Expect(ioutil.WriteFile(errorFile, []byte(templateHTML), 0600)).To(Succeed())
70+
71+ opts.TemplatesPath = customDir
72+
73+ writer, err = NewWriter(opts)
74+ Expect(err).ToNot(HaveOccurred())
75+ })
76+
77+ AfterEach(func() {
78+ Expect(os.RemoveAll(customDir)).To(Succeed())
79+ })
80+
81+ It("Writes the custom error template", func() {
82+ recorder := httptest.NewRecorder()
83+ writer.WriteErrorPage(recorder, 500, "/redirect", "Some debug error")
84+
85+ body, err := ioutil.ReadAll(recorder.Result().Body)
86+ Expect(err).ToNot(HaveOccurred())
87+ Expect(string(body)).To(Equal("Custom Template"))
88+ })
89+
90+ It("Writes the custom sign in template", func() {
91+ recorder := httptest.NewRecorder()
92+ writer.WriteSignInPage(recorder, "/redirect")
93+
94+ body, err := ioutil.ReadAll(recorder.Result().Body)
95+ Expect(err).ToNot(HaveOccurred())
96+ Expect(string(body)).To(Equal("Custom Template"))
97+ })
98+ })
99+
100+ Context("With an invalid custom template", func() {
101+ var customDir string
102+
103+ BeforeEach(func() {
104+ var err error
105+ customDir, err = ioutil.TempDir("", "oauth2-proxy-pagewriter-test")
106+ Expect(err).ToNot(HaveOccurred())
107+
108+ templateHTML := `{{ Custom Broken Template`
109+ signInFile := filepath.Join(customDir, signInTemplateName)
110+ Expect(ioutil.WriteFile(signInFile, []byte(templateHTML), 0600)).To(Succeed())
111+
112+ opts.TemplatesPath = customDir
113+ })
114+
115+ AfterEach(func() {
116+ Expect(os.RemoveAll(customDir)).To(Succeed())
117+ })
118+
119+ It("Should return an error", func() {
120+ writer, err := NewWriter(opts)
121+ Expect(err).To(MatchError(ContainSubstring("template: sign_in.html:1: function \"Custom\" not defined")))
122+ Expect(writer).To(BeNil())
123+ })
124+ })
125+ })
126+})
pkg/app/pagewriter/sign_in_page.gonew file+66
@@ -0,0 +1,66 @@
1+package pagewriter
2+
3+import (
4+ "html/template"
5+ "net/http"
6+
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
8+)
9+
10+// signInPageWriter is used to render sign-in pages.
11+type signInPageWriter struct {
12+ // Template is the sign-in page HTML template.
13+ template *template.Template
14+
15+ // errorPageWriter is used to render an error if there are problems with rendering the sign-in page.
16+ errorPageWriter *errorPageWriter
17+
18+ // ProxyPrefix is the prefix under which OAuth2 Proxy pages are served.
19+ proxyPrefix string
20+
21+ // ProviderName is the name of the provider that should be displayed on the login button.
22+ providerName string
23+
24+ // SignInMessage is the messge displayed above the login button.
25+ signInMessage string
26+
27+ // Footer is the footer to be displayed at the bottom of the page.
28+ // If not set, a default footer will be used.
29+ footer string
30+
31+ // Version is the OAuth2 Proxy version to be used in the default footer.
32+ version string
33+
34+ // DisplayLoginForm determines whether or not the basic auth password form is displayed on the sign-in page.
35+ displayLoginForm bool
36+}
37+
38+// WriteSignInPage writes the sign-in page to the given response writer.
39+// It uses the redirectURL to be able to set the final destination for the user post login.
40+func (s *signInPageWriter) WriteSignInPage(rw http.ResponseWriter, redirectURL string) {
41+ // We allow unescaped template.HTML since it is user configured options
42+ /* #nosec G203 */
43+ t := struct {
44+ ProviderName string
45+ SignInMessage template.HTML
46+ CustomLogin bool
47+ Redirect string
48+ Version string
49+ ProxyPrefix string
50+ Footer template.HTML
51+ }{
52+ ProviderName: s.providerName,
53+ SignInMessage: template.HTML(s.signInMessage),
54+ CustomLogin: s.displayLoginForm,
55+ Redirect: redirectURL,
56+ Version: s.version,
57+ ProxyPrefix: s.proxyPrefix,
58+ Footer: template.HTML(s.footer),
59+ }
60+
61+ err := s.template.Execute(rw, t)
62+ if err != nil {
63+ logger.Printf("Error rendering sign-in template: %v", err)
64+ s.errorPageWriter.WriteErrorPage(rw, http.StatusInternalServerError, redirectURL, err.Error())
65+ }
66+}
pkg/app/pagewriter/sign_in_page_test.gonew file+61
@@ -0,0 +1,61 @@
1+package pagewriter
2+
3+import (
4+ "html/template"
5+ "io/ioutil"
6+ "net/http/httptest"
7+
8+ . "github.com/onsi/ginkgo"
9+ . "github.com/onsi/gomega"
10+)
11+
12+var _ = Describe("SignIn Page Writer", func() {
13+ var signInPage *signInPageWriter
14+
15+ BeforeEach(func() {
16+ errorTmpl, err := template.New("").Parse("{{.Title}}")
17+ Expect(err).ToNot(HaveOccurred())
18+ errorPage := &errorPageWriter{
19+ template: errorTmpl,
20+ }
21+
22+ tmpl, err := template.New("").Parse("{{.ProxyPrefix}} {{.ProviderName}} {{.SignInMessage}} {{.Footer}} {{.Version}} {{.Redirect}} {{.CustomLogin}}")
23+ Expect(err).ToNot(HaveOccurred())
24+
25+ signInPage = &signInPageWriter{
26+ template: tmpl,
27+ errorPageWriter: errorPage,
28+ proxyPrefix: "/prefix/",
29+ providerName: "My Provider",
30+ signInMessage: "Sign In Here",
31+ footer: "Custom Footer Text",
32+ version: "v0.0.0-test",
33+ displayLoginForm: true,
34+ }
35+ })
36+
37+ Context("WriteSignInPage", func() {
38+ It("Writes the template to the response writer", func() {
39+ recorder := httptest.NewRecorder()
40+ signInPage.WriteSignInPage(recorder, "/redirect")
41+
42+ body, err := ioutil.ReadAll(recorder.Result().Body)
43+ Expect(err).ToNot(HaveOccurred())
44+ Expect(string(body)).To(Equal("/prefix/ My Provider Sign In Here Custom Footer Text v0.0.0-test /redirect true"))
45+ })
46+
47+ It("Writes an error if the template can't be rendered", func() {
48+ // Overwrite the template with something bad
49+ tmpl, err := template.New("").Parse("{{.Unknown}}")
50+ Expect(err).ToNot(HaveOccurred())
51+ signInPage.template = tmpl
52+
53+ recorder := httptest.NewRecorder()
54+ signInPage.WriteSignInPage(recorder, "/redirect")
55+
56+ body, err := ioutil.ReadAll(recorder.Result().Body)
57+ Expect(err).ToNot(HaveOccurred())
58+ Expect(string(body)).To(Equal("Internal Server Error"))
59+ })
60+ })
61+})
pkg/app/templates.go → pkg/app/pagewriter/templates.gorenamed+3−3
@@ -1,4 +1,4 @@
1−package app
1+package pagewriter
22
33import (
44 "fmt"
@@ -206,10 +206,10 @@ const (
206206{{end}}`
207207)
208208
209−// LoadTemplates adds the Sign In and Error templates from the custom template
209+// loadTemplates adds the Sign In and Error templates from the custom template
210210// directory, or uses the defaults if they do not exist or the custom directory
211211// is not provided.
212−func LoadTemplates(customDir string) (*template.Template, error) {
212+func loadTemplates(customDir string) (*template.Template, error) {
213213 t := template.New("").Funcs(template.FuncMap{
214214 "ToUpper": strings.ToUpper,
215215 "ToLower": strings.ToLower,
pkg/app/templates_test.go → pkg/app/pagewriter/templates_test.gorenamed+12−12
@@ -1,4 +1,4 @@
1−package app
1+package pagewriter
22
33import (
44 "bytes"
@@ -21,16 +21,16 @@ var _ = Describe("Templates", func() {
2121
2222 templateHTML := `{{.TestString}} {{.TestString | ToLower}} {{.TestString | ToUpper}}`
2323 signInFile := filepath.Join(customDir, signInTemplateName)
24− Expect(ioutil.WriteFile(signInFile, []byte(templateHTML), 0666)).To(Succeed())
24+ Expect(ioutil.WriteFile(signInFile, []byte(templateHTML), 0600)).To(Succeed())
2525 errorFile := filepath.Join(customDir, errorTemplateName)
26− Expect(ioutil.WriteFile(errorFile, []byte(templateHTML), 0666)).To(Succeed())
26+ Expect(ioutil.WriteFile(errorFile, []byte(templateHTML), 0600)).To(Succeed())
2727 })
2828
2929 AfterEach(func() {
3030 Expect(os.RemoveAll(customDir)).To(Succeed())
3131 })
3232
33− Context("LoadTemplates", func() {
33+ Context("loadTemplates", func() {
3434 var data interface{}
3535 var t *template.Template
3636
@@ -73,7 +73,7 @@ var _ = Describe("Templates", func() {
7373 Context("With no custom directory", func() {
7474 BeforeEach(func() {
7575 var err error
76− t, err = LoadTemplates("")
76+ t, err = loadTemplates("")
7777 Expect(err).ToNot(HaveOccurred())
7878 })
7979
@@ -94,7 +94,7 @@ var _ = Describe("Templates", func() {
9494 Context("With both templates", func() {
9595 BeforeEach(func() {
9696 var err error
97− t, err = LoadTemplates(customDir)
97+ t, err = loadTemplates(customDir)
9898 Expect(err).ToNot(HaveOccurred())
9999 })
100100
@@ -116,7 +116,7 @@ var _ = Describe("Templates", func() {
116116 Expect(os.Remove(filepath.Join(customDir, errorTemplateName))).To(Succeed())
117117
118118 var err error
119− t, err = LoadTemplates(customDir)
119+ t, err = loadTemplates(customDir)
120120 Expect(err).ToNot(HaveOccurred())
121121 })
122122
@@ -138,7 +138,7 @@ var _ = Describe("Templates", func() {
138138 Expect(os.Remove(filepath.Join(customDir, signInTemplateName))).To(Succeed())
139139
140140 var err error
141− t, err = LoadTemplates(customDir)
141+ t, err = loadTemplates(customDir)
142142 Expect(err).ToNot(HaveOccurred())
143143 })
144144
@@ -158,11 +158,11 @@ var _ = Describe("Templates", func() {
158158 Context("With an invalid sign_in template", func() {
159159 BeforeEach(func() {
160160 signInFile := filepath.Join(customDir, signInTemplateName)
161− Expect(ioutil.WriteFile(signInFile, []byte("{{"), 0666))
161+ Expect(ioutil.WriteFile(signInFile, []byte("{{"), 0600))
162162 })
163163
164164 It("Should return an error when loading templates", func() {
165− t, err := LoadTemplates(customDir)
165+ t, err := loadTemplates(customDir)
166166 Expect(err).To(MatchError(HavePrefix("could not add Sign In template:")))
167167 Expect(t).To(BeNil())
168168 })
@@ -171,11 +171,11 @@ var _ = Describe("Templates", func() {
171171 Context("With an invalid error template", func() {
172172 BeforeEach(func() {
173173 errorFile := filepath.Join(customDir, errorTemplateName)
174− Expect(ioutil.WriteFile(errorFile, []byte("{{"), 0666))
174+ Expect(ioutil.WriteFile(errorFile, []byte("{{"), 0600))
175175 })
176176
177177 It("Should return an error when loading templates", func() {
178− t, err := LoadTemplates(customDir)
178+ t, err := loadTemplates(customDir)
179179 Expect(err).To(MatchError(HavePrefix("could not add Error template:")))
180180 Expect(t).To(BeNil())
181181 })