diff --git a/server/bindata.go b/server/bindata.go index 4c23b49bb2825cfc1460936782d1144e0f7fbaf7..0b4491930492ff71f0f864fc05619fb274c98959 100644 --- a/server/bindata.go +++ b/server/bindata.go @@ -1107,7 +1107,7 @@ func staticJsU2fJs() (*asset, error) { var _templatesLogin_otpHtml = []byte(`{{template "header" .}} - <form class="form-signin" action="/login" method="post"> + <form class="form-signin" action="{{.URLPrefix}}/login" method="post"> {{.CSRFField}} <h1 class="form-signin-heading">Sign In / OTP</h1> @@ -1138,14 +1138,14 @@ func templatesLogin_otpHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/login_otp.html", size: 529, mode: os.FileMode(420), modTime: time.Unix(1535013418, 0)} + info := bindataFileInfo{name: "templates/login_otp.html", size: 543, mode: os.FileMode(420), modTime: time.Unix(1541234791, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _templatesLogin_passwordHtml = []byte(`{{template "header" .}} - <form class="form-signin" action="/login" method="post"> + <form class="form-signin" action="{{.URLPrefix}}/login" method="post"> {{.CSRFField}} <h1 class="form-signin-heading">Sign In</h1> @@ -1198,14 +1198,14 @@ func templatesLogin_passwordHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/login_password.html", size: 1074, mode: os.FileMode(420), modTime: time.Unix(1535013418, 0)} + info := bindataFileInfo{name: "templates/login_password.html", size: 1088, mode: os.FileMode(420), modTime: time.Unix(1541234797, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _templatesLogin_u2fHtml = []byte(`{{template "header" .}} - <form class="form-signin" id="u2fForm" action="/login" method="post"> + <form class="form-signin" id="u2fForm" action="{{.URLPrefix}}/login" method="post"> {{.CSRFField}} <input type="hidden" id="u2fResponseField" name="u2f_response" value=""> @@ -1238,7 +1238,7 @@ func templatesLogin_u2fHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/login_u2f.html", size: 498, mode: os.FileMode(420), modTime: time.Unix(1535013418, 0)} + info := bindataFileInfo{name: "templates/login_u2f.html", size: 512, mode: os.FileMode(420), modTime: time.Unix(1541234815, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1286,7 +1286,7 @@ var _templatesLogoutHtml = []byte(`{{template "header" .}} </div> {{else}} - <form class="form-signin" action="/logout" method="post"> + <form class="form-signin" action="{{.URLPrefix}}/logout" method="post"> {{.CSRFField}} <h1 class="form-signin-heading">Sign Out</h1> @@ -1319,7 +1319,7 @@ func templatesLogoutHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/logout.html", size: 1510, mode: os.FileMode(420), modTime: time.Unix(1535013418, 0)} + info := bindataFileInfo{name: "templates/logout.html", size: 1524, mode: os.FileMode(420), modTime: time.Unix(1541234913, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1330,8 +1330,8 @@ var _templatesPageHtml = []byte(`{{define "header"}}<!DOCTYPE html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> {{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}} - <link rel="stylesheet" href="/static/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M"> - <link rel="stylesheet" href="/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o"> + <link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M"> + <link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o"> <title>Sign In</title> </head> @@ -1342,15 +1342,15 @@ var _templatesPageHtml = []byte(`{{define "header"}}<!DOCTYPE html> {{define "footer"}} </div> - <script src="/static/js/jquery-3.2.1.min.js" integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f"></script> - <script src="/static/js/popper-1.11.0.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4"></script> - <script src="/static/js/bootstrap-4.0.0-beta.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1"></script> + <script src="{{.URLPrefix}}/static/js/jquery-3.2.1.min.js" integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f"></script> + <script src="{{.URLPrefix}}/static/js/popper-1.11.0.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4"></script> + <script src="{{.URLPrefix}}/static/js/bootstrap-4.0.0-beta.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1"></script> {{if .U2FSignRequest}} - <script src="/static/js/u2f-api.js" integrity="sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR"></script> - <script src="/static/js/u2f.js" integrity="sha384-7zZy25ajTABErGlCQgcyRDpQDS9QVZv9o+95IfvCjWftQe20f411F1a39Ge5xmCe"></script> + <script src="{{.URLPrefix}}/static/js/u2f-api.js" integrity="sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR"></script> + <script src="{{.URLPrefix}}/static/js/u2f.js" integrity="sha384-7zZy25ajTABErGlCQgcyRDpQDS9QVZv9o+95IfvCjWftQe20f411F1a39Ge5xmCe"></script> {{end}} {{if .IncludeLogoutScripts}} - <script src="/static/js/logout.js" integrity="sha384-lChVngGLNFXetIJTSxc+scDpi1vsBL+7Xa4r2uZpQFP/6Y2z9eCDXe/Y4IUdklRD"></script> + <script src="{{.URLPrefix}}/static/js/logout.js" integrity="sha384-lChVngGLNFXetIJTSxc+scDpi1vsBL+7Xa4r2uZpQFP/6Y2z9eCDXe/Y4IUdklRD"></script> {{end}} </body> </html> @@ -1367,7 +1367,7 @@ func templatesPageHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/page.html", size: 1617, mode: os.FileMode(420), modTime: time.Unix(1541228908, 0)} + info := bindataFileInfo{name: "templates/page.html", size: 1729, mode: os.FileMode(420), modTime: time.Unix(1541234783, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/server/config.go b/server/config.go index f63600cb136843a4f852573c710e976c0bb9c324..6400138acc53043138a9d178ac906f761f56cf1c 100644 --- a/server/config.go +++ b/server/config.go @@ -4,6 +4,7 @@ import ( "errors" "log" "regexp" + "strings" "time" "github.com/gorilla/securecookie" @@ -34,6 +35,7 @@ type Config struct { CSRFSecret string `yaml:"csrf_secret"` AuthService string `yaml:"auth_service"` U2FAppID string `yaml:"u2f_app_id"` + URLPrefix string `yaml:"url_path_prefix"` DeviceManager *device.Config `yaml:"device_manager"` KeyStore *clientutil.BackendConfig `yaml:"keystore"` @@ -57,6 +59,12 @@ func (c *Config) valid() error { if c.AuthService == "" { return errors.New("auth_service is empty") } + if c.U2FAppID != "" && !strings.HasPrefix(c.U2FAppID, "https://") { + return errors.New("u2f_app_id does not start with https://") + } + if c.URLPrefix != "" && !strings.HasPrefix(c.URLPrefix, "/") { + return errors.New("url_path_prefix does not start with /") + } // Some things we can autogenerate, but for testing purposes // only. Print a warning. diff --git a/server/http.go b/server/http.go index d69f2632080b5bbc30573423ad4ad589eed7f7b1..7256f635a6cdaee2b69743c99b553c58874204bf 100644 --- a/server/http.go +++ b/server/http.go @@ -90,6 +90,7 @@ type Server struct { keystore ksclient.Client csrfSecret []byte tpl *template.Template + urlPrefix string } func sl2bl(sl []string) [][]byte { @@ -102,19 +103,21 @@ func sl2bl(sl []string) [][]byte { // New returns a new Server. func New(loginService *LoginService, authClient authclient.Client, config *Config) (*Server, error) { + urlPrefix := strings.TrimRight(config.URLPrefix, "/") sessionSecrets := sl2bl(config.SessionSecrets) store := sessions.NewCookieStore(sessionSecrets...) store.Options = &sessions.Options{ HttpOnly: true, Secure: true, MaxAge: 0, - Path: "/", + Path: urlPrefix + "/", } s := &Server{ authSessionLifetime: defaultAuthSessionLifetime, authSessionStore: store, loginService: loginService, + urlPrefix: urlPrefix, tpl: parseEmbeddedTemplates(), } if config.CSRFSecret != "" { @@ -137,7 +140,7 @@ func New(loginService *LoginService, authClient authclient.Client, config *Confi if err != nil { return nil, err } - s.loginHandler = newLoginHandler(s.loginCallback, devMgr, authClient, config.AuthService, config.U2FAppID, s.tpl, sessionSecrets...) + s.loginHandler = newLoginHandler(s.loginCallback, devMgr, authClient, config.AuthService, config.U2FAppID, config.URLPrefix, s.tpl, sessionSecrets...) return s, nil } @@ -182,17 +185,10 @@ func (h *Server) withAuth(f func(http.ResponseWriter, *http.Request, *authSessio if err := httpSession.Save(req, w); err != nil { log.Printf("error saving session: %v", err) } - http.Redirect(w, req, makeLoginURL(req), http.StatusFound) + http.Redirect(w, req, h.loginHandler.makeLoginURL(req), http.StatusFound) }) } -func makeLoginURL(req *http.Request) string { - // Just concatenate path and raw request string. - v := make(url.Values) - v.Set("r", req.URL.Path+"?"+req.URL.RawQuery) - return "/login?" + v.Encode() -} - // Homepage handler. Authorizes an authenticated user to a service by // signing a token with the user's identity. The client is redirected // back to the service, with the signed token. @@ -308,14 +304,24 @@ func (h *Server) handleExchange(w http.ResponseWriter, req *http.Request) { io.WriteString(w, token) // nolint } +func (h *Server) urlFor(path string) string { + return h.urlPrefix + path +} + // Handler returns the http.Handler for the SSO server application. func (h *Server) Handler() http.Handler { // The root HTTP handler. This must be a gorilla/mux.Router since // sessions depend on it. + // + // If a URL prefix is set, we can't just add a StripPrefix in + // front of everything, as the handlers need access to the + // actual full request URL, so we just inject the prefix + // everywhere. root := mux.NewRouter() // Serve static content to anyone. - root.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(&assetfs.AssetFS{ + staticPath := h.urlFor("/static/") + root.PathPrefix(staticPath).Handler(http.StripPrefix(staticPath, http.FileServer(&assetfs.AssetFS{ Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, @@ -325,8 +331,8 @@ func (h *Server) Handler() http.Handler { // Build the main IDP application router, with optional CSRF // protection. m := http.NewServeMux() - m.Handle("/login", h.loginHandler) - m.Handle("/logout", h.withAuth(h.handleLogout)) + m.Handle(h.urlFor("/login"), h.loginHandler) + m.Handle(h.urlFor("/logout"), h.withAuth(h.handleLogout)) idph := http.Handler(m) if h.csrfSecret != nil { idph = csrf.Protect(h.csrfSecret)(idph) @@ -338,9 +344,9 @@ func (h *Server) Handler() http.Handler { ssoh := h.withAuth(h.handleHomepage) userh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == "GET" && r.URL.Path == "/": + case r.Method == "GET" && r.URL.Path == h.urlFor("/"): ssoh.ServeHTTP(w, r) - case r.URL.Path == "/exchange": + case r.URL.Path == h.urlFor("/exchange"): h.handleExchange(w, r) default: idph.ServeHTTP(w, r) @@ -348,7 +354,7 @@ func (h *Server) Handler() http.Handler { }) // User-facing routes require cache-busting and CSP headers. - root.PathPrefix("/").Handler(withDynamicHeaders(userh)) + root.PathPrefix(h.urlFor("/")).Handler(withDynamicHeaders(userh)) return root } diff --git a/server/login.go b/server/login.go index 97fa5b146bd1993b0806265d28a735a29e1b4bb1..3685e8cefffcd1021598fc0bc90eef428c9ecf00 100644 --- a/server/login.go +++ b/server/login.go @@ -9,6 +9,7 @@ import ( "html/template" "log" "net/http" + "net/url" "strings" "time" @@ -73,6 +74,7 @@ type loginHandler struct { authClient authclient.Client authService string u2fAppID string + urlPrefix string devMgr *device.Manager loginCallback loginCallbackFunc loginSessionStore sessions.Store @@ -81,7 +83,7 @@ type loginHandler struct { // NewLoginHandler will wrap an http.Handler with the login workflow, // invoking it only on successful login. -func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authClient authclient.Client, authService, u2fAppID string, tpl *template.Template, keyPairs ...[]byte) *loginHandler { +func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authClient authclient.Client, authService, u2fAppID, urlPrefix string, tpl *template.Template, keyPairs ...[]byte) *loginHandler { store := sessions.NewCookieStore(keyPairs...) store.Options = &sessions.Options{ HttpOnly: true, @@ -92,6 +94,7 @@ func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authCl authClient: authClient, authService: authService, u2fAppID: u2fAppID, + urlPrefix: strings.TrimRight(urlPrefix, "/"), devMgr: devMgr, loginCallback: okHandler, loginSessionStore: store, @@ -291,10 +294,19 @@ func (l *loginHandler) makeAuthRequest(w http.ResponseWriter, req *http.Request, return l.authClient.Authenticate(req.Context(), &ar) } +// Return a (relative) URL that will redirect the user to the login +// page and set the continue token to the original requested URL. +func (l *loginHandler) makeLoginURL(req *http.Request) string { + v := make(url.Values) + v.Set("r", req.URL.Path+"?"+req.URL.RawQuery) + return fmt.Sprintf("%s/login?%s", l.urlPrefix, v.Encode()) +} + // Renders a template to a buffer, with a return value that makes it // convenient to use in login handlers. func (l *loginHandler) executeTemplateToBuffer(req *http.Request, templateName string, data map[string]interface{}) (loginState, []byte, error) { data["CSRFField"] = csrf.TemplateField(req) + data["URLPrefix"] = l.urlPrefix var buf bytes.Buffer if err := l.tpl.ExecuteTemplate(&buf, templateName, data); err != nil { return loginStateNone, nil, err diff --git a/server/sri.py b/server/sri.py index ba1a60cf5569bd6d07806531c4298a906edb855d..725dd07c6a5ffdbdcd04743aa1d41289130d94f4 100755 --- a/server/sri.py +++ b/server/sri.py @@ -12,7 +12,7 @@ import sys from hashlib import sha384 -script_rx = re.compile(r'<(?:script|link rel="stylesheet")[^>]*(?:src|href)="([^"]+)"[^>]*>') +script_rx = re.compile(r'<(?:script|link rel="stylesheet")[^>]*(?:src|href)="(?:{{.URLPrefix}})?([^"]+)"[^>]*>') integrity_rx = re.compile(r' +integrity="[^"]*"') diff --git a/server/templates/login_otp.html b/server/templates/login_otp.html index 8418655d1f22d4f11b0b9701f04250e55ce45389..3830e5e7b5fabe65fe4732892de42e5a3ded6f1b 100644 --- a/server/templates/login_otp.html +++ b/server/templates/login_otp.html @@ -1,6 +1,6 @@ {{template "header" .}} - <form class="form-signin" action="/login" method="post"> + <form class="form-signin" action="{{.URLPrefix}}/login" method="post"> {{.CSRFField}} <h1 class="form-signin-heading">Sign In / OTP</h1> diff --git a/server/templates/login_password.html b/server/templates/login_password.html index ff37635a80448ddba8422343591f29b7ba9d3abb..78e34e92e0d2aec92e7cc14b7e803abacf2cc848 100644 --- a/server/templates/login_password.html +++ b/server/templates/login_password.html @@ -1,6 +1,6 @@ {{template "header" .}} - <form class="form-signin" action="/login" method="post"> + <form class="form-signin" action="{{.URLPrefix}}/login" method="post"> {{.CSRFField}} <h1 class="form-signin-heading">Sign In</h1> diff --git a/server/templates/login_u2f.html b/server/templates/login_u2f.html index 9fbd538fe15b34b9a2bdbc5f0b1a6b078d6272f9..a023df44d29a7effa8f568d636d722bf944e7b4f 100644 --- a/server/templates/login_u2f.html +++ b/server/templates/login_u2f.html @@ -1,6 +1,6 @@ {{template "header" .}} - <form class="form-signin" id="u2fForm" action="/login" method="post"> + <form class="form-signin" id="u2fForm" action="{{.URLPrefix}}/login" method="post"> {{.CSRFField}} <input type="hidden" id="u2fResponseField" name="u2f_response" value=""> diff --git a/server/templates/logout.html b/server/templates/logout.html index 502358d940ac2e232c706cd7578aeab98e440f9f..76e90f6c3779c23477e747f45884f308432dadb6 100644 --- a/server/templates/logout.html +++ b/server/templates/logout.html @@ -41,7 +41,7 @@ </div> {{else}} - <form class="form-signin" action="/logout" method="post"> + <form class="form-signin" action="{{.URLPrefix}}/logout" method="post"> {{.CSRFField}} <h1 class="form-signin-heading">Sign Out</h1> diff --git a/server/templates/page.html b/server/templates/page.html index e02befdf8d0d812d5eb6c48ec26ddd10ff6701ba..275d550ea9c901b14f6162a28193d5071efa94aa 100644 --- a/server/templates/page.html +++ b/server/templates/page.html @@ -4,8 +4,8 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> {{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}} - <link rel="stylesheet" href="/static/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M"> - <link rel="stylesheet" href="/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o"> + <link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M"> + <link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o"> <title>Sign In</title> </head> @@ -16,15 +16,15 @@ {{define "footer"}} </div> - <script src="/static/js/jquery-3.2.1.min.js" integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f"></script> - <script src="/static/js/popper-1.11.0.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4"></script> - <script src="/static/js/bootstrap-4.0.0-beta.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1"></script> + <script src="{{.URLPrefix}}/static/js/jquery-3.2.1.min.js" integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f"></script> + <script src="{{.URLPrefix}}/static/js/popper-1.11.0.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4"></script> + <script src="{{.URLPrefix}}/static/js/bootstrap-4.0.0-beta.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1"></script> {{if .U2FSignRequest}} - <script src="/static/js/u2f-api.js" integrity="sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR"></script> - <script src="/static/js/u2f.js" integrity="sha384-7zZy25ajTABErGlCQgcyRDpQDS9QVZv9o+95IfvCjWftQe20f411F1a39Ge5xmCe"></script> + <script src="{{.URLPrefix}}/static/js/u2f-api.js" integrity="sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR"></script> + <script src="{{.URLPrefix}}/static/js/u2f.js" integrity="sha384-7zZy25ajTABErGlCQgcyRDpQDS9QVZv9o+95IfvCjWftQe20f411F1a39Ge5xmCe"></script> {{end}} {{if .IncludeLogoutScripts}} - <script src="/static/js/logout.js" integrity="sha384-lChVngGLNFXetIJTSxc+scDpi1vsBL+7Xa4r2uZpQFP/6Y2z9eCDXe/Y4IUdklRD"></script> + <script src="{{.URLPrefix}}/static/js/logout.js" integrity="sha384-lChVngGLNFXetIJTSxc+scDpi1vsBL+7Xa4r2uZpQFP/6Y2z9eCDXe/Y4IUdklRD"></script> {{end}} </body> </html>