From 86a8869523aaa5534b0685b72411351d07a03c1e Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Sat, 3 Nov 2018 09:12:27 +0000
Subject: [PATCH] Support deploying the SSO server below a URL path prefix

Likely fixes issue #7 (untested).
---
 server/bindata.go                    | 34 ++++++++++++-------------
 server/config.go                     |  8 ++++++
 server/http.go                       | 38 ++++++++++++++++------------
 server/login.go                      | 14 +++++++++-
 server/sri.py                        |  2 +-
 server/templates/login_otp.html      |  2 +-
 server/templates/login_password.html |  2 +-
 server/templates/login_u2f.html      |  2 +-
 server/templates/logout.html         |  2 +-
 server/templates/page.html           | 16 ++++++------
 10 files changed, 73 insertions(+), 47 deletions(-)

diff --git a/server/bindata.go b/server/bindata.go
index 4c23b49..0b44919 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 f63600c..6400138 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 d69f263..7256f63 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 97fa5b1..3685e8c 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 ba1a60c..725dd07 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 8418655..3830e5e 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 ff37635..78e34e9 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 9fbd538..a023df4 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 502358d..76e90f6 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 e02befd..275d550 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>
-- 
GitLab