mirror of
https://github.com/nais/wonderwall.git
synced 2026-05-12 19:37:04 +00:00
If autologin is enabled, check for headers that indicate that the request is a navigation request
and respond appropriately.
A navigation request is assumed to match all of the following:
- uses the GET HTTP method
- either:
- a) sends the fetch metadata headers, specifically
`Sec-Fetch-Mode=navigate` and `Sec-Fetch-Dest=document`, or (if
unsupported by the browser)
- b) sends the `Accept` header with a value that contains
`text/html` (which most browsers do by default for navigation
requests, the exception being IE8 AFAIK)
Non-navigation requests (e.g. fetch / xhr / ajax requests) will receive a
401 Unauthorized, with the Location header set to the login endpoint.
The redirect parameter is also set to point back to the URL found in the
Referer header (though with the scheme and host removed to only allow
redirects relative to the origin host.)
With this fix, autologin will also intercept requests other than GET.
This is to improve the security posture of upstreams that assume that autologin
enforces authentication for all methods.
Fixes #156.
194 lines
5.6 KiB
Go
194 lines
5.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
urllib "net/url"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/nais/wonderwall/pkg/config"
|
|
"github.com/nais/wonderwall/pkg/crypto"
|
|
"github.com/nais/wonderwall/pkg/handler/acr"
|
|
"github.com/nais/wonderwall/pkg/handler/autologin"
|
|
"github.com/nais/wonderwall/pkg/ingress"
|
|
logentry "github.com/nais/wonderwall/pkg/middleware"
|
|
openidclient "github.com/nais/wonderwall/pkg/openid/client"
|
|
"github.com/nais/wonderwall/pkg/router"
|
|
"github.com/nais/wonderwall/pkg/router/paths"
|
|
"github.com/nais/wonderwall/pkg/session"
|
|
"github.com/nais/wonderwall/pkg/url"
|
|
)
|
|
|
|
var _ router.Source = &SSOProxy{}
|
|
|
|
type SSOProxy struct {
|
|
AcrHandler *acr.Handler
|
|
AutoLogin *autologin.AutoLogin
|
|
Config *config.Config
|
|
Ingresses *ingress.Ingresses
|
|
Redirect url.Redirect
|
|
SSOServerURL *urllib.URL
|
|
SSOServerReverseProxy *ReverseProxy
|
|
SessionReader session.Reader
|
|
UpstreamProxy *ReverseProxy
|
|
}
|
|
|
|
func NewSSOProxy(cfg *config.Config, crypter crypto.Crypter) (*SSOProxy, error) {
|
|
autoLogin, err := autologin.New(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ingresses, err := ingress.ParseIngresses(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sessionReader, err := session.NewReader(cfg, crypter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
serverURL, err := urllib.ParseRequestURI(cfg.SSO.ServerURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing sso server url: %w", err)
|
|
}
|
|
|
|
upstream := &urllib.URL{
|
|
Host: cfg.UpstreamHost,
|
|
Scheme: "http",
|
|
}
|
|
|
|
return &SSOProxy{
|
|
AcrHandler: acr.NewHandler(cfg),
|
|
AutoLogin: autoLogin,
|
|
Config: cfg,
|
|
Ingresses: ingresses,
|
|
Redirect: url.NewSSOProxyRedirect(ingresses),
|
|
SSOServerURL: serverURL,
|
|
SSOServerReverseProxy: NewReverseProxy(serverURL, false),
|
|
SessionReader: sessionReader,
|
|
UpstreamProxy: NewReverseProxy(upstream, true),
|
|
}, nil
|
|
}
|
|
|
|
func (s *SSOProxy) GetAcrHandler() *acr.Handler {
|
|
return s.AcrHandler
|
|
}
|
|
|
|
func (s *SSOProxy) GetAutoLogin() *autologin.AutoLogin {
|
|
return s.AutoLogin
|
|
}
|
|
|
|
func (s *SSOProxy) GetIngresses() *ingress.Ingresses {
|
|
return s.Ingresses
|
|
}
|
|
|
|
func (s *SSOProxy) GetPath(r *http.Request) string {
|
|
return GetPath(r, s.GetIngresses())
|
|
}
|
|
|
|
func (s *SSOProxy) GetRedirect() url.Redirect {
|
|
return s.Redirect
|
|
}
|
|
|
|
func (s *SSOProxy) GetSession(r *http.Request) (*session.Session, error) {
|
|
return s.SessionReader.Get(r)
|
|
}
|
|
|
|
func (s *SSOProxy) GetSSOServerURL() *urllib.URL {
|
|
u := *s.SSOServerURL
|
|
return &u
|
|
}
|
|
|
|
func (s *SSOProxy) Login(w http.ResponseWriter, r *http.Request) {
|
|
logger := logentry.LogEntryFrom(r)
|
|
|
|
target := s.GetSSOServerURL()
|
|
targetQuery := target.Query()
|
|
|
|
// set default query parameters
|
|
if len(s.Config.OpenID.ACRValues) > 0 {
|
|
targetQuery.Set(openidclient.SecurityLevelURLParameter, s.Config.OpenID.ACRValues)
|
|
}
|
|
if len(s.Config.OpenID.UILocales) > 0 {
|
|
targetQuery.Set(openidclient.LocaleURLParameter, s.Config.OpenID.UILocales)
|
|
}
|
|
|
|
// override default query parameters, if provided in request
|
|
reqQuery := r.URL.Query()
|
|
if reqQuery.Has(openidclient.SecurityLevelURLParameter) {
|
|
targetQuery.Set(openidclient.SecurityLevelURLParameter, reqQuery.Get(openidclient.SecurityLevelURLParameter))
|
|
}
|
|
if reqQuery.Has(openidclient.LocaleURLParameter) {
|
|
targetQuery.Set(openidclient.LocaleURLParameter, reqQuery.Get(openidclient.LocaleURLParameter))
|
|
}
|
|
|
|
target.RawQuery = targetQuery.Encode()
|
|
|
|
canonicalRedirect := s.Redirect.Canonical(r)
|
|
ssoServerLoginURL := url.Login(target, canonicalRedirect)
|
|
|
|
logger.WithFields(log.Fields{
|
|
"redirect_to": ssoServerLoginURL,
|
|
"redirect_after_login": canonicalRedirect,
|
|
}).Info("login: redirecting to sso server")
|
|
|
|
http.Redirect(w, r, ssoServerLoginURL, http.StatusFound)
|
|
}
|
|
|
|
func (s *SSOProxy) LoginCallback(w http.ResponseWriter, r *http.Request) {
|
|
ingressPath := s.GetPath(r)
|
|
login := url.LoginRelative(ingressPath, ingressPath)
|
|
http.Redirect(w, r, login, http.StatusFound)
|
|
}
|
|
|
|
func (s *SSOProxy) Logout(w http.ResponseWriter, r *http.Request) {
|
|
target := s.GetSSOServerURL()
|
|
|
|
// only set a canonical redirect if it was provided in the request as a query parameter
|
|
canonicalRedirect := r.URL.Query().Get(url.RedirectQueryParameter)
|
|
if canonicalRedirect != "" {
|
|
canonicalRedirect = s.Redirect.Canonical(r)
|
|
}
|
|
ssoServerLogoutURL := url.Logout(target, canonicalRedirect)
|
|
|
|
logentry.LogEntryFrom(r).WithFields(log.Fields{
|
|
"redirect_to": ssoServerLogoutURL,
|
|
"redirect_after_logout": canonicalRedirect,
|
|
}).Info("logout: redirecting to sso server")
|
|
|
|
http.Redirect(w, r, ssoServerLogoutURL, http.StatusFound)
|
|
}
|
|
|
|
func (s *SSOProxy) LogoutCallback(w http.ResponseWriter, r *http.Request) {
|
|
target := s.GetSSOServerURL().JoinPath(paths.OAuth2, paths.Logout)
|
|
http.Redirect(w, r, target.String(), http.StatusFound)
|
|
}
|
|
|
|
func (s *SSOProxy) LogoutFrontChannel(w http.ResponseWriter, r *http.Request) {
|
|
r.URL.Path = paths.OAuth2 + paths.LogoutFrontChannel
|
|
s.SSOServerReverseProxy.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (s *SSOProxy) LogoutLocal(w http.ResponseWriter, r *http.Request) {
|
|
r.URL.Path = paths.OAuth2 + paths.LogoutLocal
|
|
s.SSOServerReverseProxy.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (s *SSOProxy) Session(w http.ResponseWriter, r *http.Request) {
|
|
r.URL.Path = paths.OAuth2 + paths.Session
|
|
s.SSOServerReverseProxy.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (s *SSOProxy) SessionRefresh(w http.ResponseWriter, r *http.Request) {
|
|
r.URL.Path = paths.OAuth2 + paths.Session + paths.Refresh
|
|
s.SSOServerReverseProxy.ServeHTTP(w, r)
|
|
}
|
|
|
|
// Wildcard proxies all requests to an upstream server.
|
|
func (s *SSOProxy) Wildcard(w http.ResponseWriter, r *http.Request) {
|
|
s.UpstreamProxy.Handler(s, w, r)
|
|
}
|