Commit 86a88695 authored by ale's avatar ale

Support deploying the SSO server below a URL path prefix

Likely fixes issue #7 (untested).
parent e502d9e2
Pipeline #1476 passed with stages
in 1 minute and 33 seconds
......@@ -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
}
......
......@@ -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.
......
......@@ -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
}
......
......@@ -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
......
......@@ -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="[^"]*"')
......
{{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>
......
{{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>
......
{{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="">
......
......@@ -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>
......
......@@ -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>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment