Commit c1705718 authored by ale's avatar ale
Browse files

Setup HTTP routes in the Service constructors

This way the constructors have more control over which base Service
features to use: in the specific case, the admin API does not need
CSRF protection.
parent 15c3ef5a
Pipeline #417 passed with stages
in 1 minute and 19 seconds
......@@ -16,7 +16,6 @@ import (
log "github.com/Sirupsen/logrus"
"github.com/google/subcommands"
"github.com/gorilla/csrf"
"github.com/gorilla/securecookie"
hydra "github.com/ory/hydra/sdk"
"github.com/prometheus/client_golang/prometheus/promhttp"
......@@ -203,6 +202,7 @@ func (c *serverCommand) run(ctx context.Context) error {
CookieEncKey: []byte(c.cookieEncKey),
ActionAuthKey: []byte(c.actionAuthKey),
ActionEncKey: []byte(c.actionEncKey),
CSRFSecret: []byte(c.csrfSecret),
InsecureCookies: c.insecureCookies,
Database: database,
Template: htmlTpl,
......@@ -219,11 +219,9 @@ func (c *serverCommand) run(ctx context.Context) error {
}
func (c *serverCommand) runServer(app *idpapp.App) error {
csrfWrapper := csrf.Protect(securecookie.GenerateRandomKey(32), csrf.Secure(false))
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(c.staticDir))))
http.Handle("/metrics", promhttp.Handler())
http.Handle("/", csrfWrapper(app.HTTPHandler()))
http.Handle("/", app.HTTPHandler())
// Start the HTTP/HTTPS server.
s := &http.Server{
......
......@@ -31,12 +31,22 @@ type Service struct {
// NewService returns a new Service.
func NewService(base *web.Service, hc *hydra.Client, actionMgr *idp.ActionManager, publicURL string) *Service {
return &Service{
s := &Service{
Service: base,
publicURL: publicURL,
actions: actionMgr,
hc: hc,
}
m := mux.NewRouter()
m.Handle("/admin/api/list_users", s.access("admin.users", "read", http.HandlerFunc(s.handleListUsers))).Methods("POST")
m.Handle("/admin/api/get_user", s.access("admin.users", "read", http.HandlerFunc(s.handleGetUser))).Methods("POST")
m.Handle("/admin/api/create_user", s.access("admin.users", "write", http.HandlerFunc(s.handleCreateUser))).Methods("POST")
m.Handle("/admin/api/update_user", s.access("admin.users", "write", http.HandlerFunc(s.handleUpdateUser))).Methods("POST")
s.Router.PathPrefix("/admin/api/").Handler(s.InstrumentHandler("admin", m))
return s
}
type adminSubjectKeyType int
......@@ -306,14 +316,3 @@ func (s *Service) handleListUsers(w http.ResponseWriter, r *http.Request) {
}
s.RenderJSON(w, r, ListUsersResponse{Users: users})
}
func (s *Service) AddRoutes(root *mux.Router) {
m := mux.NewRouter()
m.Handle("/admin/api/list_users", s.access("admin.users", "read", http.HandlerFunc(s.handleListUsers))).Methods("POST")
m.Handle("/admin/api/get_user", s.access("admin.users", "read", http.HandlerFunc(s.handleGetUser))).Methods("POST")
m.Handle("/admin/api/create_user", s.access("admin.users", "write", http.HandlerFunc(s.handleCreateUser))).Methods("POST")
m.Handle("/admin/api/update_user", s.access("admin.users", "write", http.HandlerFunc(s.handleUpdateUser))).Methods("POST")
root.PathPrefix("/admin/api/").Handler(s.InstrumentHandler("admin", m))
}
......@@ -6,6 +6,7 @@ import (
"strings"
ttemplate "text/template"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
hydra "github.com/ory/hydra/sdk"
......@@ -55,6 +56,7 @@ type Config struct {
SenderName string
Database idp.Database
RatelimitConfig []byte
CSRFSecret []byte
}
// App is the main login / consent / identity provider application.
......@@ -63,15 +65,16 @@ type App struct {
idp *consent.Service
acct *mgmt.Service
admin *admin.Service
router *mux.Router
}
// New creates a new application.
func New(hc *hydra.Client, config *Config) (*App, error) {
// Build a bunch of global objects that are used by the various services.
rlConfig, err := web.ParseRatelimitConfig(config.RatelimitConfig)
if err != nil {
return nil, err
}
if config.Mailer == nil {
config.Mailer = &sendmailBackend{}
}
......@@ -80,41 +83,51 @@ func New(hc *hydra.Client, config *Config) (*App, error) {
actionMgr := idp.NewActionManager(config.ActionAuthKey, config.ActionEncKey)
publicURL := strings.TrimRight(config.PublicURL, "/")
svcBase := web.NewService(config.Database, config.Template, store, mailer, config.InsecureCookies)
// Create the root HTTP Router.
router := mux.NewRouter()
loginSrv := login.NewService(svcBase, publicURL, rlConfig, config.InsecureCookies)
acct := mgmt.NewService(svcBase, actionMgr)
admin := admin.NewService(svcBase, hc, actionMgr, publicURL)
// If CSRF protection is enabled (it's only disabled in
// testing), wrap requests with the CSRF wrapper.
csrfWrapper := func(h http.Handler) http.Handler { return h }
if config.CSRFSecret != nil {
csrfWrapper = csrf.Protect(config.CSRFSecret, csrf.Secure(!config.InsecureCookies))
}
// Create the main Service providing access to the common
// objects to the various services.
svcBase := web.NewService(config.Database, config.Template, router, csrfWrapper, store, mailer, config.InsecureCookies)
// Create the services. Note: order is somewhat important: the
// consent service must be created before the login
// service. This is because the consent service defines HTTP
// routes with a prefix that is a subdirectory of the other.
idpSrv, err := consent.NewService(svcBase, store, hc, config.InsecureCookies)
if err != nil {
return nil, err
}
loginSrv := login.NewService(svcBase, publicURL, rlConfig, config.InsecureCookies)
acct := mgmt.NewService(svcBase, actionMgr)
admin := admin.NewService(svcBase, hc, actionMgr, publicURL)
// Install the homepage redirector.
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/account/overview", http.StatusFound)
})
return &App{
login: loginSrv,
idp: idpSrv,
acct: acct,
admin: admin,
router: router,
}, nil
}
// HTTPHandler returns the http.Handler for this web application.
func (a *App) HTTPHandler() http.Handler {
r := mux.NewRouter()
// Order is important here, sigh...
a.idp.AddRoutes(r)
a.login.AddRoutes(r)
a.acct.AddRoutes(r)
a.admin.AddRoutes(r)
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/account/overview", http.StatusFound)
})
return promhttp.InstrumentHandlerInFlight(inFlightRequests,
promhttp.InstrumentHandlerCounter(totalRequests,
withStandardHeaders(r)))
withStandardHeaders(a.router)))
}
func withStandardHeaders(h http.Handler) http.Handler {
......
......@@ -42,6 +42,13 @@ func NewService(base *web.Service, store sessions.Store, hc *hydra.Client, insec
if err := s.provider.Connect(); err != nil {
return nil, err
}
m := mux.NewRouter()
m.HandleFunc("/auth/idp/challenge", s.handleChallengeGET).Methods("GET")
m.HandleFunc("/auth/idp/consent", s.handleConsentGET).Methods("GET")
m.Handle("/auth/idp/consent", http.HandlerFunc(s.handleConsentPOST)).Methods("POST")
s.Router.PathPrefix("/auth/idp/").Handler(s.CSRF(s.InstrumentHandler("consent", s.LoggedIn(m))))
return s, nil
}
......@@ -181,11 +188,3 @@ func (s *Service) handleConsentPOST(w http.ResponseWriter, r *http.Request) {
}
}
}
func (s *Service) AddRoutes(root *mux.Router) {
m := mux.NewRouter()
m.HandleFunc("/auth/idp/challenge", s.handleChallengeGET).Methods("GET")
m.HandleFunc("/auth/idp/consent", s.handleConsentGET).Methods("GET")
m.Handle("/auth/idp/consent", http.HandlerFunc(s.handleConsentPOST)).Methods("POST")
root.PathPrefix("/auth/idp/").Handler(s.InstrumentHandler("consent", s.LoggedIn(m)))
}
......@@ -70,7 +70,7 @@ type Service struct {
// NewService returns a new Service.
func NewService(base *web.Service, publicURL string, rlConfig web.RatelimitConfig, insecureCookies bool) *Service {
return &Service{
s := &Service{
Service: base,
loginSessionOptions: &sessions.Options{
Path: "/auth/",
......@@ -83,6 +83,19 @@ func NewService(base *web.Service, publicURL string, rlConfig web.RatelimitConfi
ipFailedLoginBL: rlConfig.NewBlacklist("failed_login_ip_bl"),
userLoginRL: rlConfig.NewRatelimiter("login_user_rl"),
}
m := mux.NewRouter()
m.HandleFunc("/auth/login", s.handleLoginGET).Methods("GET")
m.Handle("/auth/login", s.withLoginSession(s.handleLoginPOST, sessionStateInit)).Methods("POST")
m.Handle("/auth/login_otp", s.withLoginSession(s.handleOTP, sessionState2FA)).Methods("GET", "POST")
m.Handle("/auth/login_u2f", s.withLoginSession(s.handleU2F, sessionState2FA)).Methods("GET")
m.Handle("/auth/login_u2f/redirect", s.withLoginSession(s.handleU2FPostAuthRedirect, sessionStateAuthenticated)).Methods("POST")
m.Handle("/auth/u2f/sign_request", s.withLoginSession(s.signU2FRequest, sessionState2FA)).Methods("GET")
m.Handle("/auth/u2f/sign_response", s.withLoginSession(s.signU2FResponse, sessionState2FA)).Methods("POST")
s.Router.PathPrefix("/auth/").Handler(s.CSRF(s.InstrumentHandler("login", m)))
return s
}
// Return an existing or new loginSession.
......@@ -448,16 +461,3 @@ func (s *Service) signU2FResponse(w http.ResponseWriter, r *http.Request, ls *lo
//s.successfulLogin(w, r, user, txn)
s.RenderJSON(w, r, map[string]string{"status": "success"})
}
func (s *Service) AddRoutes(root *mux.Router) {
m := mux.NewRouter()
m.HandleFunc("/auth/login", s.handleLoginGET).Methods("GET")
m.Handle("/auth/login", s.withLoginSession(s.handleLoginPOST, sessionStateInit)).Methods("POST")
m.Handle("/auth/login_otp", s.withLoginSession(s.handleOTP, sessionState2FA)).Methods("GET", "POST")
m.Handle("/auth/login_u2f", s.withLoginSession(s.handleU2F, sessionState2FA)).Methods("GET")
m.Handle("/auth/login_u2f/redirect", s.withLoginSession(s.handleU2FPostAuthRedirect, sessionStateAuthenticated)).Methods("POST")
m.Handle("/auth/u2f/sign_request", s.withLoginSession(s.signU2FRequest, sessionState2FA)).Methods("GET")
m.Handle("/auth/u2f/sign_response", s.withLoginSession(s.signU2FResponse, sessionState2FA)).Methods("POST")
root.PathPrefix("/auth/").Handler(s.InstrumentHandler("login", m))
}
......@@ -18,10 +18,38 @@ type Service struct {
// NewService creates a new account management web service.
func NewService(base *web.Service, actionMgr *idp.ActionManager) *Service {
return &Service{
s := &Service{
Service: base,
actions: actionMgr,
}
m := mux.NewRouter()
// Account management.
m.Handle("/account/overview", s.withUser(s.handleOverview))
m.Handle("/account/password_change", s.withUser(s.handlePasswordChange))
m.Handle("/account/u2f", s.withUser(s.handleU2FManagement))
m.Handle("/account/u2f/register_request", s.withUser(s.handleU2FRegisterRequest))
m.HandleFunc("/account/u2f/register_response", s.handleU2FRegisterResponse)
m.Handle("/account/otp", s.withUser(s.handleOTPManagement))
m.Handle("/account/otp/enable", s.withUser(s.handleOTPEnable))
m.Handle("/account/otp/reset", s.withUser(s.handleOTPReset))
m.Handle("/account/edit_profile", s.withUser(s.handleEditProfile))
// Email change + verification workflow.
//m.Handle("/account/email_change", s.withUser(s.handleEmailChange))
//m.Handle("/account/callback/email_change", s.WithTransaction(s.withUserAction(emailChangeActionName, s.handleEmailChangeAction)))
// Password reset workflow.
s.Router.Handle("/account/password_reset", http.HandlerFunc(s.handlePasswordResetRequest))
s.Router.Handle("/account/callback/password_reset", s.withUserAction(passwordResetActionName, s.handlePasswordResetAction))
// Account activation workflow
s.Router.Handle("/account/callback/activation", s.withUserAction(idp.AccountActivationActionName, s.handleAccountActivationAction))
s.Router.PathPrefix("/account/").Handler(s.CSRF(s.InstrumentHandler("mgmt", s.LoggedIn(m))))
return s
}
// Wrapper for user-specific handlers. Account management handlers run
......@@ -174,32 +202,3 @@ func (s *Service) handleEditProfile(w http.ResponseWriter, r *http.Request, txn
s.RenderTemplate(w, r, "account_edit_profile.html", tplCtx)
return nil
}
// AddRoutes adds the service routes to a mux.Router.
func (s *Service) AddRoutes(root *mux.Router) {
m := mux.NewRouter()
// Account management.
m.Handle("/account/overview", s.withUser(s.handleOverview))
m.Handle("/account/password_change", s.withUser(s.handlePasswordChange))
m.Handle("/account/u2f", s.withUser(s.handleU2FManagement))
m.Handle("/account/u2f/register_request", s.withUser(s.handleU2FRegisterRequest))
m.HandleFunc("/account/u2f/register_response", s.handleU2FRegisterResponse)
m.Handle("/account/otp", s.withUser(s.handleOTPManagement))
m.Handle("/account/otp/enable", s.withUser(s.handleOTPEnable))
m.Handle("/account/otp/reset", s.withUser(s.handleOTPReset))
m.Handle("/account/edit_profile", s.withUser(s.handleEditProfile))
// Email change + verification workflow.
//m.Handle("/account/email_change", s.withUser(s.handleEmailChange))
//m.Handle("/account/callback/email_change", s.WithTransaction(s.withUserAction(emailChangeActionName, s.handleEmailChangeAction)))
// Password reset workflow.
root.Handle("/account/password_reset", http.HandlerFunc(s.handlePasswordResetRequest))
root.Handle("/account/callback/password_reset", s.withUserAction(passwordResetActionName, s.handlePasswordResetAction))
// Account activation workflow
root.Handle("/account/callback/activation", s.withUserAction(idp.AccountActivationActionName, s.handleAccountActivationAction))
root.PathPrefix("/account/").Handler(s.InstrumentHandler("mgmt", s.LoggedIn(m)))
}
......@@ -12,6 +12,7 @@ import (
log "github.com/Sirupsen/logrus"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
......@@ -34,10 +35,12 @@ type Service struct {
DB idp.Database
Store sessions.Store
Mailer idp.Mailer
CSRF func(http.Handler) http.Handler
Router *mux.Router
}
// NewService returns a new Service object.
func NewService(db idp.Database, tpl *template.Template, store sessions.Store, mailer idp.Mailer, insecureCookies bool) *Service {
func NewService(db idp.Database, tpl *template.Template, router *mux.Router, csrfWrapper func(http.Handler) http.Handler, store sessions.Store, mailer idp.Mailer, insecureCookies bool) *Service {
return &Service{
tpl: tpl,
authSessionOptions: &sessions.Options{
......@@ -48,9 +51,13 @@ func NewService(db idp.Database, tpl *template.Template, store sessions.Store, m
DB: db,
Store: store,
Mailer: mailer,
Router: router,
CSRF: csrfWrapper,
}
}
// InstrumentHandler wraps a service-level http.Handler with request
// latency instrumentation.
func (s *Service) InstrumentHandler(name string, h http.Handler) http.Handler {
// Dynamic registration will be invoked multiple times in
// tests, so handle AlreadyRegisteredError properly and re-use
......
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