Skip to content

Commit 34b084b

Browse files
crewjamsolodyagin
andauthored
feat: allow login page customization (crewjam#597)
based on crewjam#493 Co-authored-by: Sergey Solodyagin <[email protected]>
1 parent e9fe44b commit 34b084b

File tree

3 files changed

+41
-32
lines changed

3 files changed

+41
-32
lines changed

samlidp/samlidp.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package samlidp
55
import (
66
"crypto"
77
"crypto/x509"
8+
"html/template"
89
"net/http"
910
"net/url"
1011
"regexp"
@@ -19,12 +20,13 @@ import (
1920

2021
// Options represent the parameters to New() for creating a new IDP server
2122
type Options struct {
22-
URL url.URL
23-
Key crypto.PrivateKey
24-
Signer crypto.Signer
25-
Logger logger.Interface
26-
Certificate *x509.Certificate
27-
Store Store
23+
URL url.URL
24+
Key crypto.PrivateKey
25+
Signer crypto.Signer
26+
Logger logger.Interface
27+
Certificate *x509.Certificate
28+
Store Store
29+
LoginFormTemplate *template.Template
2830
}
2931

3032
// Server represents an IDP server. The server provides the following URLs:
@@ -39,11 +41,12 @@ type Options struct {
3941
// /shortcuts - RESTful interface to Shortcut objects
4042
type Server struct {
4143
http.Handler
42-
idpConfigMu sync.RWMutex // protects calls into the IDP
43-
logger logger.Interface
44-
serviceProviders map[string]*saml.EntityDescriptor
45-
IDP saml.IdentityProvider // the underlying IDP
46-
Store Store // the data store
44+
idpConfigMu sync.RWMutex // protects calls into the IDP
45+
logger logger.Interface
46+
serviceProviders map[string]*saml.EntityDescriptor
47+
IDP saml.IdentityProvider // the underlying IDP
48+
Store Store // the data store
49+
LoginFormTemplate *template.Template
4750
}
4851

4952
// New returns a new Server
@@ -69,8 +72,9 @@ func New(opts Options) (*Server, error) {
6972
MetadataURL: metadataURL,
7073
SSOURL: ssoURL,
7174
},
72-
logger: logr,
73-
Store: opts.Store,
75+
logger: logr,
76+
Store: opts.Store,
77+
LoginFormTemplate: opts.LoginFormTemplate,
7478
}
7579

7680
s.IDP.SessionProvider = s

samlidp/session.go

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"encoding/hex"
66
"encoding/json"
77
"fmt"
8+
"html/template"
89
"net/http"
9-
"text/template"
1010
"time"
1111

1212
"golang.org/x/crypto/bcrypt"
@@ -39,13 +39,13 @@ func (s *Server) GetSession(w http.ResponseWriter, r *http.Request, req *saml.Id
3939
user := User{}
4040
if err := s.Store.Get(fmt.Sprintf("/users/%s", r.PostForm.Get("user")), &user); err != nil {
4141
s.logger.Printf("ERROR: User '%s' doesn't exists", r.PostForm.Get("user"))
42-
s.sendLoginForm(w, r, req, "Invalid username or password")
42+
s.sendLoginForm(w, req, "Invalid username or password")
4343
return nil
4444
}
4545

4646
if err := bcrypt.CompareHashAndPassword(user.HashedPassword, []byte(r.PostForm.Get("password"))); err != nil {
4747
s.logger.Printf("ERROR: Invalid password for user '%s'", r.PostForm.Get("user"))
48-
s.sendLoginForm(w, r, req, "Invalid username or password")
48+
s.sendLoginForm(w, req, "Invalid username or password")
4949
return nil
5050
}
5151

@@ -86,39 +86,44 @@ func (s *Server) GetSession(w http.ResponseWriter, r *http.Request, req *saml.Id
8686
session := &saml.Session{}
8787
if err := s.Store.Get(fmt.Sprintf("/sessions/%s", sessionCookie.Value), session); err != nil {
8888
if err == ErrNotFound {
89-
s.sendLoginForm(w, r, req, "")
89+
s.sendLoginForm(w, req, "")
9090
return nil
9191
}
9292
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
9393
return nil
9494
}
9595

9696
if saml.TimeNow().After(session.ExpireTime) {
97-
s.sendLoginForm(w, r, req, "")
97+
s.sendLoginForm(w, req, "")
9898
return nil
9999
}
100100
return session
101101
}
102102

103-
s.sendLoginForm(w, r, req, "")
103+
s.sendLoginForm(w, req, "")
104104
return nil
105105
}
106106

107+
var defaultLoginFormTemplate = template.Must(template.New("saml-post-form").Parse(`` +
108+
`<html>` +
109+
`<p>{{.Toast}}</p>` +
110+
`<form method="post" action="{{.URL}}">` +
111+
`<input type="text" name="user" placeholder="user" value="" />` +
112+
`<input type="password" name="password" placeholder="password" value="" />` +
113+
`<input type="hidden" name="SAMLRequest" value="{{.SAMLRequest}}" />` +
114+
`<input type="hidden" name="RelayState" value="{{.RelayState}}" />` +
115+
`<input type="submit" value="Log In" />` +
116+
`</form>` +
117+
`</html>`))
118+
107119
// sendLoginForm produces a form which requests a username and password and directs the user
108120
// back to the IDP authorize URL to restart the SAML login flow, this time establishing a
109121
// session based on the credentials that were provided.
110-
func (s *Server) sendLoginForm(w http.ResponseWriter, _ *http.Request, req *saml.IdpAuthnRequest, toast string) {
111-
tmpl := template.Must(template.New("saml-post-form").Parse(`` +
112-
`<html>` +
113-
`<p>{{.Toast}}</p>` +
114-
`<form method="post" action="{{.URL}}">` +
115-
`<input type="text" name="user" placeholder="user" value="" />` +
116-
`<input type="password" name="password" placeholder="password" value="" />` +
117-
`<input type="hidden" name="SAMLRequest" value="{{.SAMLRequest}}" />` +
118-
`<input type="hidden" name="RelayState" value="{{.RelayState}}" />` +
119-
`<input type="submit" value="Log In" />` +
120-
`</form>` +
121-
`</html>`))
122+
func (s *Server) sendLoginForm(w http.ResponseWriter, req *saml.IdpAuthnRequest, toast string) {
123+
tmpl := s.LoginFormTemplate
124+
if tmpl == nil {
125+
tmpl = defaultLoginFormTemplate
126+
}
122127
data := struct {
123128
Toast string
124129
URL string
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<html><p></p><form method="post" action="https://idp.example.com/sso"><input type="text" name="user" placeholder="user" value="" /><input type="password" name="password" placeholder="password" value="" /><input type="hidden" name="SAMLRequest" value="PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0iaWQtMDAwMjA0MDYwODBhMGMwZTEwMTIxNDE2MTgxYTFjMWUyMDIyMjQyNiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMTItMDFUMDE6NTc6MDlaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9pZHAuZXhhbXBsZS5jb20vc3NvIiBBc3NlcnRpb25Db25zdW1lclNlcnZpY2VVUkw9Imh0dHBzOi8vc3AuZXhhbXBsZS5jb20vc2FtbDIvYWNzIiBQcm90b2NvbEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLVBPU1QiPjxzYW1sOklzc3VlciBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI+aHR0cHM6Ly9zcC5leGFtcGxlLmNvbS9zYW1sMi9tZXRhZGF0YTwvc2FtbDpJc3N1ZXI+PHNhbWxwOk5hbWVJRFBvbGljeSBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCIgQWxsb3dDcmVhdGU9InRydWUiLz48L3NhbWxwOkF1dGhuUmVxdWVzdD4=" /><input type="hidden" name="RelayState" value="frob" /><input type="submit" value="Log In" /></form></html>
1+
<html><p></p><form method="post" action="https://idp.example.com/sso"><input type="text" name="user" placeholder="user" value="" /><input type="password" name="password" placeholder="password" value="" /><input type="hidden" name="SAMLRequest" value="PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0iaWQtMDAwMjA0MDYwODBhMGMwZTEwMTIxNDE2MTgxYTFjMWUyMDIyMjQyNiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMTItMDFUMDE6NTc6MDlaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9pZHAuZXhhbXBsZS5jb20vc3NvIiBBc3NlcnRpb25Db25zdW1lclNlcnZpY2VVUkw9Imh0dHBzOi8vc3AuZXhhbXBsZS5jb20vc2FtbDIvYWNzIiBQcm90b2NvbEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLVBPU1QiPjxzYW1sOklzc3VlciBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI&#43;aHR0cHM6Ly9zcC5leGFtcGxlLmNvbS9zYW1sMi9tZXRhZGF0YTwvc2FtbDpJc3N1ZXI&#43;PHNhbWxwOk5hbWVJRFBvbGljeSBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCIgQWxsb3dDcmVhdGU9InRydWUiLz48L3NhbWxwOkF1dGhuUmVxdWVzdD4=" /><input type="hidden" name="RelayState" value="frob" /><input type="submit" value="Log In" /></form></html>

0 commit comments

Comments
 (0)